Files
tasq/docs/superpowers/specs/2026-09-27-programmer-oversight-design.md

449 lines
23 KiB
Markdown
Raw Permalink 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.
# 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.