fix(delivery-notes): move 'Add item' to per-card button after each row

Step A — move the 'Add item' button from the top of the items
section to the bottom of the items list.

Step B — match the quote-form.tsx pattern: every item card is
followed by its own ghost + dashed-border 'Add another item'
button, so the operator never has to scroll back to the top
to add the next line. The dashed-border styling keeps it from
being mistaken for a submit/finish affordance.

The data-testid="dn-adhoc-add-item" selector is preserved on
the per-card buttons so any future test selectors continue to
work.

Fixes PINW-18
This commit is contained in:
Mika 2026-08-26 01:08:52 +02:00
parent e99850c646
commit e900772faa

View file

@ -1,9 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { Box, Loader2, Package, Trash2 } from "lucide-react";
import { Loader2, Package, Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -365,216 +365,217 @@ export function CreateAdHocDeliveryNoteDialog({ open, onOpenChange }: Props) {
selling. The description + quantity are always
editable so the operator can override. */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Items *</Label>
<Button
type="button"
size="sm"
variant="ghost"
onClick={addItem}
disabled={submitting}
data-testid="dn-adhoc-add-item"
>
<Box className="mr-1 h-3.5 w-3.5" />
Add item
</Button>
</div>
<Label>Items *</Label>
<div className="space-y-3">
{items.map((it, idx) => (
<div
key={idx}
className="rounded-md border bg-card/50 p-3 space-y-2"
data-testid={`dn-adhoc-item-row-${idx}`}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-semibold">Item #{idx + 1}</span>
{it.stock_item_id && (
<span
className="inline-flex items-center gap-1 rounded-full bg-info/15 px-2 py-0.5 text-[10px] font-medium text-info"
title="This row was filled from a stock item"
<Fragment key={idx}>
<div
className="rounded-md border bg-card/50 p-3 space-y-2"
data-testid={`dn-adhoc-item-row-${idx}`}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-semibold">Item #{idx + 1}</span>
{it.stock_item_id && (
<span
className="inline-flex items-center gap-1 rounded-full bg-info/15 px-2 py-0.5 text-[10px] font-medium text-info"
title="This row was filled from a stock item"
>
<Package className="h-3 w-3" />
From stock
</span>
)}
{it.priced_from_catalogue && (
<span
className="inline-flex items-center gap-1 rounded-full bg-success/15 px-2 py-0.5 text-[10px] font-medium text-success"
title="Cost + selling price were auto-filled from price_list"
>
Priced from catalogue
</span>
)}
</div>
{items.length > 1 && (
<Button
type="button"
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => removeItem(idx)}
disabled={submitting}
aria-label={`Remove item ${idx + 1}`}
>
<Package className="h-3 w-3" />
From stock
</span>
)}
{it.priced_from_catalogue && (
<span
className="inline-flex items-center gap-1 rounded-full bg-success/15 px-2 py-0.5 text-[10px] font-medium text-success"
title="Cost + selling price were auto-filled from price_list"
>
Priced from catalogue
</span>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div>
{items.length > 1 && (
<Button
type="button"
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => removeItem(idx)}
disabled={submitting}
aria-label={`Remove item ${idx + 1}`}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div>
{/* "Pick from stock" combobox. When the operator
picks an item, the row auto-fills. Leaving
this empty just means the operator types
freehand. */}
<div>
<Label
htmlFor={`dn-adhoc-stock-${idx}`}
className="text-xs"
>
Pick from stock (optional)
</Label>
<StockItemSearch
stockItems={stockItems}
suppliers={suppliers}
value={it.stock_item_id}
onValueChange={(id) => void pickStockItem(idx, id)}
placeholder={
loadingLookups
? "Loading stock…"
: "Type a SKU or item name…"
}
disabled={loadingLookups || submitting}
/>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
<div className="sm:col-span-3">
<Label
htmlFor={`dn-adhoc-item-desc-${idx}`}
className="text-xs"
>
Description
</Label>
<Input
id={`dn-adhoc-item-desc-${idx}`}
value={it.description}
onChange={(e) =>
updateItem(idx, { description: e.target.value })
}
placeholder="e.g. 10x M8 bolts"
disabled={submitting}
/>
</div>
{/* "Pick from stock" combobox. When the operator
picks an item, the row auto-fills. Leaving
this empty just means the operator types
freehand. */}
<div>
<Label
htmlFor={`dn-adhoc-item-qty-${idx}`}
htmlFor={`dn-adhoc-stock-${idx}`}
className="text-xs"
>
Quantity
Pick from stock (optional)
</Label>
<Input
id={`dn-adhoc-item-qty-${idx}`}
value={it.quantity}
onChange={(e) =>
updateItem(idx, { quantity: e.target.value })
}
type="number"
min="1"
step="1"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-supplier-${idx}`}
className="text-xs"
>
Supplier
</Label>
<SupplierSearch
value={it.supplier}
onValueChange={(id) => updateItem(idx, { supplier: id })}
<StockItemSearch
stockItems={stockItems}
suppliers={suppliers}
placeholder="Pick supplier…"
value={it.stock_item_id}
onValueChange={(id) => void pickStockItem(idx, id)}
placeholder={
loadingLookups
? "Loading stock…"
: "Type a SKU or item name…"
}
disabled={loadingLookups || submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-sku-${idx}`}
className="text-xs"
>
SKU
</Label>
<Input
id={`dn-adhoc-item-sku-${idx}`}
value={it.sku}
onChange={(e) =>
updateItem(idx, { sku: e.target.value })
}
placeholder="optional"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-cost-${idx}`}
className="text-xs"
>
Cost (R)
</Label>
<Input
id={`dn-adhoc-item-cost-${idx}`}
value={it.cost_price}
onChange={(e) =>
updateItem(idx, { cost_price: e.target.value })
}
type="number"
step="0.01"
min="0"
placeholder="optional"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-selling-${idx}`}
className="text-xs"
>
Selling (R)
</Label>
<Input
id={`dn-adhoc-item-selling-${idx}`}
value={it.selling_price}
onChange={(e) =>
updateItem(idx, { selling_price: e.target.value })
}
type="number"
step="0.01"
min="0"
placeholder="optional"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-notes-${idx}`}
className="text-xs"
>
Notes
</Label>
<Input
id={`dn-adhoc-item-notes-${idx}`}
value={it.notes}
onChange={(e) =>
updateItem(idx, { notes: e.target.value })
}
placeholder="optional"
disabled={submitting}
/>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
<div className="sm:col-span-3">
<Label
htmlFor={`dn-adhoc-item-desc-${idx}`}
className="text-xs"
>
Description
</Label>
<Input
id={`dn-adhoc-item-desc-${idx}`}
value={it.description}
onChange={(e) =>
updateItem(idx, { description: e.target.value })
}
placeholder="e.g. 10x M8 bolts"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-qty-${idx}`}
className="text-xs"
>
Quantity
</Label>
<Input
id={`dn-adhoc-item-qty-${idx}`}
value={it.quantity}
onChange={(e) =>
updateItem(idx, { quantity: e.target.value })
}
type="number"
min="1"
step="1"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-supplier-${idx}`}
className="text-xs"
>
Supplier
</Label>
<SupplierSearch
value={it.supplier}
onValueChange={(id) => updateItem(idx, { supplier: id })}
suppliers={suppliers}
placeholder="Pick supplier…"
disabled={loadingLookups || submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-sku-${idx}`}
className="text-xs"
>
SKU
</Label>
<Input
id={`dn-adhoc-item-sku-${idx}`}
value={it.sku}
onChange={(e) =>
updateItem(idx, { sku: e.target.value })
}
placeholder="optional"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-cost-${idx}`}
className="text-xs"
>
Cost (R)
</Label>
<Input
id={`dn-adhoc-item-cost-${idx}`}
value={it.cost_price}
onChange={(e) =>
updateItem(idx, { cost_price: e.target.value })
}
type="number"
step="0.01"
min="0"
placeholder="optional"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-selling-${idx}`}
className="text-xs"
>
Selling (R)
</Label>
<Input
id={`dn-adhoc-item-selling-${idx}`}
value={it.selling_price}
onChange={(e) =>
updateItem(idx, { selling_price: e.target.value })
}
type="number"
step="0.01"
min="0"
placeholder="optional"
disabled={submitting}
/>
</div>
<div>
<Label
htmlFor={`dn-adhoc-item-notes-${idx}`}
className="text-xs"
>
Notes
</Label>
<Input
id={`dn-adhoc-item-notes-${idx}`}
value={it.notes}
onChange={(e) =>
updateItem(idx, { notes: e.target.value })
}
placeholder="optional"
disabled={submitting}
/>
</div>
</div>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={addItem}
disabled={submitting}
data-testid="dn-adhoc-add-item"
className="h-9 w-full border border-dashed text-muted-foreground hover:text-foreground"
aria-label="Add another item"
>
<Plus className="mr-1 h-3 w-3" />
Add another item
</Button>
</Fragment>
))}
</div>
</div>