[ADD] OFFOARDING: Added dashboard for offboarding module

This commit is contained in:
Horilla
2025-04-08 12:09:07 +05:30
parent 0bdafc6df8
commit 454314e5e9
10 changed files with 806 additions and 6 deletions

View File

@@ -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'<script>window.location.href = "{previous_url}"</script>'
key = "HTTP_HX_REQUEST"

View File

@@ -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
)

View File

@@ -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");
});

View File

@@ -0,0 +1,87 @@
{% load static i18n offboarding_filter %}
<div class="oh-card-dashboard__header oh-card-dashboard__header--divider">
<span class="oh-card-dashboard__title"
>{% trans "Not Returned Assets" %}</span
>
</div>
<div class="oh-card-dashboard__body h-75 overflow-auto position-relative">
{% if assets %}
<div
class="oh-sticky-table__table"
style="border: 1px solid hsl(213, 22%, 93%)"
>
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Employee" %}</div>
<div class="oh-sticky-table__th">{% trans "Asset" %}</div>
<div class="oh-sticky-table__th text-center">
{% trans "Reminder" %}
</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for asset in assets %}
<div
class="oh-sticky-table__tr oh-multiple-table-sort__movable"
onclick="
localStorage.setItem('activeTabAsset','#tab_2');
window.location.href = '{% url 'asset-request-allocation-view' %}?assigned_to_employee_id={{asset.assigned_to_employee_id.id}}'
"
>
<div class="oh-sticky-table__sd">
<div class="oh-profile oh-profile--md">
<div class="oh-profile__avatar mr-1">
<img
src="{{ asset.assigned_to_employee_id.get_avatar }}"
class="oh-profile__image"
/>
</div>
<span class="oh-profile__name oh-text--dark"
>{{ asset.assigned_to_employee_id.get_full_name}}
</span>
</div>
</div>
<div class="oh-sticky-table__td">
{{asset.asset_id.asset_name}} -
{{asset.asset_id.asset_category_id}}
</div>
<div
class="oh-sticky-table__td text-center"
onclick="event.stopPropagation()"
>
<a
hx-get="{% url 'send-mail-employee' asset.assigned_to_employee_id.id %}"
data-toggle="oh-modal-toggle"
data-target="#sendMailModal"
title="{% trans 'Send Mail' %}"
hx-target="#mail-content"
>
<ion-icon name="mail-outline"></ion-icon>
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="oh-404">
<img
style="display: block; width: 120px; margin: 20px auto"
src="{% static 'images/ui/asset.png' %}"
class=""
/>
<h3 style="font-size: 16px" class="oh-404__subtitle">
{% trans "No Assets Due for Return from Offboarding Employees." %}
</h3>
</div>
{% endif %}
</div>
<script>
$(document).ready(function () {
$("[data-toggle='oh-modal-toggle']").on("click", function () {
var target = $(this).data("target");
$(target).toggleClass("oh-modal--show");
});
});
</script>

View File

@@ -0,0 +1,208 @@
{% extends 'index.html' %} {% block content %} {% load static i18n horillafilters %}
<div class="oh-wrapper" id="offboardingDashboard">
<div class="oh-dashboard mb-5" id="dashboard">
<div class="oh-dashboard__left col-12 col-sm-12 col-md-12 col-lg-12">
<div class="oh-dashboard__cards row">
<div class="col-12 col-sm-12 col-md-6 col-lg-4">
<div
class="oh-card-dashboard oh-card-dashboard--success h-100"
>
<a
href="{% url 'offboarding-pipeline' %}"
class="text-decoration-none recruitment"
>
<div
class="oh-card-dashboard__header d-flex justify-content-between align-items-center"
>
<span class="oh-card-dashboard__title"
>{% trans "Exit Ratio" %}</span
>
<span
style="font-size: 24px"
title="{% trans 'Archived Employees / Total Employees' %}"
><ion-icon
name="help-circle-outline"
></ion-icon
></span>
</div>
<div class="oh-card-dashboard__body">
<span class="oh-card-dashboard__count"
>{{exit_ratio}}
</span>
</div>
</a>
</div>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-4">
<div
class="oh-card-dashboard oh-card-dashboard--warning h-100"
>
<div
class="oh-card-dashboard__header d-flex justify-content-between align-items-center"
>
<span class="oh-card-dashboard__title"
>{% trans "Exiting to Joining Ratio" %}</span
>
<span
style="font-size: 24px"
title="{% trans 'Exiting Employees : Joining Employees' %}"
><ion-icon name="help-circle-outline"></ion-icon
></span>
</div>
<div class="oh-card-dashboard__body">
<span class="oh-card-dashboard__count"
>{{resigning_employees.count}} :
{{onboarding_employees}}
</span>
</div>
</div>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-4">
<div
class="oh-card-dashboard oh-card-dashboard--success h-100"
>
<a
href="{% url 'employee-view' %}?is_active=False"
style="text-decoration: none"
>
<div class="oh-card-dashboard__header">
<span class="oh-card-dashboard__title"
>{% trans "Archived Employees" %}</span
>
</div>
<div class="oh-card-dashboard__body">
<span
style="text-decoration: none"
class="oh-card-dashboard__counts"
>
<span class="oh-card-dashboard__count">
{{archived_employees.count}}
</span>
</span>
</div>
</a>
</div>
</div>
</div>
<div class="row">
{% if perms.offboarding.view_offboardingtask %}
<div class="col-12 col-sm-12 col-md-12 col-lg-6 mt-4">
<div
class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent oh-offboarding-card"
hx-get="{% url 'dashboard-task-table' %}"
hx-trigger="load"
>
<div class="animated-background"></div>
</div>
</div>
{% endif %}
{% if "asset"|app_installed %}
{% if perms.offboarding.view_offboarding %}
<div class="col-12 col-sm-12 col-md-12 col-lg-6 mt-4">
<div
class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent oh-offboarding-card"
style="cursor: default"
hx-get="{% url 'dashboard-asset-table' %}"
hx-trigger="load"
>
<div class="animated-background"></div>
</div>
</div>
{% endif %}
{% endif %}
{% if perms.offboarding.view_offboarding %}
<div class="col-12 col-sm-12 col-md-12 col-lg-4 mt-4">
<div
class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent oh-offboarding-card"
style="cursor: default"
>
<div
class="oh-card-dashboard__header oh-card-dashboard__header--divider"
>
<span class="oh-card-dashboard__title"
>{% trans "Department - JobPosition Offboarding" %}</span
>
</div>
<div
class="oh-card-dashboard__body"
style="height: 350px; overflow: auto"
>
<canvas id="departmentChart"></canvas>
</div>
</div>
</div>
<div class="col-12 col-sm-12 col-md-12 col-lg-4 mt-4">
<div
class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent oh-offboarding-card"
style="cursor: default"
hx-get="{% url 'dashboard-feedback-table' %}"
hx-trigger="load"
>
<div
class="oh-card-dashboard__body"
>
<div class="animated-background"></div>
</div>
</div>
</div>
<div class="col-12 col-sm-12 col-md-12 col-lg-4 mt-4">
<div
class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent oh-offboarding-card"
style="cursor: default"
>
<div
class="oh-card-dashboard__header oh-card-dashboard__header--divider"
>
<span class="oh-card-dashboard__title"
>{% trans "Joining and Offboarding Chart" %}</span
>
<span
class="oh-card-dashboard__title float-end"
id="joinChartChange"
style="cursor: pointer"
>
<ion-icon
name="caret-forward"
role="img"
class="md hydrated"
></ion-icon>
</span>
</div>
<div
class="oh-card-dashboard__body"
style="height: 350px; overflow: auto"
>
<canvas id="joinChart"></canvas>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<div
class="oh-modal"
id="sendMailModal"
role="dialog"
aria-labelledby="sendMailModal"
aria-hidden="true"
>
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<h5 class="oh-modal__dialog-title" id="sendMailModalLabel">
{% trans "Send Mail" %}
</h5>
<button class="oh-modal__close" aria-label="Close">
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="mail-content"></div>
</div>
</div>
</div>
<script src="{% static 'offboarding/dashboard.js' %}"></script>
{% endblock content %}

View File

@@ -0,0 +1,75 @@
{% load static i18n offboarding_filter %}
<div class="oh-card-dashboard__header oh-card-dashboard__header--divider">
<span class="oh-card-dashboard__title"
>{% trans "Offboarding Employees Feedbacks" %}</span
>
</div>
<div class="oh-card-dashboard__body h-75 overflow-auto position-relative">
{% if feedbacks %}
<div
class="oh-sticky-table__table"
style="border: 1px solid hsl(213, 22%, 93%)"
>
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Employee" %}</div>
<div class="oh-sticky-table__th">{% trans "Feedback" %}</div>
<div class="oh-sticky-table__th">{% trans "Status" %}</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for feedback in feedbacks %}
<div class="oh-sticky-table__tr oh-multiple-table-sort__movable">
<div class="oh-sticky-table__sd">
<a href="">
<div class="oh-profile oh-profile--md">
<div class="oh-profile__avatar mr-1">
<img
src="{{ feedback.employee_id.get_avatar }}"
class="oh-profile__image"
/>
</div>
<span class="oh-profile__name oh-text--dark"
>{{ feedback.employee_id.get_full_name}}
</span>
</div>
</a>
</div>
<div
class="oh-sticky-table__td"
onclick="event.stopPropagation()"
>
{{feedback.review_cycle}}
</div>
<div
class="oh-sticky-table__td fw-bold"
onclick="event.stopPropagation()"
>
{{feedback.status}}
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class= "oh-404">
<img
style="display: block; width: 120px; margin: 20px auto"
src="{% static 'images/ui/feedback.png' %}"
class=""
/>
<h3 style="font-size: 16px" class="oh-404__subtitle">
{% trans "No feedbacks for Offboarding Employees." %}
</h3>
</div>
{% endif %}
</div>
<script>
$(document).ready(function () {
$("[data-toggle='oh-modal-toggle']").on("click", function () {
var target = $(this).data("target");
$(target).toggleClass("oh-modal--show");
});
});
</script>

View File

@@ -0,0 +1,70 @@
{% load static i18n offboarding_filter %}
<div class="oh-card-dashboard__header oh-card-dashboard__header--divider">
<span class="oh-card-dashboard__title">{% trans "Task Status" %}</span>
</div>
<div class="oh-card-dashboard__body h-75 overflow-auto position-relative">
{% if employees %}
<div
class="oh-sticky-table__table"
style="border: 1px solid hsl(213, 22%, 93%)"
>
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Employee" %}</div>
<div class="oh-sticky-table__th">{% trans "Stage" %}</div>
<div class="oh-sticky-table__th text-center">
{% trans "Task Status" %}
</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for employee in employees %}
<div class="oh-sticky-table__tr oh-multiple-table-sort__movable">
<div class="oh-sticky-table__sd">
<a href="{% url 'employee-view-individual' employee.employee_id.id %}">
<div class="oh-profile oh-profile--md">
<div class="oh-profile__avatar mr-1">
<img
src="{{ employee.employee_id.get_avatar }}"
class="oh-profile__image"
/>
</div>
<span class="oh-profile__name oh-text--dark"
>{{ employee.employee_id.get_full_name }}</span
>
</div>
</a>
</div>
<div
class="oh-sticky-table__td"
onclick="event.stopPropagation()"
>
{{employee.stage_id}}
</div>
<div class="oh-sticky-table__td text-center">
<div
class="oh-checkpoint-badge oh-checkpoint-badge--secondary"
title="Completed {{ employee.employeetask_set|completed_tasks }} of {{employee.employeetask_set.all|length}} tasks"
>
{{ employee.employeetask_set|completed_tasks }} /
{{ employee.employeetask_set.all|length }}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class= "oh-404">
<img
style="display: block; width: 120px; margin: 20px auto"
src="{% static 'images/ui/conditions.png' %}"
class=""
/>
<h3 style="font-size: 16px" class="oh-404__subtitle">
{% trans "No Pending Tasks for Offboarding Employees." %}
</h3>
</div>
{% endif %}
</div>

View File

@@ -112,7 +112,7 @@
><ion-icon name="create-outline"></ion-icon
></a>
{% endif %}
{% if employee.employee_id.get_archive_condition %}
{% if employee.employee_id and employee.employee_id.get_archive_condition %}
<a
type="button"
title="{% trans 'Show managing records' %}"

View File

@@ -4,6 +4,7 @@ offboarding/urls.py
This module is used to register url mappings to functions
"""
from django.apps import apps
from django.urls import path
from offboarding import views
@@ -96,4 +97,42 @@ urlpatterns = [
views.filter_pipeline,
name="offboarding-pipeline-filter",
),
path(
"dashboard",
views.offboarding_dashboard,
name="offboarding-dashboard",
),
path(
"dashboard-task-table",
views.dashboard_task_table,
name="dashboard-task-table",
),
path(
"dashboard-department-chart",
views.department_job_postion_chart,
name="dashboard-department-chart",
),
path(
"dashboard-join-chart",
views.dashboard_join_chart,
name="dashboard-join-chart",
),
]
if apps.is_installed("asset"):
urlpatterns += [
path(
"dashboard-asset-table",
views.dashboard_asset_table,
name="dashboard-asset-table",
),
]
if apps.is_installed("pms"):
urlpatterns += [
path(
"dashboard-feedback-table",
views.dashboard_feedback_table,
name="dashboard-feedback-table",
),
]

View File

@@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from base.context_processors import intial_notice_period
from base.methods import closest_numbers, eval_validate, paginator_qry, sortby
from base.models import Department, JobPosition
from base.views import general_settings
from employee.models import Employee
from horilla.decorators import (
@@ -73,9 +74,7 @@ def pipeline_grouper(filters={}, offboardings=[]):
all_stages_grouper.append({"grouper": stage, "list": []})
stage_employees = PipelineEmployeeFilter(
filters,
OffboardingEmployee.objects.filter(
stage_id=stage, employee_id__is_active=True
),
OffboardingEmployee.objects.filter(stage_id=stage),
).qs.order_by("stage_id__id")
page_name = "page" + stage.title + str(offboarding.id)
employee_grouper = group_by(
@@ -997,3 +996,206 @@ def get_notice_period_end_date(request):
"end_date": end_date,
}
return JsonResponse(response)
@login_required
@any_manager_can_enter(
perm=[
"offboarding.view_offboarding",
"offboarding.view_offboardingtask",
"offboarding.view_offboardingemployee",
]
)
def offboarding_dashboard(request):
"""
This method is used to render the offboarding dashboard page.
"""
onboarding_employees = []
if apps.is_installed("recruitment"):
Candidate = get_horilla_model_class("recruitment", "candidate")
onboarding_employees = Candidate.objects.filter(
onboarding_stage__isnull=False, converted_employee_id__isnull=True
)
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"
)
exit_ratio = (
(archived_employees.count() / employees.count()) if employees.count() > 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})