diff --git a/docs/superpowers/plans/2026-09-11-project-month-week.md b/docs/superpowers/plans/2026-09-11-project-month-week.md new file mode 100644 index 0000000..75a0db3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-11-project-month-week.md @@ -0,0 +1,1769 @@ +# Project month/week filter — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let every site define its own "work month" (e.g. the 21st of one calendar month through the 20th of the next, not the 1st–30th), then tag every row on Timesheets, Delivery Notes, Purchase Orders, and Fuel Logs with which project Month/Week it falls in, and add a Today / This week / This month / All time filter to each of those four pages. + +**Architecture:** One new PocketBase field (`sites.work_month_start_day`) plus the existing `sites.job_start_date` are the only inputs. One new pure, fully-tested module (`src/lib/project-calendar.ts`) does all the date-bucketing math and is the single place the "which work-month cycle does this date fall in" logic lives. Each of the four sheet pages calls it once per row in its existing server-side loader (no new data fetching — every loader already fetches full `sites` records) and renders the result via one shared badge component. The filter itself reuses each page's own existing local `SelectFilter` (or raw ` setField("work_month_start_day", e.target.value)} + placeholder="1" + /> + +``` + +- [ ] **Step 5: Wire the form state** + +```typescript +interface FormState { + project_code: string; + name: string; + duration: string; + job_start_date: string; + work_month_start_day: string; + contract_value: string; + work_hours: string; + hours_per_week: string; + wages_hours_per_day: string; + latitude: string; + longitude: string; +} + +function initialValues(initial?: SiteFormInitial): FormState { + return { + project_code: initial?.project_code ?? "", + name: initial?.name ?? "", + duration: initial?.duration ?? "", + job_start_date: initial?.job_start_date ?? "", + work_month_start_day: + initial?.work_month_start_day != null ? String(initial.work_month_start_day) : "", + contract_value: initial?.contract_value != null ? String(initial.contract_value) : "", + work_hours: initial?.work_hours != null ? String(initial.work_hours) : "", + hours_per_week: initial?.hours_per_week != null ? String(initial.hours_per_week) : "", + wages_hours_per_day: + initial?.wages_hours_per_day != null ? String(initial.wages_hours_per_day) : "", + latitude: initial?.latitude != null ? String(initial.latitude) : "", + longitude: initial?.longitude != null ? String(initial.longitude) : "", + }; +} +``` + +- [ ] **Step 6: Typecheck** + +Run: `npm run typecheck` +Expected: no errors. + +- [ ] **Step 7: Set it for the one real site** + +Not a code step — a reminder for whoever runs this plan: after deploying, open the Sites page, edit "The Hills ext 12," and fill in both **Job start date** and (if its work month doesn't run 1st–30th) **Work month starts on day**. Until `job_start_date` is set, every sheet will show "—" instead of a Month/Week badge for that site's rows. + +- [ ] **Step 8: Commit** + +```bash +git add src/components/site-form-dialog.tsx +git commit -m "feat(sites): add work-month-start-day field to the site form" +``` + +--- + +### Task 6: Timesheets + +**Files:** +- Modify: `src/app/dashboard/workforce/timesheets/page.tsx` +- Modify: `src/app/dashboard/workforce/timesheets/timesheets-view.tsx` + +- [ ] **Step 1: Attach `projectPeriod` in the loader** + +In `src/app/dashboard/workforce/timesheets/page.tsx`, add the import and compute `now` once, then attach the field in the `rows` map: + +```typescript +import Link from "next/link"; +import { ClipboardList } from "lucide-react"; +import { pbAdmin } from "@/lib/pb"; +import { Topbar } from "@/components/layout/sidebar"; +import { Button } from "@/components/ui/button"; +import { ImportTimesheetsButton } from "@/components/import-timesheets-button"; +import { deriveProjectPeriodLabel } from "@/lib/project-calendar"; +import { TimesheetsView, type TimesheetEntryRow } from "./timesheets-view"; +``` + +In the loader body, right after `const docStatusById = new Map(...)`, add: + +```typescript + const now = new Date(); +``` + +Then in the `rows` map, add `projectPeriod` to the returned object: + +```typescript + const rows: TimesheetEntryRow[] = entries.map((e) => { + const emp = (e.expand?.employee ?? + employeeById.get(e.employee)) as { name?: string; employee_no?: string } | undefined; + const site = (e.expand?.site ?? siteById.get(e.site)) as + | { name?: string; project_code?: string } + | undefined; + const timesheetId = e.expand?.timesheet?.id ?? (e.timesheet ?? ""); + const siteRecord = siteById.get(e.site); + return { + id: e.id, + employeeId: e.employee ?? "", + employeeName: emp?.name ?? "(unknown)", + employeeNo: emp?.employee_no ?? "", + siteId: e.site ?? "", + siteName: site?.name ?? "(unknown)", + siteProjectCode: site?.project_code ?? "", + dateLogged: e.date_logged ?? "", + hoursWorked: e.hours_worked ?? 0, + hasDocument: !!timesheetId, + documentStatus: timesheetId ? docStatusById.get(timesheetId) ?? null : null, + projectPeriod: deriveProjectPeriodLabel(e.date_logged ?? "", siteRecord ?? {}, now), + }; + }); +``` + +- [ ] **Step 2: Add the field to `TimesheetEntryRow` and filter/display it** + +In `src/app/dashboard/workforce/timesheets/timesheets-view.tsx`: + +Add the import: + +```typescript +import { + type ProjectPeriodFilterValue, + type ProjectPeriodLabel, + matchesProjectPeriodFilter, +} from "@/lib/project-calendar"; +import { ProjectPeriodBadge } from "@/components/project-period-badge"; +``` + +Add the field to the row interface: + +```typescript +export interface TimesheetEntryRow { + id: string; + employeeId: string; + employeeName: string; + employeeNo: string; + siteId: string; + siteName: string; + siteProjectCode: string; + /** ISO date string (YYYY-MM-DD). */ + dateLogged: string; + hoursWorked: number; + /** True when the row was created from a spreadsheet upload. */ + hasDocument: boolean; + /** Document status from the source upload, if any. */ + documentStatus?: string | null; + projectPeriod: ProjectPeriodLabel | null; +} +``` + +Add filter state (next to `sourceFilter`): + +```typescript + const [periodFilter, setPeriodFilter] = React.useState("all"); +``` + +Add it to the filter predicate: + +```typescript + const filtered = React.useMemo(() => { + const q = search.trim().toLowerCase(); + return rows.filter((r) => { + if (siteFilter !== "all" && r.siteId !== siteFilter) return false; + if (sourceFilter === "uploaded" && !r.hasDocument) return false; + if (sourceFilter === "direct" && r.hasDocument) return false; + if (!matchesProjectPeriodFilter(periodFilter, r.projectPeriod)) return false; + if (q) { + const hay = `${r.employeeName} ${r.employeeNo}`.toLowerCase(); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [rows, siteFilter, sourceFilter, periodFilter, search]); +``` + +Add it to the page-reset effect and the `filtersActive`/reset-button logic: + +```typescript + React.useEffect(() => { + setPage(1); + }, [siteFilter, sourceFilter, periodFilter, search]); + + const filtersActive = + siteFilter !== "all" || sourceFilter !== "all" || periodFilter !== "all" || search !== ""; +``` + +```typescript + {filtersActive && ( + + )} +``` + +Add the filter control, right after the "Source" `SelectFilter`: + +```tsx + setPeriodFilter(v as ProjectPeriodFilterValue)} + options={[ + { value: "all", label: "All time" }, + { value: "today", label: "Today" }, + { value: "week", label: "This week" }, + { value: "month", label: "This month" }, + ]} + /> +``` + +Add the badge column to the table. Header: + +```tsx + + Date + Worker + Site + Month/Week + Hours + Source + +``` + +Body — add one `` after the Site cell: + +```tsx + +
+ {r.siteName} + {r.siteProjectCode && ( + + {r.siteProjectCode} + + )} +
+
+ + + +``` + +- [ ] **Step 3: Typecheck** + +Run: `npm run typecheck` +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +git add src/app/dashboard/workforce/timesheets/page.tsx src/app/dashboard/workforce/timesheets/timesheets-view.tsx +git commit -m "feat(timesheets): add project Month/Week badge and filter" +``` + +--- + +### Task 7: Delivery Notes + +**Files:** +- Modify: `src/app/dashboard/financial/delivery-notes/page.tsx` +- Modify: `src/app/dashboard/financial/delivery-notes/delivery-notes-view.tsx` + +- [ ] **Step 1: Attach `projectPeriod` in the loader** + +In `src/app/dashboard/financial/delivery-notes/page.tsx`, add the import: + +```typescript +import { deriveProjectPeriodLabel } from "@/lib/project-calendar"; +``` + +Right after `const siteById = new Map(...)`, add: + +```typescript + const now = new Date(); +``` + +In the `rows` map, add `projectPeriod`: + +```typescript + const rows: DeliveryNoteRow[] = dns.map((d) => { + const site = + (d.expand?.site as { name?: string; project_code?: string } | undefined) ?? + siteById.get(d.site); + const poId = + (d.expand?.purchase_order as { id?: string } | undefined)?.id ?? + (d.purchase_order ?? ""); + const itemAgg = itemsByDn.get(d.id) ?? { count: 0, total: 0 }; + return { + id: d.id, + deliveryNoteNumber: d.delivery_note_number ?? "", + siteId: d.site ?? "", + siteName: site?.name ?? "(unknown)", + siteProjectCode: site?.project_code ?? "", + purchaseOrderId: poId, + purchaseOrderNumber: poId ? poById.get(poId) ?? "" : "", + dateReceived: d.date_received ?? "", + receivedByName: d.received_by_name ?? d.received_by ?? "", + processingStatus: d.processing_status ?? "", + isVerified: !!d.is_verified, + ocrConfidence: d.ocr_confidence ?? null, + slipImage: d.slip_image ?? "", + itemCount: itemAgg.count, + totalReceived: itemAgg.total, + projectPeriod: deriveProjectPeriodLabel( + d.date_received ?? "", + siteById.get(d.site) ?? {}, + now, + ), + }; + }); +``` + +- [ ] **Step 2: Add the field to `DeliveryNoteRow` and filter/display it** + +In `src/app/dashboard/financial/delivery-notes/delivery-notes-view.tsx`: + +Add the import: + +```typescript +import { + type ProjectPeriodFilterValue, + type ProjectPeriodLabel, + matchesProjectPeriodFilter, +} from "@/lib/project-calendar"; +import { ProjectPeriodBadge } from "@/components/project-period-badge"; +``` + +Add the field: + +```typescript +export interface DeliveryNoteRow { + id: string; + deliveryNoteNumber: string; + siteId: string; + siteName: string; + siteProjectCode: string; + purchaseOrderId: string; + purchaseOrderNumber: string; + dateReceived: string; + receivedByName: string; + processingStatus: string; + isVerified: boolean; + ocrConfidence: number | null; + slipImage: string; + itemCount: number; + totalReceived: number; + projectPeriod: ProjectPeriodLabel | null; +} +``` + +Add filter state: + +```typescript + const [periodFilter, setPeriodFilter] = React.useState("all"); +``` + +Add it to the filter predicate: + +```typescript + const filtered = React.useMemo(() => { + const q = search.trim().toLowerCase(); + return rows.filter((r) => { + if (siteFilter !== "all" && r.siteId !== siteFilter) return false; + if (statusFilter !== "all") { + if (statusFilter === "manually-verified") { + if (!r.isVerified) return false; + } else if (r.processingStatus !== statusFilter) { + return false; + } + } + if (!matchesProjectPeriodFilter(periodFilter, r.projectPeriod)) return false; + if (q) { + const hay = `${r.deliveryNoteNumber} ${r.purchaseOrderNumber}`.toLowerCase(); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [rows, siteFilter, statusFilter, periodFilter, search]); +``` + +Reset effect and `filtersActive`: + +```typescript + React.useEffect(() => { + setPage(1); + }, [siteFilter, statusFilter, periodFilter, search]); +``` + +```typescript + const filtersActive = + siteFilter !== "all" || statusFilter !== "all" || periodFilter !== "all" || search !== ""; +``` + +Reset button handler: + +```typescript + {filtersActive && ( + + )} +``` + +Filter control, right after the "Status" `SelectFilter`: + +```tsx + setPeriodFilter(v as ProjectPeriodFilterValue)} + options={[ + { value: "all", label: "All time" }, + { value: "today", label: "Today" }, + { value: "week", label: "This week" }, + { value: "month", label: "This month" }, + ]} + /> +``` + +Table header — add a column after "Site": + +```tsx + + Date + DN # + PO # + Site + Month/Week + Lines + Qty + Status + + +``` + +Table body — add the cell after the Site cell: + +```tsx + +
+ {r.siteName} + {r.siteProjectCode && ( + + {r.siteProjectCode} + + )} +
+
+ + + +``` + +- [ ] **Step 3: Typecheck** + +Run: `npm run typecheck` +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +git add src/app/dashboard/financial/delivery-notes/page.tsx src/app/dashboard/financial/delivery-notes/delivery-notes-view.tsx +git commit -m "feat(delivery-notes): add project Month/Week badge and filter" +``` + +--- + +### Task 8: Purchase Orders + +Unlike the other three sheets, `PoRow` doesn't currently carry a raw `siteId` or upload date at all (only the derived `projectCode`/`projectName`, and `requiredByDate` which is a different date than when the PO was uploaded) — those two fields need adding first. This page also renders as a card list (`
  • `), not a ``, so the badge goes on the card, not in a table cell. + +**Files:** +- Modify: `src/app/dashboard/financial/purchase-orders/page.tsx` +- Modify: `src/app/dashboard/financial/purchase-orders/purchasing-view.tsx` + +- [ ] **Step 1: Add `siteId`/`dateUploaded`, build a site lookup, attach `projectPeriod`** + +In `src/app/dashboard/financial/purchase-orders/page.tsx`, add the import: + +```typescript +import { deriveProjectPeriodLabel } from "@/lib/project-calendar"; +``` + +Right after `const dnsByPo = new Map(...)` block, add a site lookup (this page doesn't build one today): + +```typescript + const siteById = new Map(sites.map((s) => [s.id, s])); + const now = new Date(); +``` + +In the `rows` map, add `siteId`, `dateUploaded`, and `projectPeriod` to the returned object: + +```typescript + return { + id: p.id, + poNumber: p.purchase_order_number, + supplier: p.supplier, + supplierRefId: p.supplier_ref ?? null, + supplierRefName: supplierRef?.name ?? null, + projectCode, + projectName: site?.name ?? projectCode, + siteId: p.site ?? "", + dateUploaded: p.date_uploaded ?? "", + requiredByDate: p.required_by_date, + orderedBy: p.ordered_by, + isVerified: !!p.is_verified, + totalValue: p.total_value, + itemCount: its.length, + deliveryNoteCount: dnsForPo.length, + deliveryNoteVerified: dnsForPo.filter((d) => d.is_verified).length, + pendingDnCount, + fulfillmentPct, + totalOrdered: ord, + totalReceived: rcv, + projectPeriod: deriveProjectPeriodLabel( + p.date_uploaded ?? "", + siteById.get(p.site) ?? {}, + now, + ), + }; +``` + +- [ ] **Step 2: Add the fields to `PoRow`, filter, and render the badge** + +In `src/app/dashboard/financial/purchase-orders/purchasing-view.tsx`: + +Add the import: + +```typescript +import { + type ProjectPeriodFilterValue, + type ProjectPeriodLabel, + matchesProjectPeriodFilter, +} from "@/lib/project-calendar"; +import { ProjectPeriodBadge } from "@/components/project-period-badge"; +``` + +Add the fields to `PoRow`: + +```typescript +export interface PoRow { + id: string; + poNumber: string; + projectCode: string; + projectName: string; + siteId: string; + /** ISO date string (YYYY-MM-DD) — when the PO document was uploaded. */ + dateUploaded: string; + supplier: string; + /** Resolved supplier record id, if a match was made at upload or later. */ + supplierRefId: string | null; + /** Display name of the linked supplier (mirrors supplierRefId's record). */ + supplierRefName: string | null; + requiredByDate: string; + orderedBy: string; + totalValue: number; + isVerified: boolean; + itemCount: number; + deliveryNoteCount: number; + deliveryNoteVerified: number; + pendingDnCount: number; + fulfillmentPct: number; + totalOrdered: number; + totalReceived: number; + projectPeriod: ProjectPeriodLabel | null; +} +``` + +Add filter state, next to `statusFilter`: + +```typescript + const [periodFilter, setPeriodFilter] = React.useState("all"); +``` + +Add it to the filter predicate: + +```typescript + const filtered = React.useMemo(() => { + const q = search.trim().toLowerCase(); + return rows.filter((r) => { + if (projectFilter !== "all" && r.projectCode !== projectFilter) return false; + if (statusFilter === "verified" && !r.isVerified) return false; + if (statusFilter === "unverified" && r.isVerified) return false; + if (statusFilter === "fulfilled" && r.fulfillmentPct < 100) return false; + if (statusFilter === "open" && r.fulfillmentPct >= 100) return false; + if (!matchesProjectPeriodFilter(periodFilter, r.projectPeriod)) return false; + if (q) { + const hay = `${r.poNumber} ${r.supplier} ${r.projectCode}`.toLowerCase(); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [rows, projectFilter, statusFilter, periodFilter, search]); +``` + +Reset effect and `filtersActive`: + +```typescript + React.useEffect(() => { + setPage(1); + }, [projectFilter, statusFilter, periodFilter, search]); +``` + +```typescript + const filtersActive = + projectFilter !== "all" || statusFilter !== "all" || periodFilter !== "all" || search !== ""; +``` + +Reset button handler: + +```typescript + {filtersActive && ( + + )} +``` + +Filter control — this file uses a raw `` block: + +```tsx + +``` + +Render the badge on the PO card. In `PoListItem`, add it next to the project-code badge: + +```tsx + + {po.projectCode || "—"} + + +``` + +- [ ] **Step 3: Typecheck** + +Run: `npm run typecheck` +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +git add src/app/dashboard/financial/purchase-orders/page.tsx src/app/dashboard/financial/purchase-orders/purchasing-view.tsx +git commit -m "feat(purchase-orders): add project Month/Week badge and filter" +``` + +--- + +### Task 9: Fuel Logs + +**Files:** +- Modify: `src/app/dashboard/plant/fuel-logs/page.tsx` +- Modify: `src/app/dashboard/plant/fuel-logs/fuel-logs-view.tsx` + +- [ ] **Step 1: Attach `projectPeriod` in the loader** + +In `src/app/dashboard/plant/fuel-logs/page.tsx`, add the import: + +```typescript +import { deriveProjectPeriodLabel } from "@/lib/project-calendar"; +``` + +Right after `const fuelPrices = await loadFuelPrices();`, add: + +```typescript + const now = new Date(); +``` + +In the `rows` map, add `projectPeriod`: + +```typescript + return { + id: l.id, + dateLogged: l.date_logged ?? "", + vehicleId: l.vehicle ?? "", + vehicleDescription: v?.description ?? "(unknown)", + unitType: + v?.unit_type === "plant" ? "plant" : "fuel", + siteId: l.site ?? "", + siteName: site?.name ?? "(unknown)", + siteProjectCode: site?.project_code ?? "", + litres: l.litres ?? 0, + odoReading: l.odo_reading ?? null, + cost: (l.litres ?? 0) * resolveFuelPrice(l.date_logged, fuelPrices), + latestConsumption: + "latest_consumption" in l + ? ((l as { latest_consumption?: number }).latest_consumption ?? null) + : null, + hasDocument: !!fuelSheetId, + documentStatus: fuelSheetId + ? docStatusById.get(fuelSheetId) ?? null + : null, + projectPeriod: deriveProjectPeriodLabel( + l.date_logged ?? "", + siteById.get(l.site) ?? {}, + now, + ), + }; +``` + +- [ ] **Step 2: Add the field to `FuelLogRow` and filter/display it** + +In `src/app/dashboard/plant/fuel-logs/fuel-logs-view.tsx`: + +Add the import: + +```typescript +import { + type ProjectPeriodFilterValue, + type ProjectPeriodLabel, + matchesProjectPeriodFilter, +} from "@/lib/project-calendar"; +import { ProjectPeriodBadge } from "@/components/project-period-badge"; +``` + +Add the field: + +```typescript +export interface FuelLogRow { + id: string; + dateLogged: string; + vehicleId: string; + vehicleDescription: string; + unitType: "fuel" | "plant"; + siteId: string; + siteName: string; + siteProjectCode: string; + litres: number; + odoReading: number | null; + cost: number; + latestConsumption: number | null; + hasDocument: boolean; + documentStatus?: string | null; + projectPeriod: ProjectPeriodLabel | null; +} +``` + +Add filter state, next to `sourceFilter`: + +```typescript + const [periodFilter, setPeriodFilter] = React.useState("all"); +``` + +Add it to the filter predicate: + +```typescript + const filtered = React.useMemo(() => { + const q = search.trim().toLowerCase(); + return rows.filter((r) => { + if (siteFilter !== "all" && r.siteId !== siteFilter) return false; + if (unitFilter !== "all" && r.unitType !== unitFilter) return false; + if (sourceFilter === "uploaded" && !r.hasDocument) return false; + if (sourceFilter === "direct" && r.hasDocument) return false; + if (!matchesProjectPeriodFilter(periodFilter, r.projectPeriod)) return false; + if (q) { + const hay = `${r.vehicleDescription}`.toLowerCase(); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [rows, siteFilter, unitFilter, sourceFilter, periodFilter, search]); +``` + +Reset effect and `filtersActive`: + +```typescript + React.useEffect(() => { + setPage(1); + }, [siteFilter, unitFilter, sourceFilter, periodFilter, search]); +``` + +```typescript + const filtersActive = + siteFilter !== "all" || unitFilter !== "all" || sourceFilter !== "all" || periodFilter !== "all" || search !== ""; +``` + +Reset button handler: + +```typescript + {filtersActive && ( + + )} +``` + +Filter control, right after the "Source" `SelectFilter`: + +```tsx + setPeriodFilter(v as ProjectPeriodFilterValue)} + options={[ + { value: "all", label: "All time" }, + { value: "today", label: "Today" }, + { value: "week", label: "This week" }, + { value: "month", label: "This month" }, + ]} + /> +``` + +Table header — add a column after "Site": + +```tsx + + Date + Vehicle / equipment + Site + Month/Week + Litres + Reading + Cost + Source + +``` + +Table body — add the cell after the Site cell: + +```tsx + +
    + {r.siteName} + {r.siteProjectCode && ( + + {r.siteProjectCode} + + )} +
    +
    + + + +``` + +- [ ] **Step 3: Typecheck** + +Run: `npm run typecheck` +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +git add src/app/dashboard/plant/fuel-logs/page.tsx src/app/dashboard/plant/fuel-logs/fuel-logs-view.tsx +git commit -m "feat(fuel-logs): add project Month/Week badge and filter" +``` + +--- + +### Task 10: Full verification pass + +**Files:** none (verification only) + +- [ ] **Step 1: Typecheck the whole project** + +Run: `npm run typecheck` +Expected: 0 errors. + +- [ ] **Step 2: Run the full test suite** + +Run: `npm run test:run` +Expected: all tests pass, including the new `project-calendar.test.ts`. + +- [ ] **Step 3: Manual check (not automatable)** + +Per the design decisions above, confirm by hand: +- Edit the one real site ("The Hills ext 12") and set a Job start date (and a Work month start day if it isn't the 1st). Confirm the four sheets start showing "M1 · W1"-style badges for that site's rows instead of "—". +- On each of the four pages, confirm the Period filter narrows to the expected rows for Today / This week / This month, and "All time" shows everything again. +- Confirm a row from BEFORE the site's job start date still renders "—" (out of range) rather than a negative or zero month number. + +- [ ] **Step 4: Commit any fixes found during verification** + +```bash +git add -A +git commit -m "fix(project-calendar): address verification findings" +``` + +(Skip this step entirely if nothing needed fixing.) + +--- + +## Self-Review + +**Spec coverage:** +- Custom per-site work-month start day, not just calendar months → Tasks 1, 2 ✅ +- Day-of-month overflow clamps to the last real day → Task 2 (`clampDay`, tested against Feb) ✅ +- Weeks are simple 7-day blocks from the cycle start, capped at 4 → Task 2 (tested) ✅ +- Every row on Timesheets/Delivery Notes/Purchase Orders/Fuel Logs shows its Month/Week → Tasks 6–9 ✅ +- Today/This week/This month/All time filter on all four → Tasks 6–9 ✅ +- Setting the start day + start date happens on the site edit page → Task 5 ✅ +- Vehicles/Equipment explicitly out of scope → not touched anywhere in this plan ✅ + +**Placeholder scan:** none found — every step has complete code or an exact command with expected output. The one non-code step (Task 5 Step 7) is explicitly marked as a manual reminder, not a coding task, and says exactly what to do. + +**Type consistency:** `ProjectPeriod`, `ProjectPeriodLabel`, `ProjectPeriodSite`, and `ProjectPeriodFilterValue` are defined once in `project-calendar.ts` (Task 2) and imported by that exact name everywhere else (Tasks 3, 6–9) — no renamed variants. `deriveProjectPeriodLabel`'s signature (`dateISO, site, now?`) is called identically in all four loaders. `matchesProjectPeriodFilter`'s signature (`filter, period`) is called identically in all four views. + +--- + +## Execution Choice + +Plan complete and saved to `docs/superpowers/plans/2026-09-11-project-month-week.md`. Two execution options: + +1. **Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration. +2. **Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints. + +Which approach? (Or hand it to your MiniMax agent, same as the last few — I'll review the diff afterward.) diff --git a/scripts/migrations/2026-09-11-add-site-work-month-field.py b/scripts/migrations/2026-09-11-add-site-work-month-field.py index a752b21..bbb247c 100644 --- a/scripts/migrations/2026-09-11-add-site-work-month-field.py +++ b/scripts/migrations/2026-09-11-add-site-work-month-field.py @@ -8,7 +8,7 @@ One-shot PocketBase migration applied via: python3 scripts/migrations/2026-09-11-add-site-work-month-field.py Adds sites.work_month_start_day (see -docs/superpowers/specs/2026-09-11-project-month-week-design.md for why: +docs/superpowers/plans/2026-09-11-project-month-week.md for why: some sites' "work months" don't run 1st-30th, they run e.g. 21st-21st). Strictly additive, and the field is not required — existing sites keep