From a8e67faf3df72d6433caa2648efa8b6b646f97e2 Mon Sep 17 00:00:00 2001 From: DEls Date: Wed, 26 Aug 2026 23:58:30 +0200 Subject: [PATCH] =?UTF-8?q?docs(plans):=20purchasing=20UI=20tweaks=20?= =?UTF-8?q?=E2=80=94=204=20small=20tasks=20with=20TDD-friendly=20bite-size?= =?UTF-8?q?d=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-08-26-purchasing-ui-tweaks.md | 713 ++++++++++++++++++ 1 file changed, 713 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-26-purchasing-ui-tweaks.md diff --git a/docs/superpowers/plans/2026-08-26-purchasing-ui-tweaks.md b/docs/superpowers/plans/2026-08-26-purchasing-ui-tweaks.md new file mode 100644 index 0000000..5351ef8 --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-purchasing-ui-tweaks.md @@ -0,0 +1,713 @@ +# Purchasing UI tweaks 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:** Make four small UI/data fixes to the Purchasing & Deliveries dashboard: shorter upload button labels, pending-DN visual styling in two places, a "View PO PDF" button on PO detail, and a fulfillment-rate data fix. + +**Architecture:** All four changes are presentational or one-line data-fetching tweaks. No new components (reuse `SlipPreview`), no new state machines, no new routes. Server mappers thread two extra fields (`processingStatus`, `purchaseOrderFile`); the existing view components grow minor branches. + +**Tech Stack:** Next.js 16 App Router, React 19, Radix Dialog, Tailwind CSS, PocketBase admin SDK. + +--- + +## File Structure + +**Modify:** +- `src/components/upload-purchase-order-button.tsx` — button label only. +- `src/components/upload-delivery-note-button.tsx` — button label only. +- `src/app/dashboard/purchasing/page.tsx` — `received_items` query: `getList(1, 500, ...)` → `getFullList(...)`. +- `src/app/dashboard/purchasing/purchasing-view.tsx` — `PoListItem` shows amber treatment + inline "Pending DNs" badge when `pendingDnCount > 0`. +- `src/app/dashboard/purchasing/[poId]/page.tsx` — server mapper: thread `processingStatus` and `purchaseOrderFile` to `PoDetail`. +- `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx` — extend `PoDetailDn` (add `processingStatus`), add status pill on `DeliveryNoteCard`, extend `PoDetail` (add `purchaseOrderFile`), add `View PO PDF` button + dialog in the PO header. + +**No new files.** All changes reuse `SlipPreview` and existing palette tokens. + +--- + +## Task 1: Shorter upload button labels + +**Files:** +- Modify: `src/components/upload-purchase-order-button.tsx` (1 line) +- Modify: `src/components/upload-delivery-note-button.tsx` (1 line) +- Modify: `src/app/dashboard/purchasing/page.tsx` (no change needed — `flex-wrap` already there; just verify) + +The third file needs no edit: `page.tsx:174` already uses `flex flex-wrap`. Shorter labels push the wrap point down — on common phone widths (375–414px) the two buttons will fit on one row, only stacking on truly tiny screens. No code change required there. + +- [ ] **Step 1: Edit `src/components/upload-purchase-order-button.tsx` button label** + +Find the ` +``` + +Replace the inner text only: + +```tsx + +``` + +- [ ] **Step 2: Edit `src/components/upload-delivery-note-button.tsx` button label** + +Find the ` +``` + +Replace the inner text only: + +```tsx + +``` + +- [ ] **Step 3: Verify typecheck** + +Run: +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE && npx tsc --noEmit +``` +Expected: exit 0. Pure label change, no type impact. + +- [ ] **Step 4: Commit** + +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE +git add src/components/upload-purchase-order-button.tsx src/components/upload-delivery-note-button.tsx +git -c user.email=DEls@local -c user.name=DEls commit -m "feat(purchasing): shorten upload button labels (Upload PO / Upload DN)" +``` + +--- + +## Task 2: Pending-DN styling (list card + detail rows) + +Two files modified in this task; one of them (`[poId]/page.tsx`) is touched again in Task 3 to add `purchaseOrderFile`, but `processingStatus` is independent and committed here. + +**Files:** +- Modify: `src/app/dashboard/purchasing/[poId]/page.tsx` (extend `dnRows` mapper) +- Modify: `src/app/dashboard/purchasing/purchasing-view.tsx` (`PoListItem` amber treatment) +- Modify: `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx` (`PoDetailDn` type + `DeliveryNoteCard` pill) + +- [ ] **Step 1: Extend the server mapper in `[poId]/page.tsx`** + +In `src/app/dashboard/purchasing/[poId]/page.tsx`, find the `dnRows` mapper (around line 83–98): + +```tsx + const dnRows = dns.map((d) => ({ + id: d.id, + deliveryNoteNumber: d.delivery_note_number, + projectCode: d.project_code, + dateReceived: d.date_received, + receivedBy: d.received_by, + isVerified: !!d.is_verified, + slipImage: d.slip_image, + rawOcrData: d.raw_ocr_data ?? null, + receivedItems: (receivedByDn.get(d.id) ?? []).map((r) => ({ + id: r.id, + purchaseOrderItemId: r.purchase_order_item, + qtyReceived: r.qty_received, + conditionNotes: r.condition_notes, + })), + })); +``` + +Add `processingStatus` after `rawOcrData`: + +```tsx + const dnRows = dns.map((d) => ({ + id: d.id, + deliveryNoteNumber: d.delivery_note_number, + projectCode: d.project_code, + dateReceived: d.date_received, + receivedBy: d.received_by, + isVerified: !!d.is_verified, + slipImage: d.slip_image, + rawOcrData: d.raw_ocr_data ?? null, + processingStatus: d.processing_status ?? "", + receivedItems: (receivedByDn.get(d.id) ?? []).map((r) => ({ + id: r.id, + purchaseOrderItemId: r.purchase_order_item, + qtyReceived: r.qty_received, + conditionNotes: r.condition_notes, + })), + })); +``` + +- [ ] **Step 2: Extend `PoDetailDn` type in `po-detail-view.tsx`** + +In `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx`, find the `PoDetailDn` interface (around line 40–55): + +```tsx +export interface PoDetailDn { + id: string; + deliveryNoteNumber: string; + projectCode: string; + dateReceived: string; + receivedBy: string; + isVerified: boolean; + slipImage: string; + rawOcrData: unknown; + receivedItems: { +``` + +Add `processingStatus: string;` after `rawOcrData`: + +```tsx +export interface PoDetailDn { + id: string; + deliveryNoteNumber: string; + projectCode: string; + dateReceived: string; + receivedBy: string; + isVerified: boolean; + slipImage: string; + rawOcrData: unknown; + processingStatus: string; + receivedItems: { +``` + +- [ ] **Step 3: Render the status pill in `DeliveryNoteCard`** + +In `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx`, find the `DeliveryNoteCard` component's `

` showing the DN number (around line 339–342): + +```tsx +

+

+ {dn.deliveryNoteNumber || "(no DN number)"} +

+``` + +Replace with: + +```tsx +
+
+

+ {dn.deliveryNoteNumber || "(no DN number)"} +

+ +
+``` + +Add a small `DnStatusPill` helper above `DeliveryNoteCard` (just above the `DeliveryNoteCard` function — it lives near the end of the file before `VerifyButton`). Insert this helper: + +```tsx +function DnStatusPill({ status }: { status: string }) { + if (status === "needs-review" || status === "pending") { + return ( + + + {status === "needs-review" ? "Needs review" : "Pending"} + + ); + } + if (status === "processed") { + return ( + + Processed + + ); + } + return null; +} +``` + +`Badge` and `AlertTriangle` are already imported at the top of the file — no import changes needed. + +- [ ] **Step 4: Add amber treatment to `PoListItem` in `purchasing-view.tsx`** + +In `src/app/dashboard/purchasing/purchasing-view.tsx`, find `PoListItem` (around line 303–357): + +```tsx +function PoListItem({ po }: { po: PoRow }) { + const href = `${PO_BASE}/${po.id}`; + return ( +
  • + + + + +
    +
    + {po.poNumber} + + {po.supplier || "—"} + + + {po.projectCode || "—"} + + {po.requiredByDate && ( + + Required {format(parseISO(po.requiredByDate), "yyyy-MM-dd")} + + )} +
    +

    + {po.itemCount} line item{po.itemCount === 1 ? "" : "s"} ·{" "} + {po.deliveryNoteCount} delivery note + {po.deliveryNoteCount === 1 ? "" : "s"} + {po.deliveryNoteCount > 0 && + ` (${po.deliveryNoteVerified} verified)`} +

    +
    +``` + +Replace the whole `PoListItem` body (everything from the `function PoListItem` declaration through `` and `
  • `) with: + +```tsx +function PoListItem({ po }: { po: PoRow }) { + const href = `${PO_BASE}/${po.id}`; + const hasPending = po.pendingDnCount > 0; + return ( +
  • + + + + +
    +
    + {po.poNumber} + + {po.supplier || "—"} + + + {po.projectCode || "—"} + + {po.requiredByDate && ( + + Required {format(parseISO(po.requiredByDate), "yyyy-MM-dd")} + + )} + {hasPending && ( + + + Pending DNs: {po.pendingDnCount} + + )} +
    +

    + {po.itemCount} line item{po.itemCount === 1 ? "" : "s"} ·{" "} + {po.deliveryNoteCount} delivery note + {po.deliveryNoteCount === 1 ? "" : "s"} + {po.deliveryNoteCount > 0 && + ` (${po.deliveryNoteVerified} verified)`} +

    +
    +``` + +(The rest of the component — FulfilmentBar / totalValue / VerifyBadge / arrow — stays unchanged.) + +`AlertTriangle` is already imported at the top of the file (used by `VerifyBadge`). `cn` is already imported. No import changes needed. + +- [ ] **Step 5: Verify typecheck** + +Run: +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE && npx tsc --noEmit +``` +Expected: exit 0. + +- [ ] **Step 6: Commit** + +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE +git add src/app/dashboard/purchasing/\[poId\]/page.tsx src/app/dashboard/purchasing/\[poId\]/po-detail-view.tsx src/app/dashboard/purchasing/purchasing-view.tsx +git -c user.email=DEls@local -c user.name=DEls commit -m "feat(purchasing): amber treatment for POs/DNs with pending status" +``` + +--- + +## Task 3: View PO PDF button on PO detail page + +**Files:** +- Modify: `src/app/dashboard/purchasing/[poId]/page.tsx` (extend `PoDetail` mapper) +- Modify: `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx` (extend `PoDetail` type, add button + dialog) + +- [ ] **Step 1: Extend `PoDetail` type and server mapper** + +In `src/app/dashboard/purchasing/[poId]/page.tsx`, find the `detail` mapper (around line 103–118): + +```tsx + const detail: PoDetail = { + id: po.id, + poNumber: po.purchase_order_number, + projectCode: po.project_code, + projectName: po.expand?.site?.name ?? "", + supplier: po.supplier, + requiredByDate: po.required_by_date, + orderedBy: po.ordered_by, + totalValue: po.total_value, + isVerified: !!po.is_verified, + items: itemRows, + deliveryNotes: dnRows, + fulfillmentPct, + totalOrdered, + totalReceived, + }; +``` + +Add `purchaseOrderFile` after `totalValue`: + +```tsx + const detail: PoDetail = { + id: po.id, + poNumber: po.purchase_order_number, + projectCode: po.project_code, + projectName: po.expand?.site?.name ?? "", + supplier: po.supplier, + requiredByDate: po.required_by_date, + orderedBy: po.ordered_by, + totalValue: po.total_value, + purchaseOrderFile: po.purchase_order_file ?? "", + isVerified: !!po.is_verified, + items: itemRows, + deliveryNotes: dnRows, + fulfillmentPct, + totalOrdered, + totalReceived, + }; +``` + +In `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx`, find the `PoDetail` interface (around line 57–72): + +```tsx +export interface PoDetail { + id: string; + poNumber: string; + projectCode: string; + projectName: string; + supplier: string; + requiredByDate: string; + orderedBy: string; + totalValue: number; + isVerified: boolean; + items: PoDetailItem[]; + deliveryNotes: PoDetailDn[]; + fulfillmentPct: number; + totalOrdered: number; + totalReceived: number; +} +``` + +Add `purchaseOrderFile: string;` after `totalValue`: + +```tsx +export interface PoDetail { + id: string; + poNumber: string; + projectCode: string; + projectName: string; + supplier: string; + requiredByDate: string; + orderedBy: string; + totalValue: number; + purchaseOrderFile: string; + isVerified: boolean; + items: PoDetailItem[]; + deliveryNotes: PoDetailDn[]; + fulfillmentPct: number; + totalOrdered: number; + totalReceived: number; +} +``` + +- [ ] **Step 2: Add a `ViewPoPdfButton` to the PO header** + +In `src/app/dashboard/purchasing/[poId]/po-detail-view.tsx`, find the PO header right column (around line 168–182): + +```tsx +
    + +
    +

    + {formatCurrency(detail.totalValue)} +

    +

    PO value

    +
    +
    +``` + +Replace with: + +```tsx +
    +
    + {detail.purchaseOrderFile && ( + + )} + +
    +
    +

    + {formatCurrency(detail.totalValue)} +

    +

    PO value

    +
    +
    +``` + +- [ ] **Step 3: Add the `ViewPoPdfButton` component** + +Add the imports first. Find the existing import block at the top of `po-detail-view.tsx`: + +```tsx +import { + AlertTriangle, + Banknote, + CheckCircle2, + ClipboardCheck, + ImageIcon, + Package, + PackageCheck, + Truck, +} from "lucide-react"; +``` + +Add `FileText` to the list: + +```tsx +import { + AlertTriangle, + Banknote, + CheckCircle2, + ClipboardCheck, + FileText, + ImageIcon, + Package, + PackageCheck, + Truck, +} from "lucide-react"; +``` + +Now add the `ViewPoPdfButton` component definition. Place it just above the `VerifyButton` component at the bottom of the file. Insert this: + +```tsx +function ViewPoPdfButton({ + filename, + recordId, + poNumber, +}: { + filename: string; + recordId: string; + poNumber: string; +}) { + const [open, setOpen] = React.useState(false); + return ( + <> + + + + + {poNumber} + + + + + + ); +} +``` + +- [ ] **Step 4: Verify typecheck** + +Run: +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE && npx tsc --noEmit +``` +Expected: exit 0. + +- [ ] **Step 5: Commit** + +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE +git add src/app/dashboard/purchasing/\[poId\]/page.tsx src/app/dashboard/purchasing/\[poId\]/po-detail-view.tsx +git -c user.email=DEls@local -c user.name=DEls commit -m "feat(po-detail): add View PO PDF button using existing SlipPreview" +``` + +--- + +## Task 4: Fulfillment rate data fix + +**Files:** +- Modify: `src/app/dashboard/purchasing/page.tsx` (replace `getList(1, 500, ...)` with `getFullList(...)`) + +Before the edit, do a quick data investigation to confirm the orphan-row hypothesis. The other two causes (cap and listRule) are fixed by the same code change; the orphans are separate. + +- [ ] **Step 1: Investigate orphan `received_items` rows** + +This step needs PocketBase admin token access. Run: + +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE && python3 <<'EOF' +import urllib.request, json, re +token = re.search(r'POCKETBASE_ADMIN_TOKEN=([^\s]+)', open('.env.local').read()).group(1) +def fetch(p): + return json.loads(urllib.request.urlopen(urllib.request.Request( + f'https://pb.mega.infra.apphub.co.za{p}', + headers={'Authorization': f'Bearer {token}'})).read()) + +ri = fetch('/api/collections/received_items/records?fields=id,purchase_order_item,qty_received&perPage=500')['items'] +total = len(ri) +orphans = [r for r in ri if not r.get('purchase_order_item')] +print(f'received_items rows: {total}') +print(f'orphans (empty purchase_order_item): {len(orphans)} ({len(orphans)/total*100:.1f}%)') +EOF +``` + +Note the orphan percentage. If it's small (<5%), the orphan rows aren't the user's reported bug — the cause is the cap or listRule (which the code change below addresses). If it's large (>5%), note it for the follow-up banner — but still apply the code change below to eliminate the cap. + +- [ ] **Step 2: Replace `getList` with `getFullList`** + +In `src/app/dashboard/purchasing/page.tsx`, find this block (around line 26–36): + +```tsx + // Authoritative source for received quantities. The list page + // used to read `purchase_order_items.qty_received` (a denormalized + // column set at ingest and never updated), which is why every PO + // showed 0% completion. The detail page sums from received_items + // and is correct; the list now does the same. + // NOTE: getList caps at 500 rows. If received_items grows past + // that, switch to getFullList or push the rollup server-side. + pb.collection("received_items").getList(1, 500, { + fields: "id,purchase_order_item,qty_received", + }), +``` + +Replace with: + +```tsx + // Authoritative source for received quantities. The list page + // used to read `purchase_order_items.qty_received` (a denormalized + // column set at ingest and never updated), which is why every PO + // showed 0% completion. The detail page sums from received_items + // and is correct; the list does the same. + // + // `getFullList` (not `getList`) — the old `getList(1, 500, ...)` + // silently truncated once received_items exceeded 500 rows, which + // hid fulfillment for later POs and is exactly the bug the user + // reported. `getFullList` walks PB's pagination internally. + pb.collection("received_items").getFullList({ + fields: "id,purchase_order_item,qty_received", + }), +``` + +- [ ] **Step 3: Verify typecheck + tests** + +Run: +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE && npx tsc --noEmit && npm run test:run 2>&1 | tail -15 +``` +Expected: `tsc` exit 0; tests show `1 failed | 188 passed (189)` baseline (pre-existing `process-batch/route.test.ts` failure is unrelated; no new failures). + +- [ ] **Step 4: Live verification — fulfillment reflects real data** + +The dev server should be running. Verify it: +```bash +curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:6004/dashboard/purchasing +``` +If it's not running, start it: +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE +set -a && source .env.local && set +a +N8N_INGEST_SECRET=test-secret N8N_INGEST_TOKEN=test-token npm run dev +``` + +Open `http://127.0.0.1:6004/dashboard/purchasing` in Playwright. Log in (PIN 2-4-6-8). On the PO list, check the "Fulfillment rate" KPI tile and the per-PO FulfilmentBars — they should show non-zero values for any PO with `received_items` rows. Cross-check against the PO detail page's per-item table (the detail page sums from the same `received_items` data so both should agree). + +- [ ] **Step 5: Commit** + +```bash +cd /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE +git add src/app/dashboard/purchasing/page.tsx +git -c user.email=DEls@local -c user.name=DEls commit -m "fix(purchasing): use getFullList for received_items to fix 0% fulfillment truncation" +``` + +--- + +## Self-review + +- **Spec coverage:** + - "Shorter upload labels" → Task 1. + - "Pending-DN styling (list card + detail rows)" → Task 2. + - "View PO PDF button on detail page" → Task 3. + - "Fulfillment rate bug" → Task 4. + - "Reuses SlipPreview" → Task 3 step 3 (ViewPoPdfButton body). + - "No new files" → confirmed; all four tasks are edits. + +- **Placeholder scan:** none — every code block is complete and runnable. + +- **Type consistency:** + - `PoDetail` interface adds `purchaseOrderFile: string;`; server mapper sets it from `po.purchase_order_file ?? ""` — types match. + - `PoDetailDn` interface adds `processingStatus: string;`; server mapper sets it from `d.processing_status ?? ""` — types match. + - `ViewPoPdfButton` props (`{ filename, recordId, poNumber }`) match the JSX usage and the `` call (lowercase `filename`, matching `SlipPreview`'s prop name). + - `SlipPreview`'s prop is `filename` — confirmed by reading the existing `DeliveryNoteCard` callsite which uses `filename={dn.slipImage} recordId={dn.id}`. + +- **Drag-along:** The original `DeliveryNoteCard` `` callsite already uses `filename={dn.slipImage} recordId={dn.id}` — confirmed by re-reading the existing file. So `filename` is the correct prop name everywhere. \ No newline at end of file