docs(plans): purchasing UI tweaks — 4 small tasks with TDD-friendly bite-sized steps
This commit is contained in:
parent
7020449853
commit
a8e67faf3d
1 changed files with 713 additions and 0 deletions
713
docs/superpowers/plans/2026-08-26-purchasing-ui-tweaks.md
Normal file
713
docs/superpowers/plans/2026-08-26-purchasing-ui-tweaks.md
Normal file
|
|
@ -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 `<Button>` at the start of the component return (around line 431–434):
|
||||
|
||||
```tsx
|
||||
<Button onClick={() => setOpen(true)} className="gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload purchase order
|
||||
</Button>
|
||||
```
|
||||
|
||||
Replace the inner text only:
|
||||
|
||||
```tsx
|
||||
<Button onClick={() => setOpen(true)} className="gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload PO
|
||||
</Button>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Edit `src/components/upload-delivery-note-button.tsx` button label**
|
||||
|
||||
Find the `<Button>` at the start of the component return (around line 136–139):
|
||||
|
||||
```tsx
|
||||
<Button onClick={() => setOpen(true)} className="gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload delivery note
|
||||
</Button>
|
||||
```
|
||||
|
||||
Replace the inner text only:
|
||||
|
||||
```tsx
|
||||
<Button onClick={() => setOpen(true)} className="gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload DN
|
||||
</Button>
|
||||
```
|
||||
|
||||
- [ ] **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 `<p>` showing the DN number (around line 339–342):
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
<p className="font-mono text-sm font-semibold">
|
||||
{dn.deliveryNoteNumber || "(no DN number)"}
|
||||
</p>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="font-mono text-sm font-semibold">
|
||||
{dn.deliveryNoteNumber || "(no DN number)"}
|
||||
</p>
|
||||
<DnStatusPill status={dn.processingStatus} />
|
||||
</div>
|
||||
```
|
||||
|
||||
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 (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="gap-1 border-amber-500/40 bg-amber-500/15 text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{status === "needs-review" ? "Needs review" : "Pending"}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (status === "processed") {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-border bg-muted/40 text-muted-foreground"
|
||||
>
|
||||
Processed
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
className="group flex items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#F2B165]/40"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-[#F2B165]/15"
|
||||
>
|
||||
<Package className="h-3.5 w-3.5 text-[#F2B165]" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<span className="font-mono text-sm font-semibold">{po.poNumber}</span>
|
||||
<span className="truncate text-sm text-foreground">
|
||||
{po.supplier || "—"}
|
||||
</span>
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{po.projectCode || "—"}
|
||||
</Badge>
|
||||
{po.requiredByDate && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Required {format(parseISO(po.requiredByDate), "yyyy-MM-dd")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{po.itemCount} line item{po.itemCount === 1 ? "" : "s"} ·{" "}
|
||||
{po.deliveryNoteCount} delivery note
|
||||
{po.deliveryNoteCount === 1 ? "" : "s"}
|
||||
{po.deliveryNoteCount > 0 &&
|
||||
` (${po.deliveryNoteVerified} verified)`}
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace the whole `PoListItem` body (everything from the `function PoListItem` declaration through `</Link>` and `</li>`) with:
|
||||
|
||||
```tsx
|
||||
function PoListItem({ po }: { po: PoRow }) {
|
||||
const href = `${PO_BASE}/${po.id}`;
|
||||
const hasPending = po.pendingDnCount > 0;
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
"group relative flex items-center gap-3 px-4 py-3 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#F2B165]/40",
|
||||
hasPending
|
||||
? "border-l-2 border-amber-500/60 bg-amber-500/5 hover:bg-amber-500/10"
|
||||
: "hover:bg-muted/40",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md",
|
||||
hasPending ? "bg-amber-500/20" : "bg-[#F2B165]/15",
|
||||
)}
|
||||
>
|
||||
<Package
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
hasPending ? "text-amber-700 dark:text-amber-300" : "text-[#F2B165]",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<span className="font-mono text-sm font-semibold">{po.poNumber}</span>
|
||||
<span className="truncate text-sm text-foreground">
|
||||
{po.supplier || "—"}
|
||||
</span>
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{po.projectCode || "—"}
|
||||
</Badge>
|
||||
{po.requiredByDate && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Required {format(parseISO(po.requiredByDate), "yyyy-MM-dd")}
|
||||
</span>
|
||||
)}
|
||||
{hasPending && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="gap-1 border-amber-500/40 bg-amber-500/15 text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Pending DNs: {po.pendingDnCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{po.itemCount} line item{po.itemCount === 1 ? "" : "s"} ·{" "}
|
||||
{po.deliveryNoteCount} delivery note
|
||||
{po.deliveryNoteCount === 1 ? "" : "s"}
|
||||
{po.deliveryNoteCount > 0 &&
|
||||
` (${po.deliveryNoteVerified} verified)`}
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
(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
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<VerifyButton
|
||||
collection="purchase_orders"
|
||||
id={detail.id}
|
||||
verified={detail.isVerified}
|
||||
onDone={refresh}
|
||||
/>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold tabular-nums">
|
||||
{formatCurrency(detail.totalValue)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">PO value</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{detail.purchaseOrderFile && (
|
||||
<ViewPoPdfButton
|
||||
filename={detail.purchaseOrderFile}
|
||||
recordId={detail.id}
|
||||
poNumber={detail.poNumber}
|
||||
/>
|
||||
)}
|
||||
<VerifyButton
|
||||
collection="purchase_orders"
|
||||
id={detail.id}
|
||||
verified={detail.isVerified}
|
||||
onDone={refresh}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold tabular-nums">
|
||||
{formatCurrency(detail.totalValue)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">PO value</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **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 (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
View PO PDF
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{poNumber}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SlipPreview filename={filename} recordId={recordId} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **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 `<SlipPreview filename={filename} recordId={recordId} />` 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` `<SlipPreview>` callsite already uses `filename={dn.slipImage} recordId={dn.id}` — confirmed by re-reading the existing file. So `filename` is the correct prop name everywhere.
|
||||
Loading…
Reference in a new issue