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:
parent
e99850c646
commit
e900772faa
1 changed files with 196 additions and 195 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue