325 lines
12 KiB
HTML
325 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% load static i18n %}
|
|
|
|
{% block title %}{% trans "Recruitment Dashboard" %} - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold text-gray-900">{% trans "Recruitment Analytics" %}</h1>
|
|
<p class="text-sm text-gray-400 mt-1">{% trans "Overview of your recruitment pipeline" %}</p>
|
|
</div>
|
|
|
|
{# Job Filter Dropdown #}
|
|
<form method="get" action="" class="flex items-center gap-3">
|
|
<label for="job-select" class="text-sm font-medium text-gray-700">{% trans "Filter Job:" %}</label>
|
|
<select name="selected_job_pk" id="job-select" onchange="this.form.submit()" class="px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition">
|
|
<option value="">{% trans "All Jobs (Default View)" %}</option>
|
|
{% for job in jobs%}
|
|
<option value="{{ job.pk}}" {% if current_job_id|slugify == job.pk|slugify %}selected{% endif %}>
|
|
{{ job.title }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</form>
|
|
</div>
|
|
|
|
{# -------------------------------------------------------------------------- #}
|
|
{# STATS CARDS SECTION (12 KPIs) #}
|
|
{# -------------------------------------------------------------------------- #}
|
|
{% include 'recruitment/partials/stats_cards.html' %}
|
|
|
|
{# -------------------------------------------------------------------------- #}
|
|
{# CHARTS SECTION #}
|
|
{# -------------------------------------------------------------------------- #}
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{# AREA CHART - Daily Candidate Applications Trend (Global Chart) #}
|
|
<div class="lg:col-span-2 bg-white rounded-2xl p-6 shadow-sm border border-gray-100">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h4 class="font-bold text-gray-800">{% trans "Daily Applications Trend" %}</h4>
|
|
<p class="text-xs text-gray-400">{% trans "Daily application volume over time" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="h-80">
|
|
<canvas id="dailyApplicationsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{# BAR CHART - Application Volume (Global Chart) #}
|
|
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h4 class="font-bold text-gray-800">{% trans "Top 5 Application Volume" %}</h4>
|
|
<p class="text-xs text-gray-400">{% trans "Jobs with most applications" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="h-64">
|
|
<canvas id="applicationsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{# FUNNEL CHART - Candidate Pipeline Status (Scoped Chart) #}
|
|
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h4 class="font-bold text-gray-800">
|
|
{% if current_job %}
|
|
{% trans "Pipeline Funnel: " %}{{ current_job.title }}
|
|
{% else %}
|
|
{% trans "Total Pipeline Funnel (All Jobs)" %}
|
|
{% endif %}
|
|
</h4>
|
|
<p class="text-xs text-gray-400">{% trans "Candidate pipeline stages" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="h-64">
|
|
<canvas id="application_funnel_chart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{# GAUGE CHART - Average Time-to-Hire (Avg. Days) #}
|
|
<div class="lg:col-span-2 bg-white rounded-2xl p-6 shadow-sm border border-gray-100">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h4 class="font-bold text-gray-800">{% trans "Time-to-Hire Target Check" %}</h4>
|
|
<p class="text-xs text-gray-400">{% trans "Average days to hire candidates" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="h-64 flex justify-center items-center">
|
|
<div id="timeToHireGauge" class="text-center w-full">
|
|
{% include "recruitment/partials/_guage_chart.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
|
|
|
|
|
<script>
|
|
// Pass context data safely to JavaScript
|
|
const totalCandidatesScoped = parseInt('{{ total_applications|default:0 }}');
|
|
const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5);
|
|
const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5);
|
|
const stages = JSON.parse('{{ application_stage|escapejs }}');
|
|
const counts = JSON.parse('{{ application_count|safe }}');
|
|
|
|
// --- 1. BAR CHART configuration (Top 5 Applications) ---
|
|
const ctxBar = document.getElementById('applicationsChart').getContext('2d');
|
|
new Chart(ctxBar, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: jobTitles,
|
|
datasets: [{
|
|
label: '{% trans "Applications" %}',
|
|
data: jobAppCounts,
|
|
backgroundColor: '#9d2235',
|
|
borderColor: '#7a1a29',
|
|
borderWidth: 1,
|
|
barThickness: 50,
|
|
borderRadius: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
title: {
|
|
display: true,
|
|
text: '{% trans "Top 5 Most Applied Jobs" %}',
|
|
font: { size: 14, weight: '600' },
|
|
color: '#343a40'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: { display: true, text: '{% trans "Total Applications" %}', font: { size: 11 } },
|
|
ticks: { color: '#666', precision: 0 },
|
|
grid: { color: '#e5e7eb' }
|
|
},
|
|
x: {
|
|
ticks: { color: '#666', font: { size: 11 } },
|
|
grid: { display: false }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// --- 2. CANDIDATE PIPELINE CENTERED FUNNEL CHART ---
|
|
|
|
// 1. Find the maximum count (for the widest bar)
|
|
const maxCount = Math.max(...counts);
|
|
|
|
// 2. Calculate the transparent "spacer" data needed to center each bar
|
|
const spacerData = counts.map(count => (maxCount - count) / 2);
|
|
|
|
// Define the dark-to-light temple red shades (5 stages, reverse order for funnel look)
|
|
const templeRedShades = [
|
|
'#c04050', // APPLIED - Lighter Red
|
|
'#a83242', // EXAM
|
|
'#9d2235', // INTERVIEW
|
|
'#8a1c2d', // OFFER
|
|
'#7a1a29', // HIRED - Darkest Red
|
|
];
|
|
|
|
// Slice and use the first N shades based on the number of stages
|
|
const stageColors = templeRedShades.slice(templeRedShades.length - stages.length);
|
|
|
|
const ctxFunnel = document.getElementById('application_funnel_chart').getContext('2d');
|
|
|
|
new Chart(ctxFunnel, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: stages,
|
|
datasets: [
|
|
// 1. TRANSPARENT SPACER DATASET (Pushes the bar to the center)
|
|
{
|
|
label: 'Spacer',
|
|
data: spacerData,
|
|
backgroundColor: 'transparent',
|
|
hoverBackgroundColor: 'transparent',
|
|
barThickness: 50,
|
|
datalabels: { display: false },
|
|
tooltip: { enabled: false }
|
|
},
|
|
// 2. VISIBLE CANDIDATE COUNT DATASET
|
|
{
|
|
label: '{% trans "Application Count" %}',
|
|
data: counts,
|
|
backgroundColor: stageColors,
|
|
barThickness: 50,
|
|
borderRadius: 4
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
|
|
scales: {
|
|
x: {
|
|
beginAtZero: true,
|
|
stacked: true,
|
|
display: false,
|
|
max: maxCount
|
|
},
|
|
y: {
|
|
stacked: true,
|
|
grid: { display: false },
|
|
ticks: {
|
|
color: '#343a40',
|
|
font: { size: 12, weight: '500' }
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
title: { display: false },
|
|
tooltip: {
|
|
filter: (tooltipItem) => {
|
|
return tooltipItem.datasetIndex === 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// Pass context data safely to JavaScript
|
|
const globalDates = JSON.parse('{{ global_dates|escapejs }}');
|
|
const globalCounts = JSON.parse('{{ global_counts|escapejs }}');
|
|
const scopedDates = JSON.parse('{{ scoped_dates|escapejs }}');
|
|
const scopedCounts = JSON.parse('{{ scoped_counts|escapejs }}');
|
|
const isJobScoped = '{{ is_job_scoped }}' === 'True';
|
|
|
|
|
|
// --- 4. DAILY APPLICATIONS LINE CHART ---
|
|
const ctxLine = document.getElementById('dailyApplicationsChart').getContext('2d');
|
|
|
|
// Create datasets
|
|
const datasets = [{
|
|
label: '{% trans "All Jobs" %}',
|
|
data: globalCounts,
|
|
borderColor: '#7a1a29',
|
|
backgroundColor: 'rgba(122, 26, 41, 0.05)',
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#fff',
|
|
pointBorderWidth: 2
|
|
}];
|
|
|
|
// Add scoped data if a job is selected
|
|
if (isJobScoped) {
|
|
datasets.push({
|
|
label: '{% trans "Current Job" %}',
|
|
data: scopedCounts,
|
|
borderColor: '#9d2235',
|
|
backgroundColor: 'rgba(157, 34, 53, 0.05)',
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#fff',
|
|
pointBorderWidth: 2
|
|
});
|
|
}
|
|
|
|
new Chart(ctxLine, {
|
|
type: 'line',
|
|
data: {
|
|
// Use global dates as the base labels
|
|
labels: globalDates,
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: '{% trans "Daily Applications (Last 30 Days)" %}',
|
|
font: { size: 16, weight: '600' },
|
|
color: '#343a40'
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
time: {
|
|
unit: 'day',
|
|
tooltipFormat: 'MMM D',
|
|
displayFormats: {
|
|
day: 'MMM D'
|
|
}
|
|
},
|
|
title: { display: true, text: '{% trans "Date" %}', font: { size: 11 } },
|
|
grid: { display: false }
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
title: { display: true, text: '{% trans "New Applications" %}', font: { size: 11 } },
|
|
ticks: { precision: 0 },
|
|
grid: { color: '#e5e7eb' }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
</script>
|
|
|
|
{% endblock %} |