[FIX] DASHBOARD: Missing chart styles

This commit is contained in:
Horilla
2025-08-06 15:42:50 +05:30
parent 582c3dcc10
commit b0c3afa92a
9 changed files with 1226 additions and 217 deletions

View File

@@ -1,71 +1,324 @@
$(document).ready(function () {
// function employeeChart(dataSet, labels) {
// const data = {
// labels: labels,
// datasets: dataSet,
// };
// // Create chart using the Chart.js library
// window["myChart"] = {};
// if (document.getElementById("totalEmployees")) {
// const ctx = document.getElementById("totalEmployees").getContext("2d");
// employeeChart = new Chart(ctx, {
// type: "doughnut",
// data: data,
// options: {
// responsive: true,
// maintainAspectRatio: false,
// onClick: (e, activeEls) => {
// let datasetIndex = activeEls[0].datasetIndex;
// let dataIndex = activeEls[0].index;
// let datasetLabel = e.chart.data.datasets[datasetIndex].label;
// let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
// let label = e.chart.data.labels[dataIndex];
// var active = "False";
// if (label.toLowerCase() == "active") {
// active = "True";
// }
// localStorage.removeItem("savedFilters");
// window.location.href = "/employee/employee-view?is_active=" + active;
// },
// },
// plugins: [
// {
// afterRender: (chart) => emptyChart(chart),
// },
// ],
// });
// }
// }
// function genderChart(dataSet, labels) {
// const data = {
// labels: labels,
// datasets: dataSet,
// };
// console.log(data)
// // Create chart using the Chart.js library
// window["genderChart"] = {};
// if (document.getElementById("genderChart")) {
// const ctx = document.getElementById("genderChart").getContext("2d");
// genderChart = new Chart(ctx, {
// type: "doughnut",
// data: data,
// options: {
// responsive: true,
// maintainAspectRatio: false,
// onClick: (e, activeEls) => {
// let datasetIndex = activeEls[0].datasetIndex;
// let dataIndex = activeEls[0].index;
// let datasetLabel = e.chart.data.datasets[datasetIndex].label;
// let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
// let label = e.chart.data.labels[dataIndex];
// localStorage.removeItem("savedFilters");
// window.location.href =
// "/employee/employee-view?gender=" + label.toLowerCase();
// },
// },
// plugins: [
// {
// afterRender: (chart) => emptyChart(chart),
// },
// ],
// });
// }
// }
// function departmentChart(dataSet, labels) {
// console.log(dataSet);
// console.log(labels);
// const data = {
// labels: labels,
// datasets: dataSet,
// };
// // Create chart using the Chart.js library
// window["departmentChart"] = {};
// if (document.getElementById("departmentChart")) {
// const ctx = document.getElementById("departmentChart").getContext("2d");
// departmentChart = new Chart(ctx, {
// type: "doughnut",
// data: data,
// options: {
// responsive: true,
// maintainAspectRatio: false,
// onClick: (e, activeEls) => {
// let datasetIndex = activeEls[0].datasetIndex;
// let dataIndex = activeEls[0].index;
// let datasetLabel = e.chart.data.datasets[datasetIndex].label;
// let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
// let label = e.chart.data.labels[dataIndex];
// localStorage.removeItem("savedFilters");
// window.location.href =
// "/employee/employee-view?department=" + label;
// },
// },
// plugins: [
// {
// afterRender: (chart) => emptyChart(chart),
// },
// ],
// });
// }
// }
function employeeChart(dataSet, labels) {
const data = {
labels: labels,
datasets: dataSet,
};
// Create chart using the Chart.js library
window["myChart"] = {};
if (document.getElementById("totalEmployees")) {
const ctx = document.getElementById("totalEmployees").getContext("2d");
employeeChart = new Chart(ctx, {
$(document).ready(function () {
const ctx = document.getElementById("totalEmployees")?.getContext("2d");
if (!ctx) return;
const values = dataSet[0].data;
const colors = [
"#34d399", // Active - green
"#f87171", // Inactive - red
];
const visibility = Array(labels.length).fill(true);
// Create chart instance
const employeeChartInstance = new Chart(ctx, {
type: "doughnut",
data: data,
data: {
labels: labels,
datasets: [
{
...dataSet[0],
backgroundColor: colors.slice(0, labels.length),
borderWidth: 0,
borderRadius: 10,
hoverOffset: 8,
},
],
},
options: {
cutout: "70%",
responsive: true,
maintainAspectRatio: false,
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
var active = "False";
if (label.toLowerCase() == "active") {
active = "True";
}
onClick: function (e, activeEls) {
if (!activeEls.length) return;
const dataIndex = activeEls[0].index;
const label = labels[dataIndex];
let active = label.toLowerCase() === "active" ? "True" : "False";
localStorage.removeItem("savedFilters");
window.location.href = "/employee/employee-view?is_active=" + active;
},
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "#111827",
bodyColor: "#f3f4f6",
borderColor: "#e5e7eb",
borderWidth: 1,
},
},
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
id: "centerText",
afterDraw(chart) {
const { width, height, ctx } = chart;
ctx.save();
const total = chart.data.datasets[0].data.reduce(
(sum, val) => sum + val,
0
);
ctx.font = "bold 22px sans-serif";
ctx.fillStyle = "#374151";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(total, width / 2, height / 2 - 5);
ctx.font = "15px sans-serif";
ctx.fillStyle = "#9ca3af";
ctx.fillText("Total", width / 2, height / 2 + 20);
ctx.restore();
},
},
{
afterRender: (chart) => {
if (typeof emptyChart === "function") {
emptyChart(chart);
}
},
},
],
});
}
// 🧩 Custom Legend Generation
const $legendContainer = $("#employeeChartLegend"); // Make sure to have this element in DOM
$legendContainer.empty();
labels.forEach((label, index) => {
const color = colors[index % colors.length];
const $item = $(`
<div style="display: flex; align-items: center; margin-bottom: 6px; cursor: pointer;">
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${color}; margin-right: 8px;"></span>
<span class="legend-label">${label}</span>
</div>
`);
$legendContainer.append($item);
$item.on("click", function () {
visibility[index] = !visibility[index];
employeeChartInstance.data.datasets[0].data = values.map((val, i) =>
visibility[i] ? val : 0
);
const $dot = $(this).find("span").first();
const $label = $(this).find(".legend-label");
if (visibility[index]) {
$dot.css("opacity", "1");
$label.css("text-decoration", "none");
} else {
$dot.css("opacity", "0.4");
$label.css("text-decoration", "line-through");
}
employeeChartInstance.update();
});
});
});
}
function genderChart(dataSet, labels) {
const data = {
labels: labels,
datasets: dataSet,
};
// Create chart using the Chart.js library
window["genderChart"] = {};
const centerImage = new Image();
centerImage.src = "/static/horilla_theme/assets/img/icons/gender.svg";
if (document.getElementById("genderChart")) {
const ctx = document.getElementById("genderChart").getContext("2d");
genderChart = new Chart(ctx, {
// Override dataset background colors with new design colors
const updatedDataSet = dataSet.map((ds) => ({
...ds,
backgroundColor: ["#cfe9ff", "#ffc9de", "#e6ccff"],
borderWidth: 0,
}));
window["genderChart"] = new Chart(ctx, {
type: "doughnut",
data: data,
data: {
labels: labels,
datasets: updatedDataSet,
},
options: {
cutout: "70%",
responsive: true,
maintainAspectRatio: false,
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
"/employee/employee-view?gender=" + label.toLowerCase();
if (activeEls.length > 0) {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href = "/employee/employee-view?gender=" + label.toLowerCase();
}
},
plugins: {
legend: {
position: "bottom",
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 20,
font: {
size: 12,
},
color: "#000",
},
},
tooltip: {
padding: 10,
cornerRadius: 4,
backgroundColor: "#333",
titleColor: "#fff",
bodyColor: "#fff",
callbacks: {
label: function (context) {
return context.parsed; // This shows only "13", not "Employees: 13"
},
},
},
},
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
id: "centerIcon",
afterDatasetsDraw(chart) {
if (!centerImage.complete) return;
const ctx = chart.ctx;
const size = 70;
ctx.drawImage(
centerImage,
chart.width / 2 - size / 2,
chart.height / 2 - size / 2 - 20,
size,
size
);
},
},
{
afterRender: (chart) => {
if (typeof emptyChart === "function") {
emptyChart(chart);
}
},
},
],
});
@@ -73,39 +326,124 @@ $(document).ready(function () {
}
function departmentChart(dataSet, labels) {
const data = {
labels: labels,
datasets: dataSet,
};
// Create chart using the Chart.js library
window["departmentChart"] = {};
if (document.getElementById("departmentChart")) {
const ctx = document.getElementById("departmentChart").getContext("2d");
departmentChart = new Chart(ctx, {
$(document).ready(function () {
const ctx = $("#departmentChart")[0]?.getContext("2d");
if (!ctx) return;
const values = dataSet[0].data;
const colors = [
"#facc15",
"#f87171",
"#ddd6fe",
"#a5b4fc",
"#93c5fd",
"#d1d5db",
];
const visibility = Array(labels.length).fill(true);
// Create the chart instance
const departmentChartInstance = new Chart(ctx, {
type: "doughnut",
data: data,
data: {
labels: labels,
datasets: [
{
...dataSet[0],
backgroundColor: colors.slice(0, labels.length),
borderWidth: 0,
borderRadius: 10,
hoverOffset: 8,
},
],
},
options: {
cutout: "70%",
responsive: true,
maintainAspectRatio: false,
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
"/employee/employee-view?department=" + label;
onClick: function (e, activeEls) {
if (!activeEls.length) return;
const dataIndex = activeEls[0].index;
const label = labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href = "/employee/employee-view?department=" + label;
},
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "#111827",
bodyColor: "#f3f4f6",
borderColor: "#e5e7eb",
borderWidth: 1,
},
},
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
id: "centerText",
afterDraw(chart) {
const { width, height, ctx } = chart;
ctx.save();
const total = chart.data.datasets[0].data.reduce(
(sum, val) => sum + val,
0
);
ctx.font = "bold 22px sans-serif";
ctx.fillStyle = "#374151";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(total, width / 2, height / 2 - 5);
ctx.font = "15px sans-serif";
ctx.fillStyle = "#9ca3af";
ctx.fillText("Total", width / 2, height / 2 + 20);
ctx.restore();
},
},
],
});
}
// 🧩 Generate Custom Legend
const $legendContainer = $("#chartLegend");
$legendContainer.empty();
labels.forEach((label, index) => {
const color = colors[index % colors.length];
const $item = $(`
<div style="display: flex; align-items: center; margin-bottom: 6px; cursor: pointer;">
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${color}; margin-right: 8px;"></span>
<span class="legend-label">${label}</span>
</div>
`);
$legendContainer.append($item);
$item.on("click", function () {
visibility[index] = !visibility[index];
departmentChartInstance.data.datasets[0].data = values.map((val, i) =>
visibility[i] ? val : 0
);
const $dot = $(this).find("span").first();
const $label = $(this).find(".legend-label");
if (visibility[index]) {
$dot.css("opacity", "1");
$label.css("text-decoration", "none");
} else {
$dot.css("opacity", "0.4");
$label.css("text-decoration", "line-through");
}
departmentChartInstance.update();
});
});
});
}
$.ajax({

View File

@@ -91,4 +91,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,67 +1,168 @@
$(document).ready(function () {
//Todays leave count department wise chart
if (document.getElementById("overAllLeave")){
var myChart1 = document.getElementById("overAllLeave").getContext("2d");
var overAllLeave = new Chart(myChart1, {
let overAllLeaveChart = null;
function renderOverAllLeaveChart(data, labels) {
const ctx = document.getElementById("overAllLeave")?.getContext("2d");
if (!ctx) return;
const dataset = [{
label: "Leave count",
data: data,
backgroundColor: ["#cfe9ff", "#ffc9de", "#e6ccff"], // Customize as needed
borderWidth: 0,
}];
if (overAllLeaveChart) {
overAllLeaveChart.data.labels = labels;
overAllLeaveChart.data.datasets = dataset;
overAllLeaveChart.update();
return;
}
overAllLeaveChart = new Chart(ctx, {
type: "doughnut",
data: {
labels: [],
datasets: [
{
label: "Leave count",
data: [],
backgroundColor: null,
},
],
labels: labels,
datasets: dataset,
},
options: {
cutout: "70%",
responsive: true,
maintainAspectRatio: false,
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
params =`?department_name=${label}&overall_leave=${$("#overAllLeaveSelect").val()}`;
window.location.href = "/leave/request-view" + params;
if (activeEls.length > 0) {
const datasetIndex = activeEls[0].datasetIndex;
const dataIndex = activeEls[0].index;
const label = e.chart.data.labels[dataIndex];
const selected = $("#overAllLeaveSelect").val();
const params = `?department_name=${label}&overall_leave=${selected}`;
window.location.href = "/leave/request-view" + params;
}
},
plugins: {
legend: {
position: "bottom",
labels: {
usePointStyle: true,
pointStyle: "circle",
padding: 20,
font: {
size: 12,
},
color: "#000",
},
},
tooltip: {
padding: 10,
cornerRadius: 4,
backgroundColor: "#333",
titleColor: "#fff",
bodyColor: "#fff",
callbacks: {
label: function (context) {
return context.parsed;
},
},
},
},
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
afterRender: (chart) => {
if (typeof emptyChart === "function") {
emptyChart(chart);
}
},
},
],
});
}
$.ajax({
type: "GET",
url: "/leave/overall-leave?overall_leave=today",
dataType: "json",
success: function (response) {
if (overAllLeave){
overAllLeave.data.labels = response.labels;
overAllLeave.data.datasets[0].data = response.data;
overAllLeave.data.datasets[0].backgroundColor = null;
overAllLeave.update();
}
},
});
$(document).on("change", "#overAllLeaveSelect", function () {
var selected = $(this).val();
function fetchOverAllLeaveData(overallLeaveType = "today") {
$.ajax({
type: "GET",
url: `/leave/overall-leave?overall_leave=${selected}`,
url: `/leave/overall-leave?overall_leave=${overallLeaveType}`,
dataType: "json",
success: function (response) {
overAllLeave.data.labels = response.labels;
overAllLeave.data.datasets[0].data = response.data;
overAllLeave.data.datasets[0].backgroundColor = null;
overAllLeave.update();
renderOverAllLeaveChart(response.data, response.labels);
},
});
});
}
//Today leave employees chart
// Initial chart load
fetchOverAllLeaveData();
// Dropdown change event
$(document).on("change", "#overAllLeaveSelect", function () {
fetchOverAllLeaveData($(this).val());
});
});
// $(document).ready(function () {
// //Todays leave count department wise chart
// if (document.getElementById("overAllLeave")){
// var myChart1 = document.getElementById("overAllLeave").getContext("2d");
// var overAllLeave = new Chart(myChart1, {
// type: "doughnut",
// data: {
// labels: [],
// datasets: [
// {
// label: "Leave count",
// data: [],
// backgroundColor: null,
// },
// ],
// },
// options: {
// responsive: true,
// maintainAspectRatio: false,
// onClick: (e, activeEls) => {
// let datasetIndex = activeEls[0].datasetIndex;
// let dataIndex = activeEls[0].index;
// let datasetLabel = e.chart.data.datasets[datasetIndex].label;
// let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
// let label = e.chart.data.labels[dataIndex];
// params =`?department_name=${label}&overall_leave=${$("#overAllLeaveSelect").val()}`;
// window.location.href = "/leave/request-view" + params;
// },
// },
// plugins: [
// {
// afterRender: (chart) => emptyChart(chart),
// },
// ],
// });
// }
// $.ajax({
// type: "GET",
// url: "/leave/overall-leave?overall_leave=today",
// dataType: "json",
// success: function (response) {
// if (overAllLeave){
// overAllLeave.data.labels = response.labels;
// overAllLeave.data.datasets[0].data = response.data;
// overAllLeave.data.datasets[0].backgroundColor = null;
// overAllLeave.update();
// }
// },
// });
// $(document).on("change", "#overAllLeaveSelect", function () {
// var selected = $(this).val();
// $.ajax({
// type: "GET",
// url: `/leave/overall-leave?overall_leave=${selected}`,
// dataType: "json",
// success: function (response) {
// overAllLeave.data.labels = response.labels;
// overAllLeave.data.datasets[0].data = response.data;
// overAllLeave.data.datasets[0].backgroundColor = null;
// overAllLeave.update();
// },
// });
// });
// //Today leave employees chart
// });

View File

@@ -1,59 +1,181 @@
$(document).ready(function () {
//Hired candididates recruitment wise chart
//onboarding started candidate chart
$.ajax({
type: "GET",
url: "/onboarding/onboard-candidate-chart",
success: function (response) {
const ctx = document.getElementById("onboardCandidate");
if (ctx) {
new Chart(ctx, {
type: "bar",
data: {
labels: response.labels,
datasets: [
{
label: "#onboarding candidates",
data: response.data,
backgroundColor: response.background_color,
borderColor: response.border_color,
borderWidth: 1,
},
],
// message:response.message,
// emptyImageSrc:'/static/images/ui/sunbed.png'
},
options: {
responsive: true,
const ctx = document.getElementById("onboardCandidate")?.getContext("2d");
if (!ctx || !response?.data || !response?.labels) return;
scales: {
y: {
beginAtZero: true,
},
},
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
"/recruitment/candidate-view" +
"?recruitment=" +
label +
"&start_onboard=true";
},
const labels = response.labels;
const values = response.data;
const colors = [
"#a5b4fc", "#fca5a5", "#fdba74", "#8de5b3", "#fcd34d", "#c2c7cc"
];
const visibility = Array(labels.length).fill(true);
const onboardChartInstance = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [{
label: "Onboarding Candidates",
data: values,
backgroundColor: colors,
borderRadius: 20,
barPercentage: 0.8,
categoryPercentage: 0.8,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
onClick: (e, activeEls) => {
if (!activeEls.length) return;
const dataIndex = activeEls[0].index;
const label = labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href = `/recruitment/candidate-view?recruitment=${encodeURIComponent(label)}&start_onboard=true`;
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
callbacks: {
title: tooltipItems => labels[tooltipItems[0].dataIndex],
label: tooltipItem => `Onboarding Candidates: ${tooltipItem.raw}`
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1, color: "#6b7280" },
grid: { drawBorder: false, color: "#e5e7eb" }
},
],
x: {
ticks: { display: false },
grid: { display: false },
border: { display: true, color: "#d1d5db" }
}
}
},
plugins: [{
afterRender: (chart) => {
if (typeof emptyChart === "function") emptyChart(chart);
}
}]
});
// 🧩 Generate Custom Legend (same style as departmentChart)
const $legendContainer = $("#onboardCandidateLegend");
$legendContainer.empty();
labels.forEach((label, index) => {
const color = colors[index % colors.length];
const $item = $(`
<div style="display: flex; align-items: center; margin-bottom: 6px; cursor: pointer;">
<span style="
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: ${color};
margin-right: 8px;
transition: opacity 0.3s;
"></span>
<span class="legend-label" style="color: #111827; font-size: 14px;">${label}</span>
</div>
`);
$legendContainer.append($item);
$item.on("click", function () {
visibility[index] = !visibility[index];
onboardChartInstance.data.datasets[0].data = values.map((val, i) =>
visibility[i] ? val : 0
);
const $dot = $(this).find("span").first();
const $label = $(this).find(".legend-label");
if (visibility[index]) {
$dot.css("opacity", "1");
$label.css("text-decoration", "none");
} else {
$dot.css("opacity", "0.4");
$label.css("text-decoration", "line-through");
}
onboardChartInstance.update();
});
}
});
},
error: function (xhr, status, error) {
console.error("Error fetching data:", error);
}
});
});
// $(document).ready(function () {
// //Hired candididates recruitment wise chart
// //onboarding started candidate chart
// $.ajax({
// type: "GET",
// url: "/onboarding/onboard-candidate-chart",
// success: function (response) {
// const ctx = document.getElementById("onboardCandidate");
// if (ctx) {
// new Chart(ctx, {
// type: "bar",
// data: {
// labels: response.labels,
// datasets: [
// {
// label: "#onboarding candidates",
// data: response.data,
// backgroundColor: response.background_color,
// borderColor: response.border_color,
// borderWidth: 1,
// },
// ],
// // message:response.message,
// // emptyImageSrc:'/static/images/ui/sunbed.png'
// },
// options: {
// responsive: true,
// scales: {
// y: {
// beginAtZero: true,
// },
// },
// onClick: (e, activeEls) => {
// let datasetIndex = activeEls[0].datasetIndex;
// let dataIndex = activeEls[0].index;
// let datasetLabel = e.chart.data.datasets[datasetIndex].label;
// let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
// let label = e.chart.data.labels[dataIndex];
// localStorage.removeItem("savedFilters");
// window.location.href =
// "/recruitment/candidate-view" +
// "?recruitment=" +
// label +
// "&start_onboard=true";
// },
// },
// plugins: [
// {
// afterRender: (chart) => emptyChart(chart),
// },
// ],
// });
// }
// },
// });
// });

View File

@@ -12,6 +12,7 @@ from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from horilla_views.cbv_methods import login_required
from horilla_views.generic.cbv.pipeline import KanbanView
from horilla_views.generic.cbv.views import (
HorillaFormView,
HorillaListView,
@@ -227,6 +228,7 @@ class GetStages(TemplateView):
context = super().get_context_data(**kwargs)
context["stages"] = self.stages
context["view_id"] = get_short_uuid(6, "hsv")
context["rec_id"] = kwargs["rec_id"]
return context
@@ -416,8 +418,185 @@ class CandidateList(HorillaListView):
return self.queryset
class CandidateCard(CandidateList):
template_name = "pipeline/kanban_components/candidate_kanban_components.html"
@method_decorator(login_required, name="dispatch")
@method_decorator(
manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch"
)
class CandidateCard(KanbanView):
model = models.Candidate
filter_class = filters.CandidateFilter
group_key = "stage_id"
records_per_page = 10
filter_keys_to_remove = ["rec_id"]
kanban_attrs = """
onclick="window.location.href = `{get_individual_url}`"
data-group-order='{ordered_group_json}'
"""
details = {
"image_src": "{get_avatar}",
"title": "{get_full_name}",
"email": "{email}",
"position": "{job_position_id}",
}
group_actions = [
{
"action": _("Add Candidate"),
"accessibility": "recruitment.accessibility.add_candidate_accessibility",
"attrs": """
hx-target="#genericModalBody"
hx-get="{get_add_candidate_url}"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
""",
},
{
"action": _("Edit"),
"accessibility": "recruitment.accessibility.edit_stage_accessibility",
"attrs": """
hx-target="#genericModalBody"
hx-get="{get_stage_update_url}"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
""",
},
{
"action": _("Bulk Mail"),
"accessibility": "recruitment.accessibility.edit_stage_accessibility",
"attrs": """
hx-target="#objectCreateModalTarget"
hx-get="{get_send_email_url}"
data-toggle="oh-modal-toggle"
data-target="#objectCreateModal"
""",
},
{
"action": _("Delete"),
"accessibility": "recruitment.accessibility.delete_stage_accessibility",
"attrs": """
hx-target="#deleteConfirmationBody"
hx-get="{get_delete_url}"
data-toggle="oh-modal-toggle"
data-target="#deleteConfirmation"
""",
},
]
actions = [
{
"action": _("Schedule Interview"),
"attrs": """
hx-get = "{get_schedule_interview}"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
hx-target="#genericModalBody"
""",
},
{
"action": _("Send Mail"),
"attrs": """
hx-get = "{get_send_mail}"
data-toggle="oh-modal-toggle"
data-target="#objectDetailsModal"
hx-target="#objectDetailsModalTarget"
""",
},
{
"action": "Add to Skill Zone",
"accessibility": "recruitment.cbv.accessibility.add_skill_zone",
"attrs": """
data-toggle="oh-modal-toggle"
data-target="#genericModal"
hx-get="{get_add_to_skill}"
hx-target="#genericModalBody"
class="oh-dropdown__link"
""",
},
{
"action": "View candidate self tracking",
"accessibility": "recruitment.cbv.accessibility.check_candidate_self_tracking",
"attrs": """
href="{get_self_tracking_url}"
class="oh-dropdown__link"
""",
},
{
"action": "Request Document",
"accessibility": "recruitment.cbv.accessibility.request_document",
"attrs": """
data-toggle="oh-modal-toggle"
data-target="#genericModal"
hx-get="{get_document_request_doc}"
hx-target="#genericModalBody"
class="oh-dropdown__link"
""",
},
{
"action": "Add to Rejected",
"accessibility": "recruitment.cbv.accessibility.add_reject",
"attrs": """
hx-target="#genericModalBody"
hx-swap="innerHTML"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
hx-get="{get_add_to_reject}"
class="oh-dropdown__link"
""",
},
{
"action": "Edit Rejected Candidate",
"accessibility": "recruitment.cbv.accessibility.edit_reject",
"attrs": """
hx-target="#genericModalBody"
hx-swap="innerHTML"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
hx-get="{get_add_to_reject}"
class="oh-dropdown__link"
""",
},
{
"action": _("View Note"),
"attrs": """
hx-get="{get_view_note_url}"
data-target="#activitySidebar"
hx-target="#activitySidebar"
onclick="$('#activitySidebar').addClass('oh-activity-sidebar--show')"
""",
},
{
"action": _("Resume"),
"attrs": """
href="{get_resume_url}" target="_blank"
""",
},
{
"action": "archive_status",
"attrs": """
class="oh-dropdown__link"
onclick="archiveCandidate({get_archive_url});"
""",
},
{
"action": "Delete",
"attrs": """
class="oh-dropdown__link oh-dropdown__link--danger"
onclick="event.stopPropagation();
deleteCandidate('{get_delete_url}'); "
""",
},
]
def get_related_groups(self, *args, **kwargs):
related_groups = super().get_related_groups(*args, **kwargs)
rec_id = self.request.GET.get("rec_id")
if rec_id:
related_groups = related_groups.filter(recruitment_id=rec_id)
return related_groups
@method_decorator(login_required, name="dispatch")

View File

@@ -467,9 +467,6 @@ class Stage(HorillaModel):
sequence = models.IntegerField(null=True, default=0)
objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id")
def __str__(self):
return f"{self.stage}"
class Meta:
"""
Meta class to add the additional info
@@ -571,6 +568,30 @@ class Stage(HorillaModel):
return dict(stage_types).get(self.stage_type)
def get_stage_update_url(self):
"""
This method to get update url
"""
return reverse("stage-update-pipeline", kwargs={"pk": self.id})
def get_add_candidate_url(self):
"""
This method to get add candidate url
"""
return f'{reverse_lazy("add-candidate-to-stage")}?stage_id={self.id}'
def get_send_email_url(self):
"""
This method to get send email url
"""
return f'{reverse_lazy("send-mail")}?stage_id={self.id}'
def get_delete_url(self):
"""
This method to get delete url
"""
return f"{reverse_lazy('generic-delete')}?model=recruitment.Stage&pk={self.pk}"
def candidate_upload_path(instance, filename):
"""
@@ -1126,6 +1147,23 @@ class Candidate(HorillaModel):
context={"instance": self, "interviews": interviews},
)
def ordered_group_json(self):
"""
This method is used to get the ordered stages in json format for the candidate
"""
ordered_stages = self.recruitment_id.ordered_stages()
ordered_group_json = json.dumps(
[
{
"id": s.id,
"stage": s.stage,
}
for s in ordered_stages
]
)
return ordered_group_json
def save(self, *args, **kwargs):
if self.stage_id is not None:
self.hired = self.stage_id.stage_type == "hired"

View File

@@ -1,39 +1,99 @@
$(document).ready(function () {
function recruitmentChart(dataSet, labels) {
const styledDataSets = dataSet.map((dataset, index) => {
const colors = ['#a5b4fc', '#fca5a5', '#fdba74', '#34d399', '#fbbf24', '#fb7185', '#60a5fa'];
return {
...dataset,
backgroundColor: colors[index % colors.length],
borderRadius: 10,
barPercentage: 0.6,
categoryPercentage: 0.8
};
});
const data = {
labels: labels,
datasets: dataSet,
datasets: styledDataSets,
};
// Create chart using the Chart.js library
window["myChart"] = {};
if (document.getElementById("recruitmentChart1")){
if (document.getElementById("recruitmentChart1")) {
const ctx = document.getElementById("recruitmentChart1").getContext("2d");
myChart = new Chart(ctx, {
type: "bar",
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 20,
color: '#6b7280'
},
grid: {
color: '#e5e7eb'
}
},
x: {
ticks: {
color: '#6b7280'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
pointStyle: 'circle',
font: {
size: 12
},
color: '#374151',
padding: 15
},
onClick: (e, legendItem, legend) => {
const index = legendItem.datasetIndex;
const chart = legend.chart;
if (chart.isDatasetVisible(index)) {
chart.hide(index);
legendItem.hidden = true;
} else {
chart.show(index);
legendItem.hidden = false;
}
chart.update();
}
},
tooltip: {
enabled: true
}
},
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
"/recruitment/candidate-view" +
"?recruitment=" +
datasetLabel +
"&stage_id__stage_type=" +
label.toLowerCase();
if (activeEls.length > 0) {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
"/recruitment/candidate-view" +
"?recruitment=" +
datasetLabel +
"&stage_id__stage_type=" +
label.toLowerCase();
}
},
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
},
],
});
}
}
@@ -58,55 +118,178 @@ $(document).ready(function () {
myChart.config.type = chartType;
myChart.update();
});
$.ajax({
type: "GET",
url: "/recruitment/hired-candidate-chart",
success: function (response) {
const ctx = document.getElementById("hiredCandidate");
if (ctx) {
new Chart(ctx, {
type: "bar",
data: {
labels: response.labels,
datasets: [
{
label: "#Hired candidates",
data: response.data,
backgroundColor: response.background_color,
borderColor: response.border_color,
borderWidth: 1,
},
],
},
options: {
responsive: true,
const ctx = document.getElementById("hiredCandidate")?.getContext("2d");
if (!ctx || !response?.labels || !response?.data) return;
scales: {
y: {
beginAtZero: true,
},
},
onClick: (e, activeEls) => {
let datasetIndex = activeEls[0].datasetIndex;
let dataIndex = activeEls[0].index;
let datasetLabel = e.chart.data.datasets[datasetIndex].label;
let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
let label = e.chart.data.labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
"/recruitment/candidate-view" +
"?recruitment=" +
label +
"&hired=true";
},
const labels = response.labels;
const values = response.data;
const colors = [
"#facc15",
"#f87171",
"#ddd6fe",
"#a5b4fc",
"#93c5fd",
"#d1d5db",
];
const visibility = Array(labels.length).fill(true);
const hiredCandidateInstance = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [{
label: "Hired Candidates",
data: values,
backgroundColor: colors,
borderRadius: 20,
barPercentage: 0.8,
categoryPercentage: 0.8,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
onClick: (e, activeEls) => {
if (!activeEls.length) return;
const dataIndex = activeEls[0].index;
const label = labels[dataIndex];
localStorage.removeItem("savedFilters");
window.location.href =
`/recruitment/candidate-view?recruitment=${label}&hired=true`;
},
plugins: [
{
afterRender: (chart) => emptyChart(chart),
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
callbacks: {
title: tooltipItems => labels[tooltipItems[0].dataIndex],
label: tooltipItem => `Hired Candidates: ${tooltipItem.raw}`
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1, color: "#6b7280" },
grid: { drawBorder: false, color: "#e5e7eb" }
},
],
x: {
ticks: { display: false },
grid: { display: false },
border: { display: false }
}
}
},
plugins: [{
afterRender: (chart) => {
if (typeof emptyChart === "function") emptyChart(chart);
}
}]
});
// 🧩 Generate Custom Legend
const $legendContainer = $("#hiredLegend");
$legendContainer.empty();
labels.forEach((label, index) => {
const color = colors[index % colors.length];
const $item = $(`
<div style="display: flex; align-items: center; margin-bottom: 6px; cursor: pointer;">
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${color}; margin-right: 8px;"></span>
<span class="legend-label">${label}</span>
</div>
`);
$legendContainer.append($item);
$item.on("click", function () {
visibility[index] = !visibility[index];
// Toggle bar value visibility
hiredCandidateInstance.data.datasets[0].data = values.map((val, i) =>
visibility[i] ? val : 0
);
const $dot = $(this).find("span").first();
const $label = $(this).find(".legend-label");
if (visibility[index]) {
$dot.css("opacity", "1");
$label.css("text-decoration", "none");
} else {
$dot.css("opacity", "0.4");
$label.css("text-decoration", "line-through");
}
hiredCandidateInstance.update();
});
}
});
},
error: function (xhr, status, error) {
console.error("Chart data fetch failed:", error);
}
});
//og
// $.ajax({
// type: "GET",
// url: "/recruitment/hired-candidate-chart",
// success: function (response) {
// const ctx = document.getElementById("hiredCandidate");
// if (ctx) {
// new Chart(ctx, {
// type: "bar",
// data: {
// labels: response.labels,
// datasets: [
// {
// label: "#Hired candidates",
// data: response.data,
// backgroundColor: response.background_color,
// borderColor: response.border_color,
// borderWidth: 1,
// },
// ],
// },
// options: {
// responsive: true,
// scales: {
// y: {
// beginAtZero: true,
// },
// },
// onClick: (e, activeEls) => {
// let datasetIndex = activeEls[0].datasetIndex;
// let dataIndex = activeEls[0].index;
// let datasetLabel = e.chart.data.datasets[datasetIndex].label;
// let value = e.chart.data.datasets[datasetIndex].data[dataIndex];
// let label = e.chart.data.labels[dataIndex];
// localStorage.removeItem("savedFilters");
// window.location.href =
// "/recruitment/candidate-view" +
// "?recruitment=" +
// label +
// "&hired=true";
// },
// },
// plugins: [
// {
// afterRender: (chart) => emptyChart(chart),
// },
// ],
// });
// }
// },
// });
});

View File

@@ -92,4 +92,52 @@
</div>
</div>
</div>
<script>
function archiveCandidate(url,msg) {
Swal.fire({
text: msg,
icon: "question",
showCancelButton: true,
confirmButtonColor: "green",
cancelButtonColor: "#d33",
confirmButtonText: "Confirm"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = url;
}
});
}
function getCSRFToken() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
function deleteCandidate(url) {
Swal.fire({
text: "Do you want to delete this candidate?",
icon: "question",
showCancelButton: true,
confirmButtonColor: "green",
cancelButtonColor: "#d33",
confirmButtonText: "Confirm"
}).then((result) => {
if (result.isConfirmed) {
const form = document.createElement('form');
form.setAttribute('action', url);
form.setAttribute('method', 'post');
const csrfTokenInput = document.createElement('input');
csrfTokenInput.setAttribute('type', 'hidden');
csrfTokenInput.setAttribute('name', 'csrfmiddlewaretoken');
csrfTokenInput.value = getCSRFToken();
form.appendChild(csrfTokenInput);
document.body.appendChild(form);
form.submit();
}
});
}
</script>
{% endblock content %}

View File

@@ -966,7 +966,7 @@ urlpatterns = [
name="candidate-lists-cbv",
),
path(
"candidate-card-cbv/<int:stage_id>",
"candidate-card-cbv/",
pipeline.CandidateCard.as_view(),
name="candidate-card-cbv",
),