# 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 / Org` divider → 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 `_tokens` pattern. 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: ```python { "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` — rework `dashboard_data`; add `_dashboard_personal` + `_dashboard_team`; `get_status` refactored to reuse `_dashboard_personal` (no behavioural change to the portal). - `static/src/js/fusion_clock_dashboard.js` — state holds `role` / `personal` / `team`; conditional render; action handlers: `onOpenClock` (act_url `/my/clock`), `onRequestLeave`/`onRequestCorrection` (act_url to portal), `onViewTimesheets`, plus existing `onViewAttendances`/`onViewCorrections`/`onViewActivityLogs`/`onViewPenalties`, and manager-only `onViewReports`/`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 with `t-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_mode` dash block. - `views/clock_menus.xml` — Dashboard `menuitem` groups: `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 card `status_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** → `team` present, 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_name` is 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 `error` key, 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 -- ` (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.