Add design spec: programmer oversight (day-sheet approvals, monitor, report, dashboard)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-27 17:44:44 +08:00
parent b307f66f2c
commit 7a319b6e16
@@ -0,0 +1,448 @@
# Programmer Oversight — Design Spec
**Date:** 2026-09-27 · **Branch:** `feat/programmer-tasks` · **Status:** Draft for review
Adds an oversight layer to the Programmer Tasks module: daily **Day Sheet** approvals with a
remarks ↔ justification loop, a live **admin Monitor**, a PDF **Accomplishment Report**,
and a gamified **Mine dashboard**.
## 1. Background
The Programmer Tasks module records programmers' dev work: tasks with a pause-aware timer,
per-contributor work logs (assignee progress notes + helper-entered minutes), comments, and
projects. Today:
- The admin cannot see who is working on what right now, or who is helping whom.
- Nobody reviews or approves programmers' daily work.
- There is no Accomplishment Report.
- The "Mine" tab is a flat list of cards.
## 2. Goals
1. Admin sees each programmer's live state (working / paused / idle / away), current task,
today's time, and help given/received.
2. Each programmer's work is reviewed per day: approve, or disapprove with remarks; the
programmer justifies (and may append corrections) until approved.
3. Programmers generate their own Accomplishment Report (PDF, date-filtered); the admin
generates one for any programmer.
4. The Mine tab becomes a motivating dashboard with stats, a streak, badges, and tasks
separated into In progress / Paused / Not started / Completed / Cancelled.
### Non-goals
- Live "help sessions" (help is derived from work logs after the fact).
- XP/levels and team leaderboards.
- Editing or deleting existing work-log entries (they stay append-only).
- Migrating the existing DTR / task PDFs to the new shared PDF helpers.
## 3. Decisions
| Topic | Decision |
|---|---|
| Approval unit | **Day Sheet** — one per programmer per day; admin can **flag task rows** in it |
| Submission | **Auto-submit at day end**, plus **manual early submit** |
| Same-day approval | **Allowed**; later work that day reopens the sheet as "amended after approval" |
| On disapprove | Programmer **justifies** and **may append corrective entries**; originals are immutable |
| Late entries | Back-dated work logs allowed on **pending or disapproved** days, never on approved days; no backfilling days that have no sheet |
| Helping | **Derived from work logs** (helper = work-log author ≠ task assignee) |
| Report scope | **Toggle when generating**: approved days only (default) or include unapproved days (marked) |
| Report layout | **By day**: Date · Task/Project · Accomplishment · Time, then summary and signatures |
| Gamification | **Stat tiles + streak + badges** |
| Notifications | **In-app + push** |
| Monitor states | Working / Paused / Idle / **On leave** / **Pass slip** |
| Tabs | Programmer: **Mine · All · Day Sheets** — Admin: **Monitor · Approvals · Mine · All** |
| Core approach | Day-sheet table + append-only review thread + SECURITY DEFINER RPCs + pg_cron auto-submit + approval snapshot |
**"Admin" means `profiles.role = 'admin'` only.** The existing `isAdminProvider`
(`lib/providers/profile_provider.dart:305`) also returns true for programmers, and the
module's RLS treats both roles alike. All approval and monitor gating uses a new
`isStrictAdminProvider` on the client and `role = 'admin'` in SQL.
## 4. Existing code this builds on
| Fact | Where |
|---|---|
| Task status is `queued \| in_progress \| completed \| cancelled`. **Paused** = `in_progress` whose latest lifecycle activity-log event is `paused`. **Not started** = `queued`. | `lib/models/programmer_task.model.dart:39-47` |
| Work logs are append-only; the day is known only from `created_at`; assignee rows have `minutes = null`, helper rows have minutes. | `supabase/migrations/20260927140000_add_programmer_projects_worklogs_comments.sql:90-111` |
| Duration core `computeEffectiveDuration` | `lib/utils/task_duration.dart:38-75` |
| Ledger `computeTaskLedger` credits **all** clock time to the *current* assignee — the per-day split must not do this. | `lib/utils/programmer_task_ledger.dart:46-78` |
| `ReportDateFilter` is hard-wired to `reportDateRangeProvider`; `ReportDateRange.end` is exclusive. | `lib/screens/reports/report_date_filter.dart`, `lib/providers/reports_provider.dart:13-51` |
| Per-person PDF with letterhead + preview dialog (period label is off by one day at :211). | `lib/screens/attendance/dtr_pdf.dart` |
| DTO + pure PDF builder pattern | `lib/screens/attendance/work_log_pdf.dart` |
| Notification `type` is free text; render + tap switch; push via `send_fcm`. | `lib/screens/notifications/notifications_screen.dart:63-140`, `lib/services/notification_bridge.dart:97,152`, `lib/providers/notifications_provider.dart:376` |
| Scheduled push: pg_cron → `scheduled_notifications` (`schedule_id` nullable) → edge function `notify_type` switch. | `supabase/functions/process_scheduled_notifications/index.ts` |
| SECURITY DEFINER RPC that writes status + notifications atomically | `respond_shift_swap`, `supabase/migrations/20260322190000_pm_oncall_companion_swap.sql` |
| Per-user "on leave / pass slip today" logic | `lib/screens/dashboard/dashboard_screen.dart:371-433` |
## 5. Day Sheets
### 5.1 What a Day Sheet contains
One **row per task** the programmer worked on that day:
- task title, category, project;
- **time that day**:
- *Assignee rows*: timer intervals (started/resumed → paused/completed/cancelled)
clipped to Manila day boundaries, credited to **whoever was assignee at that moment**
(walk `created` / `assigned` / `reassigned` events). A later reassignment never changes
a past day. `adjustment` events count against the day they were created.
- *Helper rows*: the helper's entered minutes, labelled "Helped <assignee>".
- **notes**: that day's work-log entries for the task.
The admin flags **rows** when disapproving.
### 5.2 Schema
New migration `supabase/migrations/20260927150000_add_programmer_day_sheets.sql`.
**`programmer_day_sheets`**
| Column | Type | Notes |
|---|---|---|
| `id` | uuid pk | |
| `programmer_id` | uuid → profiles | |
| `work_date` | date | Manila calendar day |
| `status` | text | check `draft \| pending \| disapproved \| approved`, default `draft` |
| `submitted_at` | timestamptz | |
| `auto_submitted` | bool | default false |
| `reviewed_by` | uuid → profiles | last admin to approve/disapprove |
| `reviewed_at` | timestamptz | |
| `resubmissions` | int | default 0; incremented by justify |
| `approved_snapshot` | jsonb | rows as approved (see 5.5) |
| `created_at`, `updated_at` | timestamptz | |
Unique `(programmer_id, work_date)`.
**`programmer_day_sheet_events`** — append-only review thread
| Column | Type | Notes |
|---|---|---|
| `id` | uuid pk | |
| `sheet_id` | uuid → sheets, on delete cascade | |
| `actor_id` | uuid null | null = system |
| `kind` | text | check `submitted \| auto_submitted \| disapproved \| justified \| approved \| amended` |
| `body` | text | remarks / justification; check non-empty for `disapproved` and `justified` |
| `flagged_task_ids` | uuid[] | default `'{}'` |
| `created_at` | timestamptz | |
**`programmer_task_work_logs.work_date`** — `date not null default (now() at time zone
'Asia/Manila')::date`. Existing rows are backfilled from `created_at`. A BEFORE INSERT
guard trigger allows:
- `work_date` = today (Manila); or
- a past `work_date` only if the author's sheet for that date is `pending` or
`disapproved`.
Anything else raises an error.
**`notifications.day_sheet_id`** — `uuid null references programmer_day_sheets on delete
cascade`.
**RLS:** sheets and events are readable by the owner and by `role = 'admin'`. There are no
client INSERT/UPDATE/DELETE policies; every write goes through the RPCs or triggers below.
Both tables are added to the `supabase_realtime` publication.
### 5.3 Status transitions
Each transition is a SECURITY DEFINER RPC with `set search_path = public`, explicit
`auth.uid()`, role, and current-status checks. Each one atomically updates the sheet,
inserts a thread event, and inserts notification rows, then returns the recipient user ids
so the client can call `send_fcm`. A stale status raises `already_reviewed`.
| RPC | Who | From → To | Rule |
|---|---|---|---|
| `submit_day_sheet(p_date)` | owner | draft → pending | Any time, including early. Upserts the row. |
| `disapprove_day_sheet(p_sheet, p_remarks, p_flagged_task_ids)` | admin | pending → disapproved | Remarks required. |
| `justify_day_sheet(p_sheet, p_body)` | owner | disapproved → pending | Justification required; `resubmissions += 1`. |
| `approve_day_sheets(p_items)` — `[{sheet_id, snapshot}]` | admin | pending or disapproved → approved | Same-day allowed. Bulk-capable. Stores the snapshot. |
```
draft ──submit / auto──▶ pending ──approve──▶ approved
│ ▲ │
disapprove justify │ new work on the same day
▼ │ │ (system "amended")
disapproved ◀────────────┘ (back to pending)
```
### 5.4 Automatic behaviour
- **Draft creation.** Triggers upsert a `draft` sheet when a work log is inserted (for the
author, on `work_date`) and when a lifecycle activity-log event is inserted (for the
task's assignee, on the event's Manila date). Only for `role = 'programmer'`.
- **Amended after approval.** A work log or lifecycle event landing on an **approved
sheet for today** sets it back to `pending` and adds a system `amended` event. This is not
counted as a disapproval. The Approve button warns the admin when that programmer has a
timer running.
- **Nightly auto-submit** — `auto_submit_day_sheets()` via pg_cron at 00:05 Manila:
1. For assignees of tasks still running, upsert yesterday's sheet (reopening it as
`amended` if it was approved).
2. Flip every `draft` sheet with `work_date < today` to `pending`
(`auto_submitted = true`, `auto_submitted` event).
3. If anything is pending, insert one `scheduled_notifications` row per admin with
`notify_type = 'day_sheet_digest'`, scheduled for 08:00 Manila.
The cron registration uses the same `DO $$ … EXCEPTION` guard as existing cron
migrations.
### 5.5 Approval snapshot
On approval the admin client sends the rows it displayed:
```json
{
"v": 1,
"total_seconds": 27900,
"rows": [
{
"task_id": "…", "title": "…", "category": "Bug Fix", "project_name": "HIS",
"kind": "assignee", "helped_name": null,
"seconds": 11400, "notes": ["Fixed session timeout…"]
}
]
}
```
Approved days are thereby fixed records: the report and stats read the snapshot, so later
renames or reassignments do not change what was approved.
## 6. Approval UX
### 6.1 Tabs
`programmer_tasks_list_screen.dart` shows tabs by role:
- **Programmer:** Mine · All · Day Sheets
- **Admin:** Monitor · Approvals · Mine · All
### 6.2 Day Sheets tab (programmer)
- **Today card** — today's rows (live), total time, Draft/Submitted chip, **Submit day**.
- **Needs your attention** — disapproved sheets, pinned, warning style.
- **History** — sheets by date with status filter chips (`AppStatusSummaryRow`).
- App bar: **Accomplishment Report**.
### 6.3 Sheet detail — `/programmer-tasks/day-sheets/:id` (both roles)
- Header: date, programmer, status, total time, "Round N" when resubmitted.
- Rows table: task · time · notes. Flagged rows are highlighted (`AppStatusColors`
warning + flag icon).
- Thread timeline, styled like `widgets/activity_timeline.dart`.
- **Programmer, when disapproved:**
- **Add correction** — the existing work-log dialog with `workDate` preset to that day,
flagged tasks listed first.
- Justification field (cannot be empty; modeled on the checkout-justification dialog,
`attendance_screen.dart:1462-1510`).
- **Resubmit**.
- **Admin, when pending or disapproved:**
- Row checkboxes for flagging.
- **Disapprove** — dialog lists the flagged rows; remarks required.
- **Approve** — warns if a timer is running. The admin client builds the snapshot.
### 6.4 Approvals tab (admin)
- Summary chips: Pending · Awaiting justification · Approved.
- Programmer filter and the shared date filter.
- Cards grouped by date: programmer, total time, task count, Early/Auto badge, Round N.
- Multi-select → **Approve selected**. Disapproval stays per sheet because it needs remarks.
## 7. Admin Monitor
- **Summary row:** Working · Paused · Idle · On leave/Pass slip · Sheets pending.
- **One card per programmer** (grid: 1 column on mobile, 2–3 on desktop):
- State pill: **Working** (success) / **Paused** (warning) / **Idle** (neutral) /
**On leave** or **Pass slip** (info; overrides the others).
- Current task: title, category/project `TechChip`s, live "running 1h 12m today".
- Today's total; in-progress / paused / not-started counts; today's sheet status.
- Help chips: "Helped → Ben · task · 45m", "Helped by ← Ana · 30m".
- **Drill-down** (tap a card): `ProgrammerDashboard(userId, readOnly: true)`, their recent
Day Sheets, and **Generate Accomplishment Report** with the programmer preselected.
- **Who's helping whom** panel: helper → assignee · task · minutes · time, with a day
stepper (defaults to today). The working/paused state is always live.
**Data**
- New `programmerRunStatesProvider`: realtime stream of lifecycle activity logs for all
`in_progress` task ids → latest lifecycle event per task → Working vs Paused. It also
replaces the Mine tab's `myRunningProgrammerTaskIdProvider` inference, which currently
marks every in-progress task as paused when offline.
- Today's time: the per-day split (5.1).
- Help: work logs with `work_date = day` whose author ≠ task assignee.
- Leave / pass slip: extract the per-user "today" logic from
`dashboard_screen.dart:371-433` into `lib/utils/staff_presence.dart`, used by both the
dashboard and the Monitor.
- Admin-only. Programmers keep the existing All tab and get no live view of teammates.
## 8. Mine dashboard
A single widget, `ProgrammerDashboard(userId, {readOnly})`, used for the Mine tab and for
the Monitor drill-down.
1. **Now working hero** — the running task with a live timer (today + total) and
Pause/Complete. If nothing is running: "Nothing running" and **Start** on the top
not-started task (via `startProgrammerTaskWithPausePrompt`).
2. **Stat tiles** (`AppMetricCard`):
- **Today's focus** — ring against an 8h target (pattern:
`reports/widgets/conversion_rate_card.dart:43-55`).
- **Completed this week**, with change vs last week.
- **Streak** — consecutive weekdays whose sheet is pending or approved. Weekends and
approved-leave days are skipped (neither count nor break). A disapproved day breaks
the streak until justified. Today counts once submitted. Shows best streak too.
- **First-pass approval rate** (last 30 days) — approved sheets with no `disapproved`
event ÷ reviewed sheets.
- **Help given** this month.
- Streak and approval rate are hidden for admins (admins have no sheets).
3. **Badges** — computed from data each time (no table). Locked badges are greyed out with
progress (e.g. 7/10).
| Badge | Unlocks at |
|---|---|
| First Finish | first completed task |
| Finisher | 10 / 50 / 100 completed tasks |
| Bug Squasher | 10 completed Bug Fix tasks |
| Deep Work | 100 / 500 approved hours |
| On a Roll | best streak 5 / 10 / 20 |
| Team Player | 10h help given |
| Clean Week | a Mon–Fri week with every day approved first time |
4. **Task sections** (`AppSectionHeader` + count): In progress · Paused · Not started ·
Completed (collapsed; last 10 + Show all) · Cancelled (collapsed). The list's private
`_TaskCard` is extracted to `widgets/programmer_task_card.dart`.
**Data:** hour-based stats and badges sum approved snapshots; "today" figures come from the
live per-day split; counts come from `programmerTasksProvider` filtered to the user.
Calculations live in pure functions (`computeProgrammerStats`, `computeBadges`).
## 9. Accomplishment Report (PDF)
**Entry points:** Day Sheets tab app bar (programmer, self); Approvals tab app bar and
Monitor drill-down (admin).
**Generate dialog**
- Programmer picker — admin only; searchable single-select of `role = 'programmer'`
from `profilesProvider`.
- Date filter — `ReportDateFilter.controlled`, default "This Month".
- Toggle — "Include unapproved days (marked)", off by default.
- **Generate** → preview dialog with Print and Download.
**Date filter reuse:** add `ReportDateFilter.controlled({value, onChanged})` to
`lib/screens/reports/report_date_filter.dart`. `_DateFilterDialog` returns the chosen
`ReportDateRange` instead of writing the provider. The default constructor keeps binding
`reportDateRangeProvider`, so the Reports screen is unchanged.
**Layout** (A4 MultiPage, Roboto, CRMC letterhead):
1. Info box — name, position, period (**inclusive** end date: `end − 1 day`).
2. By-day table — Date | Task/Project | Accomplishment | Time.
- Accomplishment = that day's work-log notes (Quill delta → plain text); "(no notes)"
when a row has time but no notes.
- Help rows read "Helped Ana — <task>".
- When unapproved days are included, they carry a Pending/Disapproved tag.
3. Footnote (approved-only mode) — "N days in the period are not included (x pending,
y disapproved)".
4. Summary — days reported, total time, tasks completed in the period, help given, time by
category.
5. Signatures — "Prepared by" the programmer; "Approved by" the reviewing admin's name if
one admin approved every included day, otherwise a blank line.
**Data:** approved days come from `approved_snapshot`; unapproved days (when included) are
computed live. `buildAccomplishmentReportData(...)` (pure) produces a DTO;
`buildAccomplishmentReportPdf(dto)` only draws.
**Shared helpers (used by new code only):** `lib/utils/pdf/pdf_letterhead.dart` (fonts,
logo, letterhead) and `lib/widgets/pdf_preview_dialog.dart` (pdfrx preview,
`Printing.layoutPdf`, `Printing.sharePdf`, modeled on `_DtrPdfDialog`).
## 10. Notifications
| Type | Recipient | Trigger |
|---|---|---|
| `day_sheet_submitted` | all admins | programmer submits early |
| `day_sheet_digest` | each admin, 08:00 | nightly auto-submit left sheets pending; opens Approvals |
| `day_sheet_disapproved` | programmer | admin disapproves (includes remarks) |
| `day_sheet_justified` | the reviewing admin | programmer justifies and resubmits |
| `day_sheet_approved` | programmer | admin approves (single or bulk) |
RPCs insert the notification rows and return recipients; the client sends push through
`NotificationsController.sendPush`. The digest goes through `scheduled_notifications` with
a new case in `process_scheduled_notifications`. New render cases and tap routing go into
`notifications_screen.dart` and `notification_bridge.dart`, and `day_sheet_id` is added to
`notification_item.model.dart`.
## 11. Error handling
- **Stale status** (e.g. two admins act at once) → "This sheet was already reviewed" and
the view refreshes.
- **Empty remarks/justification** → blocked in the dialog and by the DB check.
- **Disallowed back-dated entry** → the guard trigger's error is shown as a clear message.
- **Push failure** → non-blocking; logged, as in existing flows.
- **Offline** → Day Sheets are online-only (like work logs); actions are disabled with a
banner.
## 12. Files
New (each under 500 lines):
- `supabase/migrations/20260927150000_add_programmer_day_sheets.sql`
- `lib/models/programmer_day_sheet.model.dart`,
`lib/models/programmer_day_sheet_event.model.dart` (plain online models)
- `lib/providers/programmer_day_sheets_provider.dart` (streams, RPC controller, push)
- `lib/providers/programmer_run_states_provider.dart`
- `lib/utils/programmer_daily_time.dart` (per-day split with assignee attribution)
- `lib/utils/programmer_stats.dart` (stats, streak, badges)
- `lib/utils/staff_presence.dart`
- `lib/utils/pdf/pdf_letterhead.dart`, `lib/widgets/pdf_preview_dialog.dart`
- `lib/screens/programmer_tasks/dashboard/` — dashboard, hero, stat tiles, badges, sections
- `lib/screens/programmer_tasks/monitor/` — monitor tab, programmer card, help panel
- `lib/screens/programmer_tasks/day_sheets/` — Day Sheets tab, Approvals tab, detail
screen, disapprove/justify dialogs
- `lib/screens/programmer_tasks/report/` — generate dialog, report data, report PDF
- `lib/screens/programmer_tasks/widgets/programmer_task_card.dart`
Modified:
- `lib/screens/programmer_tasks/programmer_tasks_list_screen.dart` — role tabs, card extract
- `lib/models/programmer_task_work_log.model.dart`,
`lib/providers/programmer_task_work_logs_provider.dart`,
`lib/screens/programmer_tasks/widgets/work_log_section.dart` — `workDate`
- `lib/screens/reports/report_date_filter.dart` — controlled constructor
- `lib/providers/profile_provider.dart` — `isStrictAdminProvider`
- `lib/routing/app_router.dart` — day-sheet detail route
- `lib/screens/notifications/notifications_screen.dart`,
`lib/services/notification_bridge.dart`, `lib/models/notification_item.model.dart`
- `supabase/functions/process_scheduled_notifications/index.ts` — digest case
- `lib/screens/dashboard/dashboard_screen.dart` — use `staff_presence.dart`
## 13. Delivery slices
Each slice ships independently on `feat/programmer-tasks`.
1. **Mine dashboard** — run-state provider, per-day split util, dashboard. No schema
change; fixes the offline "everything paused" bug. Approval-based stats appear once
slice 3 lands.
2. **Admin Monitor** — plus the `staff_presence.dart` extraction.
3. **Day Sheets** — migration, RPCs, cron, Day Sheets/Approvals tabs, detail screen,
notifications. **The user applies the migration and redeploys
`process_scheduled_notifications`** (the Supabase migration tool is blocked in this
environment).
4. **Accomplishment Report** — date-filter refactor, PDF helpers, report.
## 14. Testing and verification
- `flutter analyze lib` clean; `flutter test` passes apart from the three known,
unrelated failures (theme_overhaul, profile_screen, user_management).
- **Unit (pure Dart, TDD):** per-day split (midnight crossing, reassignment attribution,
pauses, adjustments); stats and streak (weekends, leave, disapproved days); badges;
report data (approved-only vs marked, snapshot vs live, footnote, inclusive period).
- **Controller (mock-first, pattern of `test/programmer_tasks_controller_test.dart`):**
RPC calls, push invocation, stale-status error mapping.
- **Widget:** Disapprove dialog requires remarks; dashboard sections; tabs by role.
- **SQL checklist** after the migration is applied (rolled-back transactions with
`request.jwt.claims` set per role): every transition allowed/denied by role and status;
back-dated work-log guard; amended-after-approval; `auto_submit_day_sheets()` called
directly; `get_advisors` security check.
- **End-to-end** via the QA harness (programmer, then admin; light and dark): log work →
submit early → admin flags and disapproves → programmer adds a correction and justifies
→ admin approves → both generate the PDF (approved-only and marked) → Monitor shows
Working/Paused/On leave and help edges → notifications render and route.
- `flutter build web` succeeds.