feat(fusion_clock): redesign dashboard — layered, role-aware, gradient cards (dark+light)

- Rework /fusion_clock/dashboard_data into a personal block (everyone)
  plus a team block (team lead = direct reports, manager = org-wide).
  A regular employee's payload never contains another employee's data.
- New OWL stacked layout: gradient KPI cards (Today/Week/OT/Streak),
  Today's Shift, Recent Activity, Upcoming Leave, Recent Penalties; team
  band adds Present/Absent/Late/Pending, roster, and Needs Attention.
- Dark/light via compile-time $o-webclient-color-scheme branching;
  drop the old runtime html.o_dark dashboard block.
- Open the Dashboard menu to group_fusion_clock_user (lead/manager imply).
- Add HttpCase permission/no-leak tests. Bump 3.13.2 -> 3.14.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-31 02:28:53 -04:00
parent ea4f216c1a
commit fef99809e5
8 changed files with 681 additions and 296 deletions

View File

@@ -13,16 +13,11 @@ export class FusionClockDashboard extends Component {
this.action = useService("action");
this.state = useState({
loading: true,
clocked_in: [],
total_employees: 0,
present_count: 0,
absent_count: 0,
late_count: 0,
pending_reasons: 0,
pending_corrections: 0,
error: "",
role: "employee",
personal: {},
team: null,
});
onWillStart(async () => {
await this._fetchData();
});
@@ -30,12 +25,15 @@ export class FusionClockDashboard extends Component {
async _fetchData() {
this.state.loading = true;
this.state.error = "";
try {
const data = await rpc("/fusion_clock/dashboard_data", {});
if (data.error) {
this.state.error = data.error;
} else {
Object.assign(this.state, data);
this.state.role = data.role;
this.state.personal = data.personal;
this.state.team = data.team;
}
} catch (e) {
this.state.error = "Failed to load dashboard data.";
@@ -43,25 +41,47 @@ export class FusionClockDashboard extends Component {
this.state.loading = false;
}
async onRefresh() {
await this._fetchData();
// ---- display helpers ----
get greeting() {
const h = new Date().getHours();
if (h < 12) return "Good morning";
if (h < 17) return "Good afternoon";
return "Good evening";
}
get todayLabel() {
return new Date().toLocaleDateString(undefined, {
weekday: "long", month: "long", day: "numeric",
});
}
sourceLabel(source) {
return { schedule: "Posted schedule", shift: "Recurring shift", none: "—" }[source] || "—";
}
initials(name) {
return (name || "")
.split(" ").filter(Boolean).slice(0, 2)
.map((p) => p[0].toUpperCase()).join("");
}
fmtDate(s) {
if (!s) return "";
const d = new Date(s.replace(" ", "T") + "Z");
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}
fmtTime(s) {
if (!s) return "";
const d = new Date(s.replace(" ", "T") + "Z");
return d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
}
onViewAttendances() {
this.action.doAction("hr_attendance.hr_attendance_action");
}
onViewCorrections() {
this.action.doAction("fusion_clock.action_fusion_clock_correction");
}
onViewActivityLogs() {
this.action.doAction("fusion_clock.action_fusion_clock_activity_log");
}
onViewPenalties() {
this.action.doAction("fusion_clock.action_fusion_clock_penalty");
}
// ---- actions ----
onRefresh() { return this._fetchData(); }
onOpenClock() { this.action.doAction({ type: "ir.actions.act_url", url: "/my/clock", target: "self" }); }
onViewTimesheets() { this.action.doAction({ type: "ir.actions.act_url", url: "/my/clock/timesheets", target: "self" }); }
onViewAttendances() { this.action.doAction("hr_attendance.hr_attendance_action"); }
onViewCorrections() { this.action.doAction("fusion_clock.action_fusion_clock_correction"); }
onViewActivityLogs() { this.action.doAction("fusion_clock.action_fusion_clock_activity_log"); }
onViewPenalties() { this.action.doAction("fusion_clock.action_fusion_clock_penalty"); }
onViewShiftPlanner() { this.action.doAction("fusion_clock.action_fusion_clock_shift_planner"); }
onViewReports() { this.action.doAction("fusion_clock.action_fusion_clock_report"); }
}
registry.category("actions").add("fusion_clock.Dashboard", FusionClockDashboard);