docs(fusion_clock): dashboard redesign spec (layered, role-aware, gradient cards, dark+light)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 -- <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.
|
||||
Reference in New Issue
Block a user