Files
Odoo-Modules/fusion_clock/docs/superpowers/specs/2026-05-31-dashboard-redesign-design.md

153 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 -- <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.