# 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.