Co-Authored-By: claude-flow <ruv@ruv.net>
23 KiB
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
- Admin sees each programmer's live state (working / paused / idle / away), current task, today's time, and help given/received.
- Each programmer's work is reviewed per day: approve, or disapprove with remarks; the programmer justifies (and may append corrections) until approved.
- Programmers generate their own Accomplishment Report (PDF, date-filtered); the admin generates one for any programmer.
- 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/reassignedevents). A later reassignment never changes a past day.adjustmentevents count against the day they were created. - Helper rows: the helper's entered minutes, labelled "Helped ".
- Assignee rows: timer intervals (started/resumed → paused/completed/cancelled)
clipped to Manila day boundaries, credited to whoever was assignee at that moment
(walk
- 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_dateonly if the author's sheet for that date ispendingordisapproved.
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
draftsheet when a work log is inserted (for the author, onwork_date) and when a lifecycle activity-log event is inserted (for the task's assignee, on the event's Manila date). Only forrole = 'programmer'. -
Amended after approval. A work log or lifecycle event landing on an approved sheet for today sets it back to
pendingand adds a systemamendedevent. 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:- For assignees of tasks still running, upsert yesterday's sheet (reopening it as
amendedif it was approved). - Flip every
draftsheet withwork_date < todaytopending(auto_submitted = true,auto_submittedevent). - If anything is pending, insert one
scheduled_notificationsrow per admin withnotify_type = 'day_sheet_digest', scheduled for 08:00 Manila.
The cron registration uses the same
DO $$ … EXCEPTIONguard as existing cron migrations. - For assignees of tasks still running, upsert yesterday's sheet (reopening it as
5.5 Approval snapshot
On approval the admin client sends the rows it displayed:
{
"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 (
AppStatusColorswarning + flag icon). - Thread timeline, styled like
widgets/activity_timeline.dart. - Programmer, when disapproved:
- Add correction — the existing work-log dialog with
workDatepreset to that day, flagged tasks listed first. - Justification field (cannot be empty; modeled on the checkout-justification dialog,
attendance_screen.dart:1462-1510). - Resubmit.
- Add correction — the existing work-log dialog with
- 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
TechChips, 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 allin_progresstask ids → latest lifecycle event per task → Working vs Paused. It also replaces the Mine tab'smyRunningProgrammerTaskIdProviderinference, 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 = daywhose author ≠ task assignee. - Leave / pass slip: extract the per-user "today" logic from
dashboard_screen.dart:371-433intolib/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.
-
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). -
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
disapprovedevent ÷ reviewed sheets. - Help given this month.
- Streak and approval rate are hidden for admins (admins have no sheets).
- Today's focus — ring against an 8h target (pattern:
-
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 -
Task sections (
AppSectionHeader+ count): In progress · Paused · Not started · Completed (collapsed; last 10 + Show all) · Cancelled (collapsed). The list's private_TaskCardis extracted towidgets/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'fromprofilesProvider. - 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):
- Info box — name, position, period (inclusive end date:
end − 1 day). - 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.
- Footnote (approved-only mode) — "N days in the period are not included (x pending, y disapproved)".
- Summary — days reported, total time, tasks completed in the period, help given, time by category.
- 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.sqllib/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.dartlib/utils/programmer_daily_time.dart(per-day split with assignee attribution)lib/utils/programmer_stats.dart(stats, streak, badges)lib/utils/staff_presence.dartlib/utils/pdf/pdf_letterhead.dart,lib/widgets/pdf_preview_dialog.dartlib/screens/programmer_tasks/dashboard/— dashboard, hero, stat tiles, badges, sectionslib/screens/programmer_tasks/monitor/— monitor tab, programmer card, help panellib/screens/programmer_tasks/day_sheets/— Day Sheets tab, Approvals tab, detail screen, disapprove/justify dialogslib/screens/programmer_tasks/report/— generate dialog, report data, report PDFlib/screens/programmer_tasks/widgets/programmer_task_card.dart
Modified:
lib/screens/programmer_tasks/programmer_tasks_list_screen.dart— role tabs, card extractlib/models/programmer_task_work_log.model.dart,lib/providers/programmer_task_work_logs_provider.dart,lib/screens/programmer_tasks/widgets/work_log_section.dart—workDatelib/screens/reports/report_date_filter.dart— controlled constructorlib/providers/profile_provider.dart—isStrictAdminProviderlib/routing/app_router.dart— day-sheet detail routelib/screens/notifications/notifications_screen.dart,lib/services/notification_bridge.dart,lib/models/notification_item.model.dartsupabase/functions/process_scheduled_notifications/index.ts— digest caselib/screens/dashboard/dashboard_screen.dart— usestaff_presence.dart
13. Delivery slices
Each slice ships independently on feat/programmer-tasks.
- 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.
- Admin Monitor — plus the
staff_presence.dartextraction. - 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). - Accomplishment Report — date-filter refactor, PDF helpers, report.
14. Testing and verification
flutter analyze libclean;flutter testpasses 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.claimsset 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_advisorssecurity 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 websucceeds.