hrms emp dashboard changes

This commit is contained in:
pranaysaidurga 2026-07-09 22:21:11 +05:30
parent a707df36cf
commit e93480648a
4 changed files with 188 additions and 40 deletions

View File

@ -124,23 +124,33 @@ class HrmsEmployeeDashboard(http.Controller):
def _leave_balances(self, employee): def _leave_balances(self, employee):
LeaveType = request.env["hr.leave.type"].sudo() LeaveType = request.env["hr.leave.type"].sudo()
Allocation = request.env["hr.leave.allocation"].sudo()
Leave = request.env["hr.leave"].sudo() Leave = request.env["hr.leave"].sudo()
employee = employee.sudo()
leave_types = LeaveType.search([("active", "=", True)], order="sequence, name")
allocation_data = leave_types.get_allocation_data(employee)[employee]
allocation_data_by_type = {
leave_type_id: data
for _name, data, _requires_allocation, leave_type_id in allocation_data
}
balances = [] balances = []
for leave_type in LeaveType.search([("active", "=", True)], order="sequence, name"): for leave_type in leave_types:
allocations = Allocation.search([ if leave_type.requires_allocation == "yes":
("employee_id", "=", employee.id), data = allocation_data_by_type.get(leave_type.id, {})
("holiday_status_id", "=", leave_type.id), allocated = data.get("max_leaves", 0)
("state", "=", "validate"), taken = data.get("leaves_taken", 0)
]) planned = data.get("leaves_requested", 0)
leaves = Leave.search([ remaining = data.get("virtual_remaining_leaves", 0)
("employee_id", "=", employee.id), else:
("holiday_status_id", "=", leave_type.id), leaves = Leave.search([
("state", "in", ["confirm", "validate1", "validate"]), ("employee_id", "=", employee.id),
]) ("holiday_status_id", "=", leave_type.id),
allocated = sum(allocations.mapped("number_of_days")) ("state", "in", ["confirm", "validate1", "validate"]),
taken = sum(leaves.filtered(lambda leave: leave.state == "validate").mapped("number_of_days")) ])
planned = sum(leaves.filtered(lambda leave: leave.state in ("confirm", "validate1")).mapped("number_of_days")) allocated = 0
taken = sum(leaves.filtered(lambda leave: leave.state == "validate").mapped("number_of_days"))
planned = sum(leaves.filtered(lambda leave: leave.state in ("confirm", "validate1")).mapped("number_of_days"))
remaining = 0
chart_total = max(allocated, taken + planned + max(remaining, 0), 1)
if allocated or taken or planned or leave_type.requires_allocation == "no": if allocated or taken or planned or leave_type.requires_allocation == "no":
balances.append({ balances.append({
"id": leave_type.id, "id": leave_type.id,
@ -148,7 +158,10 @@ class HrmsEmployeeDashboard(http.Controller):
"allocated": round(allocated, 2), "allocated": round(allocated, 2),
"taken": round(taken, 2), "taken": round(taken, 2),
"planned": round(planned, 2), "planned": round(planned, 2),
"remaining": round(allocated - taken - planned, 2) if leave_type.requires_allocation != "no" else 0, "remaining": round(remaining, 2),
"taken_percent": round((taken / chart_total) * 100, 2),
"planned_percent": round((planned / chart_total) * 100, 2),
"remaining_percent": round((max(remaining, 0) / chart_total) * 100, 2),
"requires_allocation": leave_type.requires_allocation, "requires_allocation": leave_type.requires_allocation,
}) })
return balances return balances
@ -173,16 +186,19 @@ class HrmsEmployeeDashboard(http.Controller):
def _get_public_holidays(self, employee, month_start, month_end): def _get_public_holidays(self, employee, month_start, month_end):
start_dt = datetime.combine(month_start, time.min) start_dt = datetime.combine(month_start, time.min)
end_dt = datetime.combine(month_end, time.max) end_dt = datetime.combine(month_end, time.max)
calendar = employee.resource_calendar_id or employee.company_id.resource_calendar_id calendar = employee.resource_calendar_id or employee.company_id.resource_calendar_id
holidays = request.env["resource.calendar.leaves"].sudo().search([ holidays = request.env["resource.calendar.leaves"].sudo().search([
("date_from", "<=", end_dt), ("date_from", "<=", end_dt),
("date_to", ">=", start_dt), ("date_to", ">=", start_dt),
("company_id", "in", [False, employee.company_id.id]), ("company_id", "in", [False, employee.company_id.id]),
("resource_id", "=", False), # Only company/public holidays
], order="date_from asc") ], order="date_from asc")
return holidays.filtered(lambda holiday: ( if calendar:
(not holiday.resource_id or holiday.resource_id == employee.resource_id) holidays = holidays.filtered(
and (not holiday.calendar_id or holiday.calendar_id == calendar) lambda h: not h.calendar_id or h.calendar_id == calendar
)) )
return holidays
def _attendance_calendar(self, month_start, month_end, attendances, leaves, public_holidays, calendar_view="monthly"): def _attendance_calendar(self, month_start, month_end, attendances, leaves, public_holidays, calendar_view="monthly"):
if calendar_view == "yearly": if calendar_view == "yearly":
@ -229,14 +245,14 @@ class HrmsEmployeeDashboard(http.Controller):
if outside_period: if outside_period:
status = "empty" status = "empty"
show_metrics = False show_metrics = False
elif is_leave:
status = "leave"
label = _("Time Off")
show_metrics = is_half_day_leave
elif is_holiday: elif is_holiday:
status = "holiday" status = "holiday"
label = holiday_by_day[cursor] label = holiday_by_day[cursor]
show_metrics = has_worked_hours show_metrics = has_worked_hours
elif is_leave:
status = "leave"
label = _("Time Off")
show_metrics = is_half_day_leave
elif has_worked_hours: elif has_worked_hours:
status = "present" status = "present"
elif is_weekend: elif is_weekend:
@ -388,26 +404,48 @@ class HrmsEmployeeDashboard(http.Controller):
return result return result
def _attendance_summary(self, month_start, month_end, attendances, leaves, public_holidays): def _attendance_summary(self, month_start, month_end, attendances, leaves, public_holidays):
days = self._attendance_calendar(month_start, month_end, attendances, leaves, public_holidays) days = self._attendance_calendar(
month_start,
month_end,
attendances,
leaves,
public_holidays,
)
counts = defaultdict(int) counts = defaultdict(int)
expected_hours = 0.0
worked_hours = 0.0
break_hours = 0.0
leave_hours = 0.0
for day in days: for day in days:
if day["outside_period"]:
continue
counts[day["status"]] += 1 counts[day["status"]] += 1
expected_hours = sum(day["expected_hours"] for day in days if not day["outside_period"])
worked_hours = sum(attendances.mapped("worked_hours")) expected_hours += day["expected_hours"]
break_hours = sum(day["break_hours"] for day in days if not day["outside_period"]) worked_hours += day["worked_hours"]
leave_hours = sum(day["leave_hours"] for day in days if not day["outside_period"]) break_hours += day["break_hours"]
leave_hours += day["leave_hours"]
remaining_hours = expected_hours - worked_hours - leave_hours remaining_hours = expected_hours - worked_hours - leave_hours
return { return {
"present": counts["present"], "present": counts["present"],
"absent": counts["absent"], "absent": counts["absent"],
"leave": counts["leave"], "leave": counts["leave"],
"holiday": counts["holiday"], "holiday": counts["holiday"],
"hours": round(worked_hours, 2), "hours": round(worked_hours, 2),
"worked_hours": round(worked_hours, 2), "worked_hours": round(worked_hours, 2),
"break_hours": round(break_hours, 2), "break_hours": round(break_hours, 2),
"expected_hours": round(expected_hours, 2), "expected_hours": round(expected_hours, 2),
"leave_hours": round(leave_hours, 2), "leave_hours": round(leave_hours, 2),
"remaining_hours": round(remaining_hours, 2), "remaining_hours": round(remaining_hours, 2),
"worked_display": self._format_hours(worked_hours), "worked_display": self._format_hours(worked_hours),
"break_display": self._format_hours(break_hours), "break_display": self._format_hours(break_hours),
"expected_display": self._format_hours(expected_hours), "expected_display": self._format_hours(expected_hours),

View File

@ -473,6 +473,9 @@
} }
.hrms-leave-tile span { .hrms-leave-tile span {
display: inline-block;
min-width: 64px;
margin-right: 10px;
color: #64748b; color: #64748b;
font-size: 11px; font-size: 11px;
} }
@ -482,6 +485,91 @@
color: #0f172a; color: #0f172a;
font-size: 18px; font-size: 18px;
} }
.hrms-leave-graph {
display: grid;
gap: 12px;
}
.hrms-leave-graph-row {
display: grid;
grid-template-columns: minmax(140px, 190px) minmax(180px, 1fr);
gap: 10px 14px;
align-items: center;
}
.hrms-leave-graph-label strong {
display: block;
color: #0f172a;
font-size: 13px;
}
.hrms-leave-graph-label span {
color: #64748b;
font-size: 11px;
}
.hrms-leave-bar {
display: flex;
width: 100%;
height: 18px;
overflow: hidden;
border-radius: 8px;
background: #e5e7eb;
}
.hrms-leave-bar span {
min-width: 0;
}
.hrms-leave-bar-remaining {
background: #16a34a;
}
.hrms-leave-bar-taken {
background: #2563eb;
}
.hrms-leave-bar-planned {
background: #f59e0b;
}
.hrms-leave-legend {
grid-column: 2;
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
color: #64748b;
font-size: 11px;
}
.hrms-leave-legend span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.hrms-leave-legend i {
width: 9px;
height: 9px;
border-radius: 2px;
}
.hrms-leave-legend i.remaining {
background: #16a34a;
}
.hrms-leave-legend i.taken {
background: #2563eb;
}
.hrms-leave-legend i.planned {
background: #f59e0b;
}
.hrms-leave-legend b {
color: #0f172a;
}
.hrms-two-charts { .hrms-two-charts {
display: grid; display: grid;
grid-template-columns: 1.3fr 0.7fr; grid-template-columns: 1.3fr 0.7fr;
@ -553,10 +641,15 @@
.hrms-employee-grid, .hrms-employee-grid,
.hrms-grid, .hrms-grid,
.hrms-leave-graph-row,
.hrms-two-charts { .hrms-two-charts {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.hrms-leave-legend {
grid-column: 1;
}
.hrms-panel.wide { .hrms-panel.wide {
grid-column: span 1; grid-column: span 1;
} }

View File

@ -68,7 +68,6 @@ class HrmsEmployeeDashboard extends Component {
return; return;
} }
this.state.data = response; this.state.data = response;
// setTimeout(() => this.initCharts(), 100);
const expenses = response.expenses || {}; const expenses = response.expenses || {};
const total = const total =
(expenses.series || []).reduce( (expenses.series || []).reduce(
@ -76,11 +75,7 @@ class HrmsEmployeeDashboard extends Component {
0 0
); );
this.state.expenseCount = total; this.state.expenseCount = total;
if (this.state.expenseCount > 0) { setTimeout(() => this.initCharts(), 100);
setTimeout(() => this.initCharts(), 100);
} else {
this.destroyCharts();
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.state.error = "Unable to load employee dashboard."; this.state.error = "Unable to load employee dashboard.";
@ -134,17 +129,18 @@ class HrmsEmployeeDashboard extends Component {
} }
renderLeaveChart(leaves) { renderLeaveChart(leaves) {
const allocatedLeaves = leaves.filter((leave) => leave.requires_allocation === "yes");
this.chartBase("#hrmsLeaveChart", { this.chartBase("#hrmsLeaveChart", {
chart: { type: "bar" }, chart: { type: "bar" },
series: [ series: [
{ name: "Remaining", data: leaves.map((leave) => leave.remaining) }, { name: "Remaining", data: allocatedLeaves.map((leave) => Number(leave.remaining || 0)) },
{ name: "Taken", data: leaves.map((leave) => leave.taken) }, { name: "Taken", data: allocatedLeaves.map((leave) => Number(leave.taken || 0)) },
{ name: "Planned", data: leaves.map((leave) => leave.planned) }, { name: "Planned", data: allocatedLeaves.map((leave) => Number(leave.planned || 0)) },
], ],
colors: ["#16a34a", "#2563eb", "#f59e0b"], colors: ["#16a34a", "#2563eb", "#f59e0b"],
plotOptions: { bar: { borderRadius: 4, columnWidth: "50%" } }, plotOptions: { bar: { borderRadius: 4, columnWidth: "50%" } },
dataLabels: { enabled: false }, dataLabels: { enabled: false },
xaxis: { categories: leaves.map((leave) => leave.name), labels: { rotate: -25, trim: true } }, xaxis: { categories: allocatedLeaves.map((leave) => leave.name), labels: { rotate: -25, trim: true } },
yaxis: { min: 0, forceNiceScale: true }, yaxis: { min: 0, forceNiceScale: true },
}); });
} }

View File

@ -172,11 +172,32 @@
<strong t-esc="leave.name"/> <strong t-esc="leave.name"/>
<div> <div>
<span><b t-esc="leave.remaining"/> Balance</span> <span><b t-esc="leave.remaining"/> Balance</span>
<span><b t-esc="leave.taken"/> Taken</span>
<span><b t-esc="leave.planned"/> Confirmed</span>
</div>
</div>
</div>
</div>
<div class="hrms-leave-graph">
<div t-foreach="state.data.leave_balances" t-as="leave" t-key="leave.id + '-graph'">
<div t-if="leave.requires_allocation === 'yes'" class="hrms-leave-graph-row">
<div class="hrms-leave-graph-label">
<strong t-esc="leave.name"/>
<span><t t-esc="leave.allocated"/> allocated</span>
</div>
<div class="hrms-leave-bar" role="img" t-att-aria-label="leave.name + ': ' + leave.remaining + ' balance, ' + leave.taken + ' taken, ' + leave.planned + ' confirmed'">
<span class="hrms-leave-bar-remaining" t-att-style="'width: ' + leave.remaining_percent + '%'"/>
<span class="hrms-leave-bar-taken" t-att-style="'width: ' + leave.taken_percent + '%'"/>
<span class="hrms-leave-bar-planned" t-att-style="'width: ' + leave.planned_percent + '%'"/>
</div>
<div class="hrms-leave-legend">
<span><i class="remaining"/>Balance <b t-esc="leave.remaining"/></span>
<span><i class="taken"/>Taken <b t-esc="leave.taken"/></span>
<span><i class="planned"/>Confirmed <b t-esc="leave.planned"/></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="hrmsLeaveChart"/>
</section> </section>
<section class="hrms-panel"> <section class="hrms-panel">