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

12 KiB
Raw Blame History

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):

  • manageremp_ids = all employees where x_fclk_enable_clock = True.
  • team_leademp_ids = employees where parent_id == current user's employee (their direct reports). Their own personal band is computed from their own employee record.
  • employeeemp_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:

{
  "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_leadgroup_fusion_clock_user.
  • __manifest__.py — version bump (3.13.2 → 3.14.0) to rebuild asset bundles.
  • tests/test_dashboard.pynew, 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 reportsteam 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 payloadrole == '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 payloadrole == 'team_lead', team.scope == 'team', roster/counts include only the two direct reports, exclude the unrelated employee and the manager.
  • Manager payloadrole == '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.