dashboard and spinner

This commit is contained in:
Faheed 2025-10-26 18:27:09 +03:00
parent de83838392
commit b7d0dc8257
9 changed files with 226 additions and 18 deletions

View File

@ -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):

View File

@ -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),

View File

@ -474,6 +474,16 @@
</div>
</div>
</div>
<!--Vacancy fill rate-->
<div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-trophy text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-secondary fw-bold">{{ job.vacancy_fill_rate|floatformat:2 }}</div>
<small class="text-muted d-block">{% trans "Vacancy Fill Rate" %}</small>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -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;
}
}
</style>
{% endblock %}
@ -572,8 +625,20 @@
</div>
{% include 'recruitment/candidate_resume_template.html' %}
{% if candidate.is_resume_parsed %}
{% include 'recruitment/candidate_resume_template.html' %}
{% else %}
<a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none">
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 100px; height: 100px;">
<circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
</svg>
<span class="ms-2">AI Score Loading.....</span>
</div>
</a>
{% endif %}
{% if user.is_staff %}

View File

@ -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;
}
}
</style>
{% endblock %}
@ -232,11 +283,23 @@
<td>{{ candidate.phone }}</td>
<td> <span class="badge bg-primary"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-decoration-none text-white">{{ candidate.job.title }}</a></span></td>
<td>
{% if candidate.is_resume_parsed %}
{% if candidate.professional_category != 'Uncategorized' %}
<span class="badge bg-primary">
{{ candidate.professional_category }}
</span>
{% endif %}
{% endif %}
{% else %}
<a href="{% url 'candidate_list' %}">
<div>
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
<circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
</svg>
</div>
</a>
{% endif %}
</td>
<td>
<span class="badge bg-primary">

View File

@ -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;
}
}
</style>
{% endblock %}
{% block content %}
@ -351,11 +395,21 @@
</div>
</td>
<td class="text-center">
{% if candidate.is_resume_parsed %}
{% if candidate.match_score %}
<span class="badge ai-score-badge">
{{ candidate.match_score|default:"0" }}%
</span>
{% endif %}
{% else %}
<div>
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
<circle cx="25" cy="25" r="20"></circle>
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
</svg>
</div>
{% endif %}
</td>
<td class="text-center">
{% if candidate.screening_stage_rating %}

View File

@ -123,7 +123,7 @@
{% block content %}
<div class="container-fluid py-4">
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Intelligence" %} 🧠</h1>
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Analytics" %}</h1>
{# -------------------------------------------------------------------------- #}
{# STATS CARDS SECTION #}
@ -156,7 +156,7 @@
<div class="card">
<div class="card-header">
<h3><i class="fas fa-star stat-icon" style="color: var(--color-warning);"></i> {% trans "Avg. Match Score" %}</h3>
<h3><i class="fas fa-star stat-icon"></i> {% trans "Avg. Match Score" %}</h3>
</div>
<div class="stat-value">{{ avg_match_score|floatformat:1 }}</div>
<div class="stat-caption">{% trans "Average AI Score (0-100)" %}</div>
@ -164,7 +164,7 @@
<div class="card">
<div class="card-header">
<h3><i class="fas fa-trophy stat-icon" style="color: var(--color-success);"></i> {% trans "High Potential" %}</h3>
<h3><i class="fas fa-trophy stat-icon"></i> {% trans "High Potential" %}</h3>
</div>
<div class="stat-value">{{ high_potential_count }}</div>
<div class="stat-caption">{% trans "Candidates with Score ≥ 75 ({{ high_potential_ratio }}%)" %}</div>
@ -172,7 +172,7 @@
<div class="card">
<div class="card-header">
<h3><i class="fas fa-cogs stat-icon" style="color: var(--color-info);"></i> {% trans "Scored Profiles" %}</h3>
<h3><i class="fas fa-cogs stat-icon"></i> {% trans "Scored Profiles" %}</h3>
</div>
<div class="stat-value">{{ scored_ratio|floatformat:1 }}%</div>
<div class="stat-caption">{% trans "Percent of profiles processed by AI" %}</div>
@ -210,13 +210,13 @@
<small>{{my_job}}</small>
{# Job Filter Dropdown - Consistent with Card Header Layout #}
<form method="get" action="." class="job-filter-container">
<form method="get" action="" class="job-filter-container">
<label for="job-select" class="job-filter-label d-none d-md-inline">{% trans "Filter Job:" %}</label>
<select name="selected_job_id" id="job-select" class="form-select form-select-sm" onchange="this.form.submit()">
<select name="selected_job_pk" id="job-select" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">{% trans "All Jobs (Default View)" %}</option>
{% for job in jobs%}
<option value="{{ job.internal_job_id }}" {% if selected_job_id == job.internal_job_id %}selected{% endif %}>
<option value="{{ job.pk}}" {% if selected_job_pk == job.pk %}selected{% endif %}>
{{ job }}
</option>
{% 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)