12 KiB
Fusion Clock — Dashboard Redesign (Layered, Role-Aware) Design
Date: 2026-05-31
Module: fusion_clock
Status: Approved (brainstorming) — ready for implementation plan
1. Problem
The current backend dashboard (fusion_clock.Dashboard client action) is manager/team-lead only and shows nothing but org/team aggregate counts. A regular employee who opens it gets Access denied. It is plain Bootstrap (4 flat summary cards + a roster table + an alerts column), uses a runtime .o_dark_mode selector for dark mode (against the repo's compile-time rule), and surfaces none of the per-person information an employee actually wants (their hours, shift, streak, leaves).
We want one modern dashboard that:
- Works for every role, showing only what that role is permitted to see.
- Leads with vibrant gradient KPI cards (Style A, chosen during brainstorming).
- Supports both light and dark mode correctly (compile-time, per repo rule).
- Puts "the most information at fingertips" without leaking other employees' data.
2. Permission Model (the core requirement)
Three existing groups, already in an implied chain (security/security.xml):
group_fusion_clock_user ← group_fusion_clock_team_lead ← group_fusion_clock_manager
The dashboard renders bands, gated by role. The hard rule: a regular employee's payload contains only their own data — the server never sends another employee's data to a non-lead/non-manager.
| Band | Employee | Team lead | Manager |
|---|---|---|---|
| Header (greeting, date, own clock status) | ✅ own | ✅ own | ✅ own |
| Personal KPIs — Today, This Week, OT (week), On-time Streak | ✅ own | ✅ own | ✅ own |
| Today's Shift (scheduled window, status, source) | ✅ own | ✅ own | ✅ own |
| My Recent Activity / My Leave & Penalties | ✅ own | ✅ own | ✅ own |
| — employee view ends here — | |||
| Team KPIs — Present / Absent / Late / Pending | ❌ | ✅ direct reports | ✅ org-wide |
| Currently Clocked In roster | ❌ | ✅ direct reports | ✅ everyone |
| Needs Attention (genuine absences, pending reasons, pending corrections) | ❌ | ✅ their team | ✅ org-wide |
| Quick Actions | own (clock/leave/correction/timesheets) | + team views | + Reports / Settings |
Scoping rule (server-side, never client-trusted):
manager→emp_ids = all employees where x_fclk_enable_clock = True.team_lead→emp_ids = employees where parent_id == current user's employee(their direct reports). Their own personal band is computed from their own employee record.employee→emp_ids = [own employee]; the team band is omitted entirely (team: null).
Approvals decision: team leads see their team's pending corrections/leaves (counts + an alert row that links to a filtered list) but the approve action stays manager-gated by the existing ACL/record rules. Managers see org-wide and can approve. The dashboard adds no new approval capability; it only surfaces and links.
3. Look & Feel (decided in brainstorming)
- Card style A — Vibrant full-gradient: each KPI is its own bold
linear-gradient(135deg, …)card with white text and a translucent icon chip. Same gradients in light and dark (white-on-gradient reads in both). - Layout A — Stacked sections: single column, top-to-bottom: Header → Personal KPI row → Personal detail (2 cards) →
Team / Orgdivider → Team KPI row → roster + Needs Attention (2 cards) → Quick Actions. Degrades gracefully: a regular employee simply has nothing rendered below the divider. - Responsive: KPI rows are a CSS grid that collapses 4→2→1 columns; the two-up detail rows collapse to one column on narrow screens. Mobile/tablet-first since this is the same view everyone opens.
Dark / light (compile-time, per repo rule)
Branch on $o-webclient-color-scheme at SCSS compile time — no .o_dark_mode / [data-bs-theme] / prefers-color-scheme. The existing runtime .o_dark_mode block for .fclk-dash-card is removed.
- Gradient KPI cards: identical hex in both bundles (white text).
- Page background, section cards, borders, body/heading text, muted text: light vs dark hex chosen via
@if $o-webclient-color-scheme == dark { … !global }, exposed through CSS custom properties (e.g.--fclk-dash-page,--fclk-dash-card,--fclk-dash-border,--fclk-dash-text,--fclk-dash-muted) following the repo_tokenspattern. Three-layer contrast: page (grayest) → section card → KPI card (brightest).
4. Data Contract
Single endpoint, reworked: POST /fusion_clock/dashboard_data (type='jsonrpc', auth='user'). Gate changes from manager/lead-only to any group_fusion_clock_user. Response:
{
"role": "employee" | "team_lead" | "manager",
"personal": {
"employee_name": str,
"enable_clock": bool,
"is_checked_in": bool,
"check_in": str | False, # ISO, when checked in
"location_name": str,
"pending_reason": bool, # owes an auto-clock-out explanation
"today_hours": float, # sum x_fclk_net_hours today
"week_hours": float, # sum x_fclk_net_hours this week
"overtime_week": float, # employee.x_fclk_overtime_this_week
"ontime_streak": int, # employee.x_fclk_ontime_streak
"shift": { # from employee._get_fclk_day_plan(local_today)
"label": str, # "7:00 AM – 3:30 PM" or ""
"hours": float,
"source": "schedule"|"shift"|"none",
"scheduled_off": bool,
"status_note": str # "On time", "Late", "Not scheduled today", "Clock disabled"
},
"recent_activity": [ # last ~6 closed attendances
{"check_in": str, "check_out": str, "worked_hours": float,
"overtime_hours": float, "location": str}
],
"leaves": [ # own, leave_date >= today, soonest first, ~5
{"label": str, "state": str} # label via _fclk_date_label()
],
"penalties": [ # own, current month, recent first, ~5
{"type": str, "date": str, "minutes": float}
]
},
"team": null | { # present ONLY for team_lead / manager
"scope": "team" | "org",
"total_employees": int,
"present_count": int, # distinct employees with an attendance today
"on_leave_count": int, # approved leave covering today (leave_date <= today <= date_to)
"absent_count": int, # genuine no-shows = total - present - on_leave (matches absence cron)
"late_count": int, # late_clock_in logs today, scoped
"pending_reasons": int, # scoped (owe an auto-clock-out explanation)
"pending_approvals": int, # scoped: fusion.clock.correction state='pending'
# (leaves are auto-approved — nothing to approve)
"clocked_in": [
{"employee": str, "check_in": str, "location": str, "late": bool}
]
}
}
Implementation note: factor two private helpers on the controller — _dashboard_personal(employee) (builds the personal block above; reuses the same per-employee computations the existing get_status already performs for today_hours / week_hours / streak / shift / recent_activity) and _dashboard_team(emp_ids, scope) (extracted from the existing dashboard_data aggregate logic). get_status keeps its current public response keys unchanged (the portal /my/clock consumes them) — share computation via a small internal helper if convenient, but do not alter get_status's output contract. The public endpoint resolves role → builds personal always → builds team only for lead/manager. Team/org reads use sudo() but are constrained to the server-computed emp_ids; personal reads use the caller's own employee. No client input selects scope.
5. Files Touched
controllers/clock_api.py— reworkdashboard_data; add_dashboard_personal+_dashboard_team;get_statusrefactored to reuse_dashboard_personal(no behavioural change to the portal).static/src/js/fusion_clock_dashboard.js— state holdsrole/personal/team; conditional render; action handlers:onOpenClock(act_url/my/clock),onRequestLeave/onRequestCorrection(act_url to portal),onViewTimesheets, plus existingonViewAttendances/onViewCorrections/onViewActivityLogs/onViewPenalties, and manager-onlyonViewReports/onViewShiftPlanner. Header is status display + "Open My Clock" button — clocking itself stays in the existing systray widget / portal (we do not re-implement the clock flow here).static/src/xml/fusion_clock_dashboard.xml— full rewrite to the stacked layout witht-if="state.team"gating the team band.static/src/scss/fusion_clock.scss— replace the.fclk-dash-card*block with gradient KPI cards + stacked layout + section-card tokens; add compile-time dark branching; delete the runtime.o_dark_modedash block.views/clock_menus.xml— Dashboardmenuitemgroups:group_fusion_clock_manager,group_fusion_clock_team_lead→group_fusion_clock_user.__manifest__.py— version bump (3.13.2 → 3.14.0) to rebuild asset bundles.tests/test_dashboard.py— new, permission-focused.
6. Error Handling & Edge Cases
- No employee record for the user →
{"error": "No employee profile is linked to your account."}; client shows a friendly empty state (not a raw error). x_fclk_enable_clock = False→ dashboard still renders; shift cardstatus_note = "Clock disabled", KPIs show 0/own values; no team band unless lead/manager.- Not scheduled / day off today → shift card shows "Not scheduled today" (ties into the already-shipped schedule-driven resolver
_get_fclk_day_plan). This is also why we never nag — consistent with the schedule-driven attendance work. - Team lead with no direct reports →
teampresent, roster empty, counts 0, friendly "No direct reports yet." - Manager with employees but none clocked in → roster empty state "No one is clocked in right now."
7. Testing
tests/test_dashboard.py, tagged @tagged('-at_install','post_install','fusion_clock'). Create a manager, a team lead, two direct reports of that lead, and one unrelated employee; give each enabled clock + an attendance.
- Employee payload →
role == 'employee',team is None,personal.employee_nameis their own, and the payload contains no other employee's name (assert the unrelated employee's name is absent anywhere in the JSON). - Team lead payload →
role == 'team_lead',team.scope == 'team', roster/counts include only the two direct reports, exclude the unrelated employee and the manager. - Manager payload →
role == 'manager',team.scope == 'org', counts cover all enabled employees. - Personal stats → today_hours / week_hours / streak / shift label reflect the caller's own records.
- No-employee user → returns the
errorkey, not a traceback.
Run: docker exec odoo-modsdev-app odoo -d fusion-dev --test-enable --test-tags /fusion_clock -u fusion_clock --stop-after-init --http-port=0 --gevent-port=0.
8. Out of Scope (YAGNI)
Charts/trend graphs, date-range pickers, CSV export from the dashboard, websocket/live auto-refresh (the manual Refresh button stays), user-configurable card order/favourites, and any new approval workflow (leads still can't approve from here). These can be added later if asked.
9. Deployment
Standard entech path after local test: bump version (done in §5), git commit --only -- <explicit dashboard paths> (shared working tree), push to both origin and gitea, then upgrade entech (pct exec 111 native odoo.service, DB admin, --http-port=0 --gevent-port=0). Asset bundle rebuilds on version bump; hard-refresh / clear iOS website data to bust cache.