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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user