diff --git a/recruitment/forms.py b/recruitment/forms.py index a2a9291..51085ad 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -302,13 +302,25 @@ class ApplicationForm(forms.ModelForm): "hiring_agency": forms.Select(attrs={"class": "form-select"}), } - def __init__(self, *args, **kwargs): + def __init__(self, *args,current_agency=None,current_job=None,**kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_method = "post" self.helper.form_class = "form-horizontal" self.helper.label_class = "col-md-3" self.helper.field_class = "col-md-9" + if current_agency: + # IMPORTANT: Replace 'agency' below with the actual field name + # on your Person model that links it back to the Agency model. + self.fields['person'].queryset = self.fields['person'].queryset.filter( + agency=current_agency + ) + self.fields['job'].queryset = self.fields['job'].queryset.filter( + pk=current_job.id + ) + self.fields['job'].initial = current_job + + self.fields['job'].widget.attrs['readonly'] = True # Make job field read-only if it's being pre-populated job_value = self.initial.get("job") @@ -1990,7 +2002,7 @@ class OnsiteLocationForm(forms.ModelForm): class InterviewEmailForm(forms.Form): subject = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'form-control'})) message_for_candidate = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) - message_for_agency = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) + message_for_agency = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}),required=False) message_for_participants = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs): @@ -2025,7 +2037,7 @@ class InterviewEmailForm(forms.Form): job_title = job.title agency_name = ( candidate.hiring_agency.name - if candidate.belong_to_agency and candidate.hiring_agency + if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency" ) diff --git a/recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py b/recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py new file mode 100644 index 0000000..9654118 --- /dev/null +++ b/recruitment/migrations/0003_jobposting_cv_zip_file_jobposting_zip_created.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-11-19 14:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0002_alter_jobposting_job_type_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='jobposting', + name='cv_zip_file', + field=models.FileField(blank=True, null=True, upload_to='job_zips/'), + ), + migrations.AddField( + model_name='jobposting', + name='zip_created', + field=models.BooleanField(default=False), + ), + ] diff --git a/recruitment/models.py b/recruitment/models.py index 58530a5..2cef9e2 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -243,6 +243,11 @@ class JobPosting(Base): help_text=_("Whether the job posting has been parsed by AI"), verbose_name=_("AI Parsed"), ) + # Field to store the generated zip file + cv_zip_file = models.FileField(upload_to='job_zips/', null=True, blank=True) + + # Field to track if the background task has completed + zip_created = models.BooleanField(default=False) class Meta: ordering = ["-created_at"] @@ -985,8 +990,12 @@ class Application(Base): content_type = ContentType.objects.get_for_model(self.__class__) return Document.objects.filter(content_type=content_type, object_id=self.id) - # @property - # def belong_to_agency(self): + @property + def belong_to_an_agency(self): + if self.hiring_agency: + return True + else: + return False diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 7a185d6..d67bdec 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -847,3 +847,52 @@ def email_success_hook(task): logger.error(f"Task ID {task.id} failed. Error: {task.result}") + + +import io +import zipfile +import os +from django.core.files.base import ContentFile +from django.conf import settings +from .models import Application, JobPosting # Import your models + +ALLOWED_EXTENSIONS = (".pdf", ".docx") + +def generate_and_save_cv_zip(job_posting_id): + """ + Generates a zip file of all CVs for a job posting and saves it to the job model. + """ + job = JobPosting.objects.get(id=job_posting_id) + entries = Application.objects.filter(job=job) + + zip_buffer = io.BytesIO() + + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: + for entry in entries: + if not entry.resume: + continue + + file_name = entry.resume.name.split("/")[-1] + file_name_lower = file_name.lower() + + if file_name_lower.endswith(ALLOWED_EXTENSIONS): + try: + with entry.resume.open("rb") as file_obj: + file_content = file_obj.read() + zf.writestr(file_name, file_content) + + except Exception as e: + # Log the error using Django's logging system if set up + print(f"Error processing file {file_name}: {e}") + continue + + # 4. Save the generated zip buffer to the JobPosting model + zip_buffer.seek(0) + zip_filename = f"all_cvs_for_{job.title}.zip" + + # Use ContentFile to save the bytes stream into the FileField + job.cv_zip_file.save(zip_filename, ContentFile(zip_buffer.read())) + job.zip_created = True # Assuming you added a BooleanField for tracking completion + job.save() + + return f"Successfully created zip for Job ID {job_posting_id}" \ No newline at end of file diff --git a/recruitment/urls.py b/recruitment/urls.py index 5b60895..2fbcb4d 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -23,7 +23,9 @@ urlpatterns = [ path("jobs//update/", views.edit_job, name="job_update"), # path('jobs//delete/', views., name='job_delete'), path('jobs//', views.job_detail, name='job_detail'), - path('jobs//download/cvs/', views.job_cvs_download, name='job_cvs_download'), + # path('jobs//download/cvs/', views.job_cvs_download, name='job_cvs_download'), + path('job//request-download/', views.request_cvs_download, name='request_cvs_download'), + path('job//download-ready/', views.download_ready_cvs, name='download_ready_cvs'), path('careers/',views.kaauh_career,name='kaauh_career'), diff --git a/recruitment/views.py b/recruitment/views.py index 1a01d9d..7c5545b 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -163,8 +163,8 @@ class PersonCreateView(CreateView): model = Person template_name = "people/create_person.html" form_class = PersonForm - # success_url = reverse_lazy("person_list") - + success_url = reverse_lazy("person_list") + print("from agency") def form_valid(self, form): if "HX-Request" in self.request.headers: instance = form.save() @@ -596,59 +596,88 @@ def job_detail(request, slug): return render(request, "jobs/job_detail.html", context) -ALLOWED_EXTENSIONS = (".pdf", ".docx") +# ALLOWED_EXTENSIONS = (".pdf", ".docx") -def job_cvs_download(request, slug): +# def job_cvs_download(request, slug): +# job = get_object_or_404(JobPosting, slug=slug) +# entries = Application.objects.filter(job=job) + +# # 2. Create an in-memory byte stream (BytesIO) +# zip_buffer = io.BytesIO() + +# # 3. Create the ZIP archive +# with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: +# for entry in entries: +# # Check if the file field has a file +# if not entry.resume: +# continue + +# # Get the file name and check extension (case-insensitive) +# file_name = entry.resume.name.split("/")[-1] +# file_name_lower = file_name.lower() + +# if file_name_lower.endswith(ALLOWED_EXTENSIONS): +# try: +# # Open the file object (rb is read binary) +# file_obj = entry.resume.open("rb") + +# # *** ROBUST METHOD: Read the content and write it to the ZIP *** +# file_content = file_obj.read() + +# # Write the file content directly to the ZIP archive +# zf.writestr(file_name, file_content) + +# file_obj.close() + +# except Exception as e: +# # Log the error but continue with the rest of the files +# print(f"Error processing file {file_name}: {e}") +# continue + +# # 4. Prepare the response +# zip_buffer.seek(0) + +# # 5. Create the HTTP response +# response = HttpResponse(zip_buffer.read(), content_type="application/zip") + +# # Set the header for the browser to download the file +# response["Content-Disposition"] = ( +# f'attachment; filename="all_cvs_for_{job.title}.zip"' +# ) + +# return response + +def request_cvs_download(request, slug): + """ + View to initiate the background task. + """ job = get_object_or_404(JobPosting, slug=slug) - entries = Application.objects.filter(job=job) - # 2. Create an in-memory byte stream (BytesIO) - zip_buffer = io.BytesIO() - - # 3. Create the ZIP archive - with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: - for entry in entries: - # Check if the file field has a file - if not entry.resume: - continue - - # Get the file name and check extension (case-insensitive) - file_name = entry.resume.name.split("/")[-1] - file_name_lower = file_name.lower() - - if file_name_lower.endswith(ALLOWED_EXTENSIONS): - try: - # Open the file object (rb is read binary) - file_obj = entry.resume.open("rb") - - # *** ROBUST METHOD: Read the content and write it to the ZIP *** - file_content = file_obj.read() - - # Write the file content directly to the ZIP archive - zf.writestr(file_name, file_content) - - file_obj.close() - - except Exception as e: - # Log the error but continue with the rest of the files - print(f"Error processing file {file_name}: {e}") - continue - - # 4. Prepare the response - zip_buffer.seek(0) - - # 5. Create the HTTP response - response = HttpResponse(zip_buffer.read(), content_type="application/zip") - - # Set the header for the browser to download the file - response["Content-Disposition"] = ( - f'attachment; filename="all_cvs_for_{job.title}.zip"' - ) - - return response + # Use async_task to run the function in the background + # Pass only simple arguments (like the job ID) + async_task('recruitment.tasks.generate_and_save_cv_zip', job.id) + + # Provide user feedback and redirect + messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.") + return redirect('job_detail', slug=slug) # Redirect back to the job detail page +def download_ready_cvs(request, slug): + """ + View to serve the file once it is ready. + """ + job = get_object_or_404(JobPosting, slug=slug) + if job.cv_zip_file and job.zip_created: + # Django FileField handles the HttpResponse and file serving easily + response = HttpResponse(job.cv_zip_file.read(), content_type="application/zip") + response["Content-Disposition"] = f'attachment; filename="{job.cv_zip_file.name.split("/")[-1]}"' + return response + else: + # File is not ready or doesn't exist + messages.warning(request, "The ZIP file is still being generated or an error occurred.") + return redirect('job_detail', slug=slug) + @login_required @staff_user_required def job_image_upload(request, slug): @@ -2938,16 +2967,17 @@ def staff_assignment_view(request, slug): applications = job.applications.all() if request.method == "POST": - form = StaffAssignmentForm(request.POST) + form = StaffAssignmentForm(request.POST, instance=job) + if form.is_valid(): - assignment = form.save(commit=False) + assignment = form.save() messages.success(request, f"Staff assigned to job '{job.title}' successfully!") return redirect("job_detail", slug=job.slug) else: messages.error(request, "Please correct the errors below.") else: - form = StaffAssignmentForm() - + form = StaffAssignmentForm(instance=job) + print(staff_users) context = { "job": job, "applications": applications, @@ -4206,6 +4236,8 @@ def agency_portal_submit_candidate_page(request, slug): assignment = get_object_or_404( AgencyJobAssignment.objects.select_related("agency", "job"), slug=slug ) + current_agency=assignment.agency + current_job=assignment.job if assignment.is_full: messages.error(request, "Maximum candidate limit reached for this assignment.") @@ -4230,9 +4262,9 @@ def agency_portal_submit_candidate_page(request, slug): hiring_agency=assignment.agency, job=assignment.job ).count() - form = ApplicationForm() + form = ApplicationForm(current_agency=current_agency,current_job=current_job) if request.method == "POST": - form = ApplicationForm(request.POST, request.FILES) + form = ApplicationForm(request.POST, request.FILES,current_agency=current_agency,current_job=current_job) if form.is_valid(): candidate = form.save(commit=False) @@ -5602,9 +5634,9 @@ def send_interview_email(request, slug): interview = get_object_or_404(ScheduledInterview, slug=slug) # 2. Retrieve the required data for the form's constructor - candidate = interview.candidate + candidate = interview.application job = interview.job - meeting = interview.zoom_meeting + meeting = interview.interview_location participants = list(interview.participants.all()) + list( interview.system_users.all() ) @@ -5625,7 +5657,7 @@ def send_interview_email(request, slug): meeting=meeting, job=job, ) - + if form.is_valid(): # 4. Extract cleaned data subject = form.cleaned_data["subject"] @@ -5635,6 +5667,8 @@ def send_interview_email(request, slug): # --- SEND EMAILS Candidate or agency--- if candidate.belong_to_an_agency: + email=candidate.hiring_agency.email + print(email) send_mail( subject, msg_agency, @@ -5647,7 +5681,7 @@ def send_interview_email(request, slug): subject, msg_candidate, settings.DEFAULT_FROM_EMAIL, - [candidate.email], + [candidate.person.email], fail_silently=False, ) @@ -5659,6 +5693,8 @@ def send_interview_email(request, slug): attachments=None, async_task_=True, # Changed to False to avoid pickle issues, from_interview=True, + job=job + ) if email_result["success"]: @@ -5694,6 +5730,13 @@ def send_interview_email(request, slug): f"Failed to send email: {email_result.get('message', 'Unknown error')}", ) return redirect("list_meetings") + else: + + error_msg = "Failed to send email. Please check the form for errors." + print(form.errors) + messages.error(request, error_msg) + return redirect("meeting_details", slug=meeting.slug) + return redirect("meeting_details", slug=meeting.slug) # def schedule_interview_location_form(request,slug): @@ -6015,13 +6058,13 @@ def meeting_details(request, slug): participant_form = InterviewParticpantsForm(instance=interview) - # email_form = InterviewEmailForm( - # candidate=candidate, - # external_participants=external_participants, # QuerySet of Participants - # system_participants=system_participants, # QuerySet of Users - # meeting=meeting, # ← This is InterviewLocation (e.g., ZoomMeetingDetails) - # job=job, - # ) + email_form = InterviewEmailForm( + candidate=candidate, + external_participants=external_participants, # QuerySet of Participants + system_participants=system_participants, # QuerySet of Users + meeting=meeting, # ← This is InterviewLocation (e.g., ZoomMeetingDetails) + job=job, + ) context = { 'meeting': meeting, @@ -6032,7 +6075,7 @@ def meeting_details(request, slug): 'system_participants': system_participants, 'total_participants': total_participants, 'form': participant_form, - # 'email_form': email_form, + 'email_form': email_form, } return render(request, 'interviews/detail_interview.html', context) diff --git a/templates/account/email_confirm.html b/templates/account/email_confirm.html index 7489e61..c1eda3b 100644 --- a/templates/account/email_confirm.html +++ b/templates/account/email_confirm.html @@ -1,23 +1,174 @@ -{% extends "base.html" %} -{% load i18n %} +{% load static i18n %} {% load account %} - -{% block title %}{% trans "Confirm Email Address" %}{% endblock %} - -{% block content %} -
+{% get_current_language_bidi as LANGUAGE_BIDI %} +{% get_current_language as LANGUAGE_CODE %} + + + + + + {% block title %}{% trans "Confirm Email Address" %}{% endblock %} - {# Centering the main header content #} -
-
-

{% trans "Account Verification" %}

-

{% trans "Verify your email to secure your account and unlock full features." %}

+ + + + + + + +
+
+

+ +
+
جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية
+
ومستشفى الملك عبدالله بن عبدالعزيز التخصصي
+
Princess Nourah bint Abdulrahman University
+
King Abdullah bin Abdulaziz University Hospital
+
+
+

+ Powered By TENHAL | تنحل
- -
-
-
+ +
+ +
+ + {# Global Header #} +
+

{% trans "Account Verification" %}

+

{% trans "Verify your email to secure your account and unlock full features." %}

+
+ +
{% with email as email %} @@ -27,11 +178,11 @@ {# ------------------- CONFIRMATION REQUEST (GET) - Success Theme ------------------- #} {% user_display confirmation.email_address.user as user_display %} - {# Changed icon to a checkmark for clarity #} + -

{% translate "Confirm Your Email Address" %}

+

{% trans "Confirm Your Email Address" %}

-

+

{% blocktrans with email as email %}Please confirm that **{{ email }}** is the correct email address for your account.{% endblocktrans %}

@@ -39,28 +190,27 @@
{% csrf_token %} -
{% else %} {# ------------------- CONFIRMATION FAILED (Error) - Danger Theme ------------------- #} - {# Changed icon to be more specific #} + -

{% translate "Verification Failed" %}

+

{% trans "Verification Failed" %}

- {% translate "The email confirmation link is expired or invalid." %} + {% trans "The email confirmation link is expired or invalid." %}

-

- {% translate "If you recently requested a link, please ensure you use the newest one. You can request a new verification email from your account settings." %} +

+ {% trans "If you recently requested a link, please ensure you use the newest one. You can request a new verification email from your account settings." %}

- - {% translate "Go to Settings" %} + + {% trans "Go to Settings" %} {% endif %} @@ -68,7 +218,10 @@ {% endwith %}
+
-
-{% endblock content %} \ No newline at end of file + + + + \ No newline at end of file diff --git a/templates/forms/form_submission_details.html b/templates/forms/form_submission_details.html index 013342b..7bd39c5 100644 --- a/templates/forms/form_submission_details.html +++ b/templates/forms/form_submission_details.html @@ -146,7 +146,7 @@
@@ -385,8 +391,8 @@ {% trans "Assign Staff Member" %} - - {% if job.staff_assignments.exists %} + + {% if job.assigned_to %}
{% trans "Current Assignments" %}
{% for assignment in job.staff_assignments.all %} diff --git a/templates/people/create_person.html b/templates/people/create_person.html index fbc6867..b93011b 100644 --- a/templates/people/create_person.html +++ b/templates/people/create_person.html @@ -146,18 +146,22 @@

- {% trans "Create New Person" %} + {% trans "Create New Applicant" %}

{% trans "Back to List" %} diff --git a/templates/people/person_detail.html b/templates/people/person_detail.html index 79c115a..61146a6 100644 --- a/templates/people/person_detail.html +++ b/templates/people/person_detail.html @@ -215,11 +215,14 @@ @@ -288,7 +291,7 @@
{% trans "Full Name" %}: - {{ person.get_full_name }} + {{ person.full_name|title }}
{% if person.first_name %} @@ -411,7 +414,7 @@
{% trans "Applications" %} - {{ person.applications.count }} + {{ person.applications.count }}
{% if person.applications %} @@ -429,7 +432,7 @@ {% trans "Applied" %}: {{ application.created_at|date:"d M Y" }}
- {{ application.stage }} + {{ application.stage }}
{% endfor %} @@ -449,7 +452,7 @@
{% trans "Documents" %} - {{ person.documents.count }} + {{ person.documents.count }}
{% if person.documents %} diff --git a/templates/people/update_person.html b/templates/people/update_person.html index 788cb56..e0f5fdc 100644 --- a/templates/people/update_person.html +++ b/templates/people/update_person.html @@ -169,23 +169,26 @@ diff --git a/templates/portal_base.html b/templates/portal_base.html index b3703a7..46d53d7 100644 --- a/templates/portal_base.html +++ b/templates/portal_base.html @@ -84,7 +84,7 @@ {% elif request.user.user_type == 'candidate' %} diff --git a/templates/recruitment/agency_assignment_detail.html b/templates/recruitment/agency_assignment_detail.html index 73e9310..3b011b9 100644 --- a/templates/recruitment/agency_assignment_detail.html +++ b/templates/recruitment/agency_assignment_detail.html @@ -9,265 +9,285 @@ :root { --kaauh-teal: #00636e; --kaauh-teal-dark: #004a53; - --kaauh-border: #eaeff3; - --kaauh-primary-text: #343a40; - --kaauh-success: #28a745; - --kaauh-info: #17a2b8; + --kaauh-border: #e9ecef; + --kaauh-primary-text: #212529; + --kaauh-success: #198754; + --kaauh-info: #0dcaf0; --kaauh-danger: #dc3545; --kaauh-warning: #ffc107; + --kaauh-bg-light: #f8f9fa; + } + + .text-primary-teal { + color: var(--kaauh-teal) !important; } .kaauh-card { - border: 1px solid var(--kaauh-border); + border: none; border-radius: 0.75rem; - box-shadow: 0 4px 12px rgba(0,0,0,0.06); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); background-color: white; + transition: transform 0.2s ease, box-shadow 0.2s ease; + } + + .kaauh-card:hover { + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); } .btn-main-action { background-color: var(--kaauh-teal); border-color: var(--kaauh-teal); color: white; - font-weight: 600; + font-weight: 500; + padding: 0.5rem 1.2rem; transition: all 0.2s ease; } + .btn-main-action:hover { background-color: var(--kaauh-teal-dark); border-color: var(--kaauh-teal-dark); - box-shadow: 0 4px 8px rgba(0,0,0,0.15); + color: white; + transform: translateY(-1px); + } + + .btn-white { + background-color: white; + border-color: var(--kaauh-border); + color: var(--kaauh-primary-text); + } + + .btn-white:hover { + background-color: var(--kaauh-bg-light); + border-color: #dee2e6; } .status-badge { font-size: 0.75rem; - padding: 0.3em 0.7em; - border-radius: 0.35rem; - font-weight: 700; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + } + + .status-ACTIVE { + background-color: #d1e7dd; + color: #0f5132; + } + + .status-EXPIRED { + background-color: #f8d7da; + color: #842029; + } + + .status-COMPLETED { + background-color: #cff4fc; + color: #055160; + } + + .status-CANCELLED { + background-color: #fff3cd; + color: #664d03; + } + + .bg-soft-primary { + background-color: rgba(13, 110, 253, 0.1); + } + + .bg-soft-info { + background-color: rgba(13, 202, 240, 0.1); + } + + .bg-soft-success { + background-color: rgba(25, 135, 84, 0.1); + } + + .avatar-circle { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + } + + .w-20 { + width: 20px; + text-align: center; } - .status-ACTIVE { background-color: var(--kaauh-success); color: white; } - .status-EXPIRED { background-color: var(--kaauh-danger); color: white; } - .status-COMPLETED { background-color: var(--kaauh-info); color: white; } - .status-CANCELLED { background-color: var(--kaauh-warning); color: #856404; } .progress-ring { - width: 120px; - height: 120px; - position: relative; - } - - .progress-ring-circle { - transition: stroke-dashoffset 0.35s; transform: rotate(-90deg); transform-origin: 50% 50%; } - .progress-ring-text { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 1.5rem; - font-weight: 700; - color: var(--kaauh-teal-dark); - } - - .message-item { - border-left: 4px solid var(--kaauh-teal); - background-color: #f8f9fa; - padding: 1rem; - margin-bottom: 1rem; - border-radius: 0 0.5rem 0.5rem 0; - } - - .message-item.unread { - border-left-color: var(--kaauh-info); - background-color: #e7f3ff; + .progress-ring-circle { + transition: stroke-dashoffset 0.5s ease-in-out; } {% endblock %} {% block content %} -
+
-
-
-

- - {{ assignment.agency.name }} - {{ assignment.job.title }} -

-

- {% trans "Assignment Details and Management" %} -

-
-
- - {% trans "Back to Assignments" %} - - - {% trans "Edit Assignment" %} - +
+
+
+
+ +

+ {{ assignment.job.title }} +

+
+ {{ assignment.agency.name }} + {{ + assignment.get_status_display }} +
+
+ +
-
- -
+
+ +
-
-
- - {% trans "Assignment Details" %} -
- -
-
-
- -
{{ assignment.agency.name }}
-
{{ assignment.agency.contact_person }}
-
-
- -
{{ assignment.job.title }}
-
{{ assignment.job.department }}
-
-
-
-
- -
- - {{ assignment.get_status_display }} - +
+
+
+ + {% trans "Assignment Details" %} +
+
+
+
+
+
+ +
{{ assignment.agency.name }}
+
{{ assignment.agency.contact_person }}
-
- -
- - {{ assignment.deadline_date|date:"Y-m-d H:i" }} +
+
+ +
{{ assignment.job.department }}
- {% if assignment.is_expired %} - +
+
+
+ +
+ {{ assignment.deadline_date|date:"M d, Y - H:i" }} +
+ {% if assignment.is_expired %} + {% trans "Expired" %} - {% endif %} + {% endif %} +
-
-
- - {% if assignment.admin_notes %} -
- -
{{ assignment.admin_notes }}
-
- {% endif %} - - -
- - {% comment %}
-
-
- - {% trans "Access Credentials" %} -
- -
- -
- - +
+
+ +
{{ assignment.created_at|date:"M d, Y" }}
+
-
- -
- - + {% if assignment.admin_notes %} +
+ +
+ {{ assignment.admin_notes }}
- -
- -
- - -
-
- -
- - {% trans "Share these credentials securely with the agency. They can use this information to log in and submit candidates." %} -
- - {% if access_link %} - - {% trans "View Access Links Details" %} - - {% endif %} -
-
{% endcomment %} - - -
-
-
- - {% trans "Submitted Candidates" %} ({{ total_candidates }}) -
- {% if access_link %} - - {% trans "Preview Portal" %} - {% endif %}
+
- {% if candidates %} + +
+
+
+ + {% trans "Submitted Candidates" %} + {{ total_candidates }} +
+ {% if access_link %} + + {% trans "Portal Preview" %} + + {% endif %} +
+
+ {% if candidates %}
- - +
+ - - - - - + + + + + {% for candidate in candidates %} - - - - - + + @@ -276,130 +296,103 @@
{% trans "Name" %}{% trans "Contact" %}{% trans "Stage" %}{% trans "Submitted" %}{% trans "Actions" %}{% trans "Candidate" + %}{% trans "Contact" %} + {% trans "Stage" %} + {% trans "Submitted" + %}{% trans + "Actions" %}
-
{{ candidate.name }}
-
-
-
{{ candidate.email }}
-
{{ candidate.phone }}
+
+
+
+ {{ candidate.name|slice:":2"|upper }} +
+
+
{{ candidate.name }}
+
- {{ candidate.get_stage_display }} - +
- {{ candidate.created_at|date:"Y-m-d H:i" }} +
{{ + candidate.email }}
+
{{ candidate.phone }}
+ + {{ + candidate.get_stage_display }} + + {{ candidate.created_at|date:"M d, Y" }} + + class="btn btn-sm btn-white border shadow-sm text-primary" + title="{% trans 'View Details' %}">
- {% else %} -
- -
{% trans "No candidates submitted yet" %}
-

- {% trans "Candidates will appear here once the agency submits them through their portal." %} + {% else %} +

+
+
+ +
+
+
{% trans "No candidates yet" %}
+

+ {% trans "Candidates submitted by the agency will appear here." %}

- {% endif %} + {% endif %} +
- -
+ +
-
-
- {% trans "Submission Progress" %} -
+
+
+
{% trans "Submission Goal" %}
-
-
- - - +
+ + + -
- {% widthratio total_candidates assignment.max_candidates 100 as progress %} - {{ progress|floatformat:0 }}% +
+
{{ total_candidates }}
+
{% trans "of" %} {{ assignment.max_candidates + }}
-
-
-
{{ total_candidates }}
-
/ {{ assignment.max_candidates }} {% trans "candidates" %}
-
- -
- {% widthratio total_candidates assignment.max_candidates 100 as progress %} -
+

+ {% trans "Candidates submitted" %} +

- -
-
- - {% trans "Actions" %} -
- -
- - {% trans "Send Message" %} - - - {% if assignment.is_active and not assignment.is_expired %} - - {% endif %} - - - {% trans "Edit Assignment" %} - +
+
+
{% trans "Quick Actions" %}
-
-
-
+
+
+ + {% trans "Send Message" %} + - - {% if messages_ %} -
-
- - {% trans "Recent Messages" %} -
- -
- {% for message in messages_|slice:":6" %} -
-
-
-
{{ message.subject }}
- {{ message.created_at|date:"Y-m-d H:i" }} -
-
- {% trans "From" %}: {{ message.sender.get_full_name }} -
-
{{ message.message|truncatewords:30 }}
- {% if not message.is_read %} - {% trans "New" %} + {% if assignment.is_active and not assignment.is_expired %} + {% endif %}
- {% endfor %}
- {% if messages_.count > 6 %} -
- - {% trans "View All Messages" %} - + + {% if messages_ %} +
+
+
{% trans "Recent Messages" %}
+ {% if messages_.count > 3 %} + {% trans "View All" %} + {% endif %}
+
+
+ {% for message in messages_|slice:":3" %} +
+
+ {{ message.sender.get_full_name }} + {{ message.created_at|date:"M d" + }} +
+
{{ message.subject }}
+
{{ message.message }}
+
+ {% endfor %} +
+
+
{% endif %}
- {% endif %} +
@@ -420,8 +413,8 @@ - + {% trans "Current deadline:" %} {{ assignment.deadline_date|date:"Y-m-d H:i" }} @@ -443,13 +436,13 @@ {% block customJS %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/recruitment/agency_portal_persons_list.html b/templates/recruitment/agency_portal_persons_list.html index 8e26f28..012f1ab 100644 --- a/templates/recruitment/agency_portal_persons_list.html +++ b/templates/recruitment/agency_portal_persons_list.html @@ -63,16 +63,16 @@

- {% trans "All Persons" %} + {% trans "All Applicants" %}

- {% trans "All persons who come through" %} {{ agency.name }} + {% trans "All applicants who come through" %} {{ agency.name }}

@@ -120,7 +120,7 @@
-
+
@@ -131,7 +131,7 @@
-
+
@@ -297,7 +297,7 @@ diff --git a/templates/recruitment/candidate_application_detail.html b/templates/recruitment/candidate_application_detail.html index 5622daf..bbb4272 100644 --- a/templates/recruitment/candidate_application_detail.html +++ b/templates/recruitment/candidate_application_detail.html @@ -5,31 +5,66 @@ {% block customCSS %} {% endblock %} {% block content %} -
- +
- -
+ +
-
+
-

+

{{ application.job.title }}

@@ -159,26 +212,25 @@

- - {{ application.get_stage_display }} - +
+ + {{ application.get_stage_display }} + +
-
- +
- -
+
{% trans "Applied" %}
- {% if application.stage in 'Exam,Interview,Document Review,Offer,Hired,Rejected' %} -
+
@@ -186,9 +238,8 @@
{% endif %} - {% if application.stage in 'Interview,Document Review,Offer,Hired,Rejected' %} -
+
@@ -196,9 +247,8 @@
{% endif %} - {% if application.stage in 'Document Review,Offer,Hired,Rejected' %} -
+
@@ -206,9 +256,8 @@
{% endif %} - {% if application.stage in 'Offer,Hired,Rejected' %} -
+
@@ -216,44 +265,42 @@
{% endif %} - {% if application.stage in 'Hired,Rejected' %} -
+
- +
-
{% trans "Hired" %}
+
{% trans "Final Status" %}
{% endif %}
- -
+
-
+
-
{% trans "Applied Date" %}
+
{% trans "Applied Date" %}

{{ application.created_at|date:"M d, Y" }}

-
+
-
{% trans "Department" %}
+
{% trans "Department" %}

{{ application.job.department|default:"-" }}

-
+
-
{% trans "Job Type" %}
+
{% trans "Job Type" %}

{{ application.get_job_type_display }}

-
+
-
{% trans "Location" %}
+
{% trans "Location" %}

{{ application.get_workplace_type_display }}

@@ -262,11 +309,44 @@
+ +
+
+
+
+ +
{% trans "Go to Dashboard" %}
+

{% trans "View all applications" %}

+ + {% trans "Dashboard" %} + +
+
+
- - + {% if application.resume %} +
+
+
+ +
{% trans "Download Resume" %}
+

{% trans "Get your submitted file" %}

+ + + {% trans "Download" %} + +
+
+
+ {% endif %} + + + +
{% if interviews %} -
+
@@ -275,11 +355,11 @@ {% trans "Interview Schedule" %}
-
+
{% if interviews %}
- - +
+ @@ -296,7 +376,7 @@
{% trans "Date" %} {% trans "Time" %}{{ interview.interview_time|time:"H:i" }} {% if interview.zoom_meeting %} - + {% trans "Remote" %} @@ -349,9 +429,8 @@ {% endif %} - {% if application.stage == "Document Review" %} -
+
@@ -370,11 +449,11 @@
-
+
{% if documents %}
- - +
+ @@ -390,7 +469,7 @@ {% if document.file %} + class="text-decoration-none text-primary-theme"> {{ document.get_document_type_display }} @@ -446,79 +525,17 @@ {% endif %} - +
-
-
-
- -
{% trans "Back to Dashboard" %}
-

{% trans "View all your applications" %}

- - {% trans "Go to Dashboard" %} - -
-
-
- - {% if application.resume %} -
-
-
- -
{% trans "Download Resume" %}
-

{% trans "Get your submitted resume" %}

- - - {% trans "Download" %} - -
-
-
- {% endif %} - -
-
-
- -
{% trans "Print Application" %}
-

{% trans "Get a printable version" %}

- -
-
-
- - {% comment %}
-
-
- -
{% trans "Update Profile" %}
-

{% trans "Edit your personal information" %}

- - - {% trans "Update" %} - -
-
-
{% endcomment %} -
- - - {% comment %}
-
+
{% trans "Next Steps" %}
-
+
{% if application.stage == 'Applied' %}
@@ -563,10 +580,9 @@
-
{% endcomment %} +
-
{% trans "Document Name" %} {% trans "Type" %}