dashboard and spinner
This commit is contained in:
parent
de83838392
commit
b7d0dc8257
Binary file not shown.
Binary file not shown.
@ -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):
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user