From be721f82ae891374c328c6e14438431a66335189 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 31 May 2026 02:07:34 -0400 Subject: [PATCH] docs(fusion_clock): dashboard redesign spec (layered, role-aware, gradient cards, dark+light) Co-Authored-By: Claude Opus 4.8 --- .../2026-05-31-dashboard-redesign-design.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 fusion_clock/docs/superpowers/specs/2026-05-31-dashboard-redesign-design.md diff --git a/fusion_clock/docs/superpowers/specs/2026-05-31-dashboard-redesign-design.md b/fusion_clock/docs/superpowers/specs/2026-05-31-dashboard-redesign-design.md new file mode 100644 index 00000000..98e50047 --- /dev/null +++ b/fusion_clock/docs/superpowers/specs/2026-05-31-dashboard-redesign-design.md @@ -0,0 +1,151 @@ +# 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 (absences, pending reasons, requests, very-late) | ❌ | ✅ 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, + "absent_count": int, + "late_count": int, # late_clock_in logs today, scoped + "pending_reasons": int, # scoped + "pending_approvals": int, # scoped: pending corrections + pending leaves + "very_late_count": int, # scoped, today + "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.