diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index 1dc5ea3..a8ac33d 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index 85f9dab..9de137f 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index a0776f0..3f7a86e 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -338,6 +338,20 @@ class JobPosting(Base): @property def offer_candidates_count(self): return self.all_candidates.filter(stage="Offer").count() + + @property + def vacancy_fill_rate(self): + total_positions = self.open_positions + + no_of_positions_filled = self.candidates.filter(stage__in=['HIRED']).count() + + if total_positions > 0: + vacancy_fill_rate = no_of_positions_filled / total_positions + else: + vacancy_fill_rate = 0.0 + + return vacancy_fill_rate + class JobPostingImage(models.Model): @@ -632,6 +646,7 @@ class Candidate(Base): ).exists() return future_meetings or today_future_meetings + class TrainingMaterial(Base): diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index d204d97..2c06fc7 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -395,12 +395,12 @@ def dashboard_view(request): high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 jobs=models.JobPosting.objects.all().order_by('internal_job_id') - selected_job_id=request.GET.get('selected_job_id','') + selected_job_pk=request.GET.get('selected_job_pk','') candidate_stage=['APPLIED','EXAM','INTERVIEW','OFFER'] apply_count,exam_count,interview_count,offer_count=[0]*4 - if selected_job_id: - job=jobs.get(internal_job_id=selected_job_id) + if selected_job_pk: + job=jobs.get(pk=selected_job_pk) apply_count=job.screening_candidates_count exam_count=job.exam_candidates_count interview_count=job.interview_candidates_count @@ -430,7 +430,7 @@ def dashboard_view(request): 'high_potential_count': high_potential_count, 'high_potential_ratio': high_potential_ratio, 'scored_ratio': scored_ratio, - 'current_job_id':selected_job_id, + 'current_job_id':selected_job_pk, 'jobs':jobs, 'all_candidates_count':all_candidates_count, 'candidate_stage':json.dumps(candidate_stage), diff --git a/templates/jobs/job_detail.html b/templates/jobs/job_detail.html index 3d5139f..a55900d 100644 --- a/templates/jobs/job_detail.html +++ b/templates/jobs/job_detail.html @@ -474,6 +474,16 @@ + +
+
+
+ +
{{ job.vacancy_fill_rate|floatformat:2 }}
+ {% trans "Vacancy Fill Rate" %} +
+
+
diff --git a/templates/recruitment/candidate_detail.html b/templates/recruitment/candidate_detail.html index f5a405f..5c33e40 100644 --- a/templates/recruitment/candidate_detail.html +++ b/templates/recruitment/candidate_detail.html @@ -176,6 +176,59 @@ .timeline-bg-interview { background-color: #ffc107 !important; } .timeline-bg-offer { background-color: #28a745 !important; } .timeline-bg-rejected { background-color: #dc3545 !important; } + + + + +/* ------------------------------------------- */ +/* 1. Base Spinner Styling */ +/* ------------------------------------------- */ +.kaats-spinner { + animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ + width: 40px; /* Standard size */ + height: 40px; + display: inline-block; /* Useful for table cells */ + vertical-align: middle; +} + +.kaats-spinner .path { + stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke-linecap: round; + /* Optional: Add a lighter background circle for contrast */ + /* stroke-dashoffset will be reset by the dash animation */ +} + +/* Optional: Background circle for better contrast (similar to Bootstrap) */ +.kaats-spinner circle { + stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ + fill: none; + stroke-width: 5; /* Keep stroke-width on both circles */ +} + +/* ------------------------------------------- */ +/* 2. Keyframe Animations */ +/* ------------------------------------------- */ +@keyframes kaats-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes kaats-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} + {% endblock %} @@ -572,8 +625,20 @@ - -{% include 'recruitment/candidate_resume_template.html' %} +{% if candidate.is_resume_parsed %} + {% include 'recruitment/candidate_resume_template.html' %} +{% else %} + +
+ + + + + AI Score Loading..... +
+
+{% endif %} {% if user.is_staff %} diff --git a/templates/recruitment/candidate_list.html b/templates/recruitment/candidate_list.html index 72867db..41c2be9 100644 --- a/templates/recruitment/candidate_list.html +++ b/templates/recruitment/candidate_list.html @@ -132,6 +132,57 @@ display: flex; gap: 0.5rem; } + + +/* ------------------------------------------- */ +/* 1. Base Spinner Styling */ +/* ------------------------------------------- */ +.kaats-spinner { + animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ + width: 40px; /* Standard size */ + height: 40px; + display: inline-block; /* Useful for table cells */ + vertical-align: middle; +} + +.kaats-spinner .path { + stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke-linecap: round; + /* Optional: Add a lighter background circle for contrast */ + /* stroke-dashoffset will be reset by the dash animation */ +} + +/* Optional: Background circle for better contrast (similar to Bootstrap) */ +.kaats-spinner circle { + stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ + fill: none; + stroke-width: 5; /* Keep stroke-width on both circles */ +} + +/* ------------------------------------------- */ +/* 2. Keyframe Animations */ +/* ------------------------------------------- */ +@keyframes kaats-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes kaats-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} + {% endblock %} @@ -232,11 +283,23 @@ {{ candidate.phone }} {{ candidate.job.title }} + {% if candidate.is_resume_parsed %} {% if candidate.professional_category != 'Uncategorized' %} {{ candidate.professional_category }} - {% endif %} + {% endif %} + {% else %} + +
+ + + + +
+
+ {% endif %} diff --git a/templates/recruitment/candidate_screening_view.html b/templates/recruitment/candidate_screening_view.html index b7e33b8..b828b8c 100644 --- a/templates/recruitment/candidate_screening_view.html +++ b/templates/recruitment/candidate_screening_view.html @@ -163,7 +163,51 @@ } +.kaats-spinner { + animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */ + width: 40px; /* Standard size */ + height: 40px; + display: inline-block; /* Useful for table cells */ + vertical-align: middle; +} +.kaats-spinner .path { + stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */ + stroke-linecap: round; + /* Optional: Add a lighter background circle for contrast */ + /* stroke-dashoffset will be reset by the dash animation */ +} + +/* Optional: Background circle for better contrast (similar to Bootstrap) */ +.kaats-spinner circle { + stroke: var(--kaauh-border, #e9ecef); /* Light gray background */ + fill: none; + stroke-width: 5; /* Keep stroke-width on both circles */ +} + +/* ------------------------------------------- */ +/* 2. Keyframe Animations */ +/* ------------------------------------------- */ +@keyframes kaats-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes kaats-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} {% endblock %} {% block content %} @@ -351,11 +395,21 @@ + {% if candidate.is_resume_parsed %} {% if candidate.match_score %} {{ candidate.match_score|default:"0" }}% {% endif %} + {% else %} +
+ + + + +
+ {% endif %} {% if candidate.screening_stage_rating %} diff --git a/templates/recruitment/dashboard.html b/templates/recruitment/dashboard.html index d55c31b..c297668 100644 --- a/templates/recruitment/dashboard.html +++ b/templates/recruitment/dashboard.html @@ -123,7 +123,7 @@ {% block content %}
-

{% trans "Recruitment Intelligence" %} 🧠

+

{% trans "Recruitment Analytics" %}

{# -------------------------------------------------------------------------- #} {# STATS CARDS SECTION #} @@ -156,7 +156,7 @@
-

{% trans "Avg. Match Score" %}

+

{% trans "Avg. Match Score" %}

{{ avg_match_score|floatformat:1 }}
{% trans "Average AI Score (0-100)" %}
@@ -164,7 +164,7 @@
-

{% trans "High Potential" %}

+

{% trans "High Potential" %}

{{ high_potential_count }}
{% trans "Candidates with Score ≥ 75 ({{ high_potential_ratio }}%)" %}
@@ -172,7 +172,7 @@
-

{% trans "Scored Profiles" %}

+

{% trans "Scored Profiles" %}

{{ scored_ratio|floatformat:1 }}%
{% trans "Percent of profiles processed by AI" %}
@@ -210,13 +210,13 @@ {{my_job}} {# Job Filter Dropdown - Consistent with Card Header Layout #} -
+ - {% for job in jobs%} - {% endfor %} @@ -275,7 +275,7 @@ // Pass context data safely to JavaScript const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5); // Take top 5 const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5); // Take top 5 - + // BAR CHART configuration const ctxBar = document.getElementById('applicationsChart').getContext('2d'); new Chart(ctxBar, { @@ -285,7 +285,7 @@ datasets: [{ label: '{% trans "Applications" %}', data: jobAppCounts, - backgroundColor: 'var(--kaauh-teal)', + backgroundColor: '#00636e', borderColor: 'var(--kaauh-teal-dark)', borderWidth: 1, barThickness: 50 @@ -307,7 +307,7 @@ y: { beginAtZero: true, title: { display: true, text: '{% trans "Total Applications" %}' }, - ticks: { color: '#333333', precision: 0 }, + ticks: { color: '#2222', precision: 0 }, grid: { color: '#e0e0e0' } }, x: { @@ -318,6 +318,7 @@ } }); + // DONUT CHART configuration const ctxDonut = document.getElementById('candidate_donout_chart').getContext('2d'); @@ -330,7 +331,7 @@ label: '{% trans "Candidate Count" %}', data: JSON.parse('{{ candidates_count|safe }}'), backgroundColor: [ - 'var(--kaauh-teal)', // Applied (Primary) + '#00636e', // Applied (Primary) 'rgb(255, 159, 64)', // Exam (Orange) 'rgb(54, 162, 235)', // Interview (Blue) 'rgb(75, 192, 192)' // Offer (Green)