diff --git a/docs/superpowers/specs/2026-09-27-programmer-oversight-design.md b/docs/superpowers/specs/2026-09-27-programmer-oversight-design.md new file mode 100644 index 00000000..f786c59f --- /dev/null +++ b/docs/superpowers/specs/2026-09-27-programmer-oversight-design.md @@ -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 ". +- **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 — ". + - 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.