From 8f813d45457341d0b45da647e923cbc52f28f577 Mon Sep 17 00:00:00 2001 From: Horilla Date: Wed, 7 Feb 2024 17:19:40 +0530 Subject: [PATCH] [ADD] RECRUITMENT: New dashboard charts and design updates in dashboard --- recruitment/models.py | 1 + .../static/dashboard/candidateChart.js | 59 +++ .../templates/dashboard/dashboard.html | 347 ++++++++++++------ .../templates/stage/stage_component.html | 2 +- recruitment/urls.py | 5 + recruitment/views/dashboard.py | 49 ++- 6 files changed, 347 insertions(+), 116 deletions(-) create mode 100644 recruitment/static/dashboard/candidateChart.js diff --git a/recruitment/models.py b/recruitment/models.py index b3e31986c..9e9770396 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -334,6 +334,7 @@ class Candidate(models.Model): ("rejected", "Rejected / Canceled"), ], default="not_sent", + editable = False, ) probation_end = models.DateField(null=True, editable=False) offer_letter_status = models.CharField( diff --git a/recruitment/static/dashboard/candidateChart.js b/recruitment/static/dashboard/candidateChart.js new file mode 100644 index 000000000..20c6d88b9 --- /dev/null +++ b/recruitment/static/dashboard/candidateChart.js @@ -0,0 +1,59 @@ +$(document).ready(function(){ + let myChart; // Declare myChart globally to access it outside the scope + + function candidateChart(dataSet, labels){ + const data = { + labels: labels, + datasets: [{ + data: dataSet.map(item => item.data), + backgroundColor: ['#C6BEC4', '#FFF255', '#55C4FF', '#FF4646', '#2AFF0C'] + }] + }; + + const ctx = document.getElementById('candidateChart').getContext('2d'); + myChart = new Chart(ctx, { + type: 'pie', + data: data, + options: { + onClick: handleClick // Attach onClick event handler + } + }); + } + + function handleClick(event, chartElements) { + if (chartElements.length > 0) { + // Get the index of the clicked element + const index = chartElements[0].index; + + if(index === 0){ + // Assuming each data point corresponds to a URL + url = '/recruitment/candidate-view?offer_letter_status=not_sent' + }else if(index === 1){ + + url = '/recruitment/candidate-view?offer_letter_status=sent' + }else if(index === 2){ + + url = '/recruitment/candidate-view?offer_letter_status=accepted' + }else if(index === 3){ + + url = '/recruitment/candidate-view?offer_letter_status=rejected' + }else{ + + url = '/recruitment/candidate-view?offer_letter_status=joined' + } + // Redirect to the corresponding URL + window.location.href = url; + } + } + + $.ajax({ + url: "/recruitment/candidate-status", + type: "GET", + success: function(response){ + dataSet = response.dataSet; + labels = response.labels; + candidateChart(dataSet, labels); + }, + }); + +}); diff --git a/recruitment/templates/dashboard/dashboard.html b/recruitment/templates/dashboard/dashboard.html index b84823319..894dbf27c 100644 --- a/recruitment/templates/dashboard/dashboard.html +++ b/recruitment/templates/dashboard/dashboard.html @@ -15,51 +15,54 @@ .todo-task{ background-color: #8d8d8d2e !important; } + .tooltip { + position: absolute; + background-color: #000; + color: #fff; + padding: 5px; + border-radius: 5px; + display: block; + margin-top:80px; + } +
-
- -
+ -
- + - - -
-
-
-
-
- {% trans "Conversion Rate" %} -
-
-
- {{conversion_ratio}}% -
- {{100|sub:conversion_ratio|floatformat:1}}% -
-
+
+
+
+ {% trans "Conversion Rate" %} +
- -
-
-
- {% trans "Offer Acceptance Rate (OAR)" %} -
-
-
- {{acceptance_ratio}}% -
- {{100|sub:acceptance_ratio|floatformat:1}}% -
+
+
+ {{conversion_ratio}}%
+
-
+
+
+
+ {% trans "Offer Acceptance Rate (OAR)" %} + +
+
+
+ {{acceptance_ratio}}% +
+
+
+
+ +
+ +
+
+
+ {% trans "Skill Zone Status" %} +
+
+ {% if skill_zone %} +
    + {% for skill in skill_zone %} +
  • + +

    + +   {{skill.skillzonecandidate_set.all|length}}   {% if skill.skillzonecandidate_set.all|length != 1 %} {% trans "Candidates" %} {% else %} {% trans "Candidate" %} {% endif %}

    +
  • + {% endfor %} +
+ {% else %} +
+
+ +

{% trans "No skill zone available." %}

+
+
+ {% endif %} + +
+
+ + +
{% trans "Open Positions By Department" %} @@ -144,10 +173,10 @@
-
+
{% trans "Candidate on Onboard" %} - {% if onboarding_count %}{% trans "View all" %}{% endif %} + {% if onboarding_count %}{% trans "View" %}{% endif %}
{% if onboarding_count %} @@ -164,7 +193,9 @@
{{cand}}
-

- {{cand.job_position_id}}

+

+ +   {{cand.job_position_id}}

{% endfor %} @@ -176,81 +207,112 @@
{% endif %} -
-
-
- {% if request.user|is_in_task_managers %} -
-
- {% endif %} -
+
+ {% if request.user|is_in_task_managers %}
+
+ {% endif %} +
- {% trans "Candidates Per Stage" %} - +
+ {% trans "Candidates Per Stage" %} + +
+
+ {% if not stage_chart_count %} + + {% else %} +
+
+ +

{% trans "No recruitment stages currently available." %}

+
+
+ {% endif %} +
-
- {% if stage_chart_count %} - - {% else %} +
+
+ +
+ + +
+
+
+ + {% trans "Joinings Per Month" %} + + + + +
+ {% if joining %} +
+ {% else %}
- -

{% trans "No recruitment stages currently available." %}

+ +

{% trans "No records were available." %}

- {% endif %} + {% endif %}
-
-
-
-
-
+ +
+
+
+ {% trans "Candidate Offer Letter Status" %} + +
+
+ {% if total_candidates %} +
+ + + +
+ {% else %} +
+
+ +
+
+

{% trans "No Candidates available." %}

+ {% endif %} - {% trans "Joinings Per Month" %} - - - - -
- {% if joining %} -
- {% else %} -
-
- -

{% trans "No records were available." %}

+
- {% endif %}
+
+
@@ -315,8 +377,6 @@
-
-
@@ -326,6 +386,7 @@ + @@ -344,6 +405,66 @@ } selectyear.appendChild(option); } + + + document.addEventListener("DOMContentLoaded", function() { + const icon = document.getElementById('offerhelptext'); + icon.addEventListener('click', function(event) { + acceptance_helptext(event); // Pass the event object to the acceptance_helptext function + }); + }); + +function acceptance_helptext(event) { + event = event || window.event; + const icon = event.target || event.srcElement; + const tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + tooltip.textContent = 'Offer Acceptance Rate = ( Onboarding candidates / Total Hired Candidates ) * 100'; // Help text + + // Position the tooltip relative to the icon + const rect = icon.getBoundingClientRect(); + tooltip.style.top = rect.top + icon.offsetHeight + 'px'; + tooltip.style.left = rect.left + 'px'; + + // Append tooltip to the body + document.body.appendChild(tooltip); + + // Remove tooltip when mouse is moved away from the icon + icon.addEventListener('mouseout', function () { + document.body.removeChild(tooltip); + }); +} + + +document.addEventListener("DOMContentLoaded", function() { + const icon = document.getElementById('conversionhelptext'); + icon.addEventListener('click', function(event) { + conversion_helptext(event); // Pass the event object to the conversion_helptext function + }); +}); + +function conversion_helptext(event) { + event = event || window.event; + const icon = event.target || event.srcElement; + const tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + tooltip.textContent = 'Conversion Rate = ( Total Hired Candidates / Total Candidates ) * 100'; // Help text + + // Position the tooltip relative to the icon + const rect = icon.getBoundingClientRect(); + tooltip.style.top = rect.top + icon.offsetHeight + 'px'; + tooltip.style.left = rect.left + 'px'; + + // Append tooltip to the body + document.body.appendChild(tooltip); + + // Remove tooltip when mouse is moved away from the icon + icon.addEventListener('mouseout', function () { + document.body.removeChild(tooltip); + }); +} + + {% endblock content %} diff --git a/recruitment/templates/stage/stage_component.html b/recruitment/templates/stage/stage_component.html index e201405e3..9ac8a2db0 100644 --- a/recruitment/templates/stage/stage_component.html +++ b/recruitment/templates/stage/stage_component.html @@ -57,7 +57,7 @@ > {% trans "Recruitment" %}
-
+
{% trans "Actions" %}
diff --git a/recruitment/urls.py b/recruitment/urls.py index 096b90115..a9d6228ee 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -257,6 +257,11 @@ urlpatterns = [ recruitment.views.dashboard.dashboard_vacancy, name="dashboard-vacancy", ), + path( + "candidate-status", + recruitment.views.dashboard.candidate_status, + name="candidate-status", + ), path( "candidate-sequence-update", views.candidate_sequence_update, diff --git a/recruitment/views/dashboard.py b/recruitment/views/dashboard.py index 4a5784712..6db4e3dff 100644 --- a/recruitment/views/dashboard.py +++ b/recruitment/views/dashboard.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from django.shortcuts import render from horilla.decorators import login_required from recruitment.decorators import manager_can_enter -from recruitment.models import Candidate, Recruitment, Stage +from recruitment.models import Candidate, Recruitment, SkillZone, Stage from base.models import Department, JobPosition from employee.models import EmployeeWorkInformation @@ -86,6 +86,7 @@ def dashboard(request): job_data = list(zip(all_job, initial, test, interview, hired)) recruitment_obj = Recruitment.objects.filter(closed=False) + ongoing_recruitments = len(recruitment_obj) for rec in recruitment_obj: data = [stage_type_candidate_count(rec, type[0]) for type in Stage.stage_types] @@ -130,11 +131,13 @@ def dashboard(request): total_candidate_ratio = f"{((total_candidates / total_vacancy) * 100):.1f}" if total_hired_candidates != 0: acceptance_ratio = f"{((onboarding_count / total_hired_candidates) * 100):.1f}" + + skill_zone = SkillZone.objects.all() return render( request, "dashboard/dashboard.html", { - "total_candidates": total_candidates, + "ongoing_recruitments": ongoing_recruitments, "total_candidate_ratio" : total_candidate_ratio, "total_hired_candidates": total_hired_candidates, "conversion_ratio": conversion_ratio, @@ -148,6 +151,8 @@ def dashboard(request): "dep_vacancy" : dep_vacancy, "stage_chart_count" : stage_chart_count, "onboarding_count" : onboarding_count, + "total_candidates":total_candidates, + 'skill_zone' : skill_zone }, ) @@ -267,3 +272,43 @@ def get_open_position(request): job_info = serializers.serialize("json", queryset) rec_info = serializers.serialize("json", [recruitment_obj]) return JsonResponse({"openPositions": job_info, "recruitmentInfo": rec_info}) + + +@login_required +@manager_can_enter(perm="recruitment.view_recruitment") +def candidate_status(_request): + """ + This method is used to generate a CAndidate status chart for the dashboard + """ + + not_sent_candidates = Candidate.objects.filter(offer_letter_status = 'not_sent').count() + sent_candidates = Candidate.objects.filter(offer_letter_status = 'sent').count() + accepted_candidates = Candidate.objects.filter(offer_letter_status = 'accepted').count() + rejected_candidates = Candidate.objects.filter(offer_letter_status = 'rejected').count() + joined_candidates = Candidate.objects.filter(offer_letter_status = 'joined').count() + + data_set = [] + labels = ['Not Sent', 'Sent', 'Accepted', 'Rejected', 'Joined'] + data = [not_sent_candidates, sent_candidates, accepted_candidates, rejected_candidates, joined_candidates] + + for i in range(len(data)): + + data_set.append( + { + "label": labels[i], + "data":data[i] + } + ) + + # for i in range(len(data)): + # if data[i] != 0: + # data_set.append({ + # "label": labels[i], + # "data": data[i] + # }) + + # # Remove labels corresponding to data points with value 0 + # labels = [label for label, d in zip(labels, data) if d != 0] + + + return JsonResponse({"dataSet": data_set, "labels": labels})