diff --git a/offboarding/decorators.py b/offboarding/decorators.py index 9743dd464..a1811c079 100644 --- a/offboarding/decorators.py +++ b/offboarding/decorators.py @@ -21,8 +21,13 @@ from offboarding.models import ( def any_manager_can_enter(function, perm, offboarding_employee_can_enter=False): def _function(request, *args, **kwargs): employee = request.user.employee_get + permissions = perm + has_permission = False + if not isinstance(permissions, (list, tuple, set)): + permissions = [permissions] + has_permission = any(request.user.has_perm(perm) for perm in permissions) if ( - request.user.has_perm(perm) + has_permission or offboarding_employee_can_enter or ( Offboarding.objects.filter(managers=employee).exists() @@ -32,6 +37,7 @@ def any_manager_can_enter(function, perm, offboarding_employee_can_enter=False): ): return function(request, *args, **kwargs) else: + messages.info(request, "You don't have permission.") previous_url = request.META.get("HTTP_REFERER", "/") script = f'' key = "HTTP_HX_REQUEST" diff --git a/offboarding/sidebar.py b/offboarding/sidebar.py index 7d68c83eb..20bc6c181 100644 --- a/offboarding/sidebar.py +++ b/offboarding/sidebar.py @@ -17,6 +17,11 @@ ACCESSIBILITY = "offboarding.sidebar.offboarding_accessibility" SUBMENUS = [ + { + "menu": _("Dashboard"), + "redirect": reverse("offboarding-dashboard"), + "accessibility": "offboarding.sidebar.dashboard_accessibility", + }, { "menu": _("Exit Process"), "redirect": reverse("offboarding-pipeline"), @@ -33,7 +38,7 @@ def offboarding_accessibility(request, menu, user_perms, *args, **kwargs): accessible = False try: accessible = ( - request.user.has_perm("offboarding.view_offboarding") + request.user.has_module_perms("offboarding") or any_manager(request.user.employee_get) or is_offboarding_employee(request.user.employee_get) ) @@ -45,3 +50,13 @@ def resignation_letter_accessibility(request, menu, user_perms, *args, **kwargs) return resignation_request_enabled(request)[ "enabled_resignation_request" ] and request.user.has_perm("offboarding.view_resignationletter") + + +def dashboard_accessibility(request, *args): + """ + Check if the user has permission to view the dashboard. + """ + + return request.user.has_module_perms("offboarding") or any_manager( + request.user.employee_get + ) diff --git a/offboarding/static/offboarding/dashboard.js b/offboarding/static/offboarding/dashboard.js new file mode 100644 index 000000000..8806f77a7 --- /dev/null +++ b/offboarding/static/offboarding/dashboard.js @@ -0,0 +1,98 @@ +$(document).ready(function () { + var departmentChart; + var joinChart; + + var department_chart = () => { + $.ajax({ + url: "/offboarding/dashboard-department-chart", + type: "GET", + success: function (data) { + var ctx = $("#departmentChart"); + if (departmentChart) { + departmentChart.destroy(); + } + departmentChart = new Chart(ctx, { + type: "bar", + data: { + labels: data.labels, + datasets: data.datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true, + }, + }, + }, + plugins: [ + { + afterRender: (chart) => emptyChart(chart), + }, + ], + }); + }, + error: function (xhr, status, error) { + console.error("Error fetching department chart data:", error); + }, + }); + }; + + var join_chart = (type) => { + $.ajax({ + url: "/offboarding/dashboard-join-chart", + type: "GET", + success: function (data) { + var ctx = $("#joinChart"); + if (joinChart) { + joinChart.destroy(); + } + joinChart = new Chart(ctx, { + type: type, + data: { + labels: data.labels, + datasets: [ + { + label: "Employees", + data: data.items, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + }, + plugins: [ + { + afterRender: (chart) => emptyChart(chart), + }, + ], + }); + }, + error: function (xhr, status, error) { + console.error("Error fetching department chart data:", error); + }, + }); + }; + + $("#joinChartChange").click(function (e) { + var chartType = joinChart.config.type; + if (chartType === "line") { + chartType = "bar"; + } else if (chartType === "bar") { + chartType = "doughnut"; + } else if (chartType === "doughnut") { + chartType = "pie"; + } else if (chartType === "pie") { + chartType = "line"; + } + join_chart(chartType); + }); + + department_chart("pie"); + join_chart("bar"); +}); diff --git a/offboarding/templates/offboarding/dashboard/asset_returned_table.html b/offboarding/templates/offboarding/dashboard/asset_returned_table.html new file mode 100644 index 000000000..e99708cf8 --- /dev/null +++ b/offboarding/templates/offboarding/dashboard/asset_returned_table.html @@ -0,0 +1,87 @@ +{% load static i18n offboarding_filter %} + +
+ {% trans "Not Returned Assets" %} +
+
+ {% if assets %} +
+
+
+
{% trans "Employee" %}
+
{% trans "Asset" %}
+
+ {% trans "Reminder" %} +
+
+
+
+ {% for asset in assets %} +
+
+
+
+ +
+ {{ asset.assigned_to_employee_id.get_full_name}} + +
+
+
+ {{asset.asset_id.asset_name}} - + {{asset.asset_id.asset_category_id}} +
+
+ + + +
+
+ {% endfor %} +
+
+ {% else %} +
+ +

+ {% trans "No Assets Due for Return from Offboarding Employees." %} +

+
+ {% endif %} +
+ diff --git a/offboarding/templates/offboarding/dashboard/dashboard.html b/offboarding/templates/offboarding/dashboard/dashboard.html new file mode 100644 index 000000000..92ec2a5ef --- /dev/null +++ b/offboarding/templates/offboarding/dashboard/dashboard.html @@ -0,0 +1,208 @@ +{% extends 'index.html' %} {% block content %} {% load static i18n horillafilters %} + +
+
+
+
+ +
+
+
+ {% trans "Exiting to Joining Ratio" %} + +
+
+ {{resigning_employees.count}} : + {{onboarding_employees}} + +
+
+
+ +
+
+ {% if perms.offboarding.view_offboardingtask %} +
+
+
+
+
+ {% endif %} + {% if "asset"|app_installed %} + {% if perms.offboarding.view_offboarding %} +
+
+
+
+
+ {% endif %} + {% endif %} + {% if perms.offboarding.view_offboarding %} + +
+
+
+ {% trans "Department - JobPosition Offboarding" %} +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans "Joining and Offboarding Chart" %} + + + +
+
+ +
+
+
+ {% endif %} +
+
+
+ + +
+ + +{% endblock content %} diff --git a/offboarding/templates/offboarding/dashboard/employee_feedback_table.html b/offboarding/templates/offboarding/dashboard/employee_feedback_table.html new file mode 100644 index 000000000..c4a7af1c5 --- /dev/null +++ b/offboarding/templates/offboarding/dashboard/employee_feedback_table.html @@ -0,0 +1,75 @@ +{% load static i18n offboarding_filter %} + +
+ {% trans "Offboarding Employees Feedbacks" %} +
+
+ {% if feedbacks %} +
+
+
+
{% trans "Employee" %}
+
{% trans "Feedback" %}
+
{% trans "Status" %}
+
+
+
+ {% for feedback in feedbacks %} +
+ +
+ {{feedback.review_cycle}} +
+
+ {{feedback.status}} +
+
+ {% endfor %} +
+
+ {% else %} +
+ +

+ {% trans "No feedbacks for Offboarding Employees." %} +

+
+ {% endif %} +
+ diff --git a/offboarding/templates/offboarding/dashboard/employee_task_table.html b/offboarding/templates/offboarding/dashboard/employee_task_table.html new file mode 100644 index 000000000..ee2c508d6 --- /dev/null +++ b/offboarding/templates/offboarding/dashboard/employee_task_table.html @@ -0,0 +1,70 @@ +{% load static i18n offboarding_filter %} + +
+ {% trans "Task Status" %} +
+
+ {% if employees %} +
+
+
+
{% trans "Employee" %}
+
{% trans "Stage" %}
+
+ {% trans "Task Status" %} +
+
+
+
+ {% for employee in employees %} +
+ +
+ {{employee.stage_id}} +
+
+
+ {{ employee.employeetask_set|completed_tasks }} / + {{ employee.employeetask_set.all|length }} +
+
+
+ {% endfor %} +
+
+ {% else %} +
+ +

+ {% trans "No Pending Tasks for Offboarding Employees." %} +

+
+ {% endif %} +
diff --git a/offboarding/templates/offboarding/task/table_body.html b/offboarding/templates/offboarding/task/table_body.html index c0fbe61a3..3f4adb39c 100644 --- a/offboarding/templates/offboarding/task/table_body.html +++ b/offboarding/templates/offboarding/task/table_body.html @@ -112,7 +112,7 @@ > {% endif %} - {% if employee.employee_id.get_archive_condition %} + {% if employee.employee_id and employee.employee_id.get_archive_condition %} 0 else 0 + ) + + context = { + "exit_ratio": round(exit_ratio, 4), + "employees": employees, + "archived_employees": archived_employees, + "resigning_employees": resigning_employees, + "onboarding_employees": len(onboarding_employees), + } + return render(request, "offboarding/dashboard/dashboard.html", context) + + +@login_required +@any_manager_can_enter( + ["offboarding.view_offboarding", "offboarding.view_offboardingtask"] +) +def dashboard_task_table(request): + """ + This method is used to render the employee task table page in the dashboard. + """ + + employees = OffboardingEmployee.objects.entire() + return render( + request, + "offboarding/dashboard/employee_task_table.html", + { + "employees": employees, + }, + ) + + +if apps.is_installed("asset"): + + @login_required + @any_manager_can_enter(["offboarding.view_offboarding"]) + def dashboard_asset_table(request): + """ + This method is used to render the employee assets table page in the dashboard. + """ + AssetAssignment = get_horilla_model_class( + app_label="asset", model="assetassignment" + ) + + offboarding_employees = OffboardingEmployee.objects.entire().values_list( + "employee_id__id", flat=True + ) + assets = AssetAssignment.objects.entire().filter( + return_status__isnull=True, + assigned_to_employee_id__in=offboarding_employees, + ) + return render( + request, + "offboarding/dashboard/asset_returned_table.html", + {"assets": assets}, + ) + + +if apps.is_installed("pms"): + + @login_required + @any_manager_can_enter("offboarding.view_offboarding") + def dashboard_feedback_table(request): + """ + This method is used to render the employee assets table page in the dashboard. + """ + + Feedback = get_horilla_model_class(app_label="pms", model="feedback") + + offboarding_employees = OffboardingEmployee.objects.entire().values_list( + "employee_id__id", "notice_period_starts" + ) + + if offboarding_employees: + id_list, date_list = map(list, zip(*offboarding_employees)) + else: + id_list, date_list = [], [] + + feedbacks = ( + Feedback.objects.entire() + .filter(employee_id__in=id_list) + .exclude(status="Closed") + ) + return render( + request, + "offboarding/dashboard/employee_feedback_table.html", + {"feedbacks": feedbacks}, + ) + + +@login_required +@any_manager_can_enter("offboarding.view_offboarding") +def dashboard_join_chart(request): + """ + This method is used to render the joining - offboarding chart. + """ + + employees = Employee.objects.entire() + offboarding_employees = OffboardingEmployee.objects.entire() + archived_employees = offboarding_employees.filter(stage_id__type="archived") + resigning_employees = employees.filter(resignationletter__isnull=False).exclude( + offboardingemployee__stage_id__type="archived" + ) + + labels = ["resigning", "archived"] + items = [ + resigning_employees.count(), + archived_employees.count(), + ] + if apps.is_installed("recruitment"): + Candidate = get_horilla_model_class(app_label="recruitment", model="candidate") + onboarding_employees = Candidate.objects.filter( + onboarding_stage__isnull=False, converted_employee_id__isnull=True + ) + labels.append("New") + items.append(onboarding_employees.count()) + + response = { + "labels": labels, + "items": items, + } + return JsonResponse(response) + + +@login_required +@any_manager_can_enter("offboarding.view_offboarding") +def department_job_postion_chart(request): + """ + This method is used to render the department - job position chart. + """ + + departments = Department.objects.all() + offboarding_employees = OffboardingEmployee.objects.entire() + + selected_departments = [ + dept + for dept in departments + if offboarding_employees.filter( + employee_id__employee_work_info__department_id=dept.id + ).exists() + ] + + job_positions = JobPosition.objects.filter( + id__in=offboarding_employees.values( + "employee_id__employee_work_info__job_position_id" + ).distinct() + ) + + labels = [dept.department for dept in selected_departments] + + datasets = [] + for job in job_positions: + job_dept = job.department_id + if job_dept not in selected_departments: + continue + + data = [0] * len(selected_departments) + dept_index = labels.index(job_dept.department) + + count = offboarding_employees.filter( + employee_id__employee_work_info__job_position_id=job.id + ).count() + data[dept_index] = count + + datasets.append( + { + "label": f"{job.job_position} ({job_dept.department})", + "data": data, + "backgroundColor": f"hsl({hash(job.job_position) % 360}, 70%, 50%, 0.6)", + } + ) + + return JsonResponse({"labels": labels, "datasets": datasets})