ad no order title msg
This commit is contained in:
@@ -6,9 +6,21 @@ import type { OrderItemData } from "@/lib/types";
|
||||
// 6 color presets for customer grouping
|
||||
const CUSTOMER_COLORS = [
|
||||
{ border: "border-l-blue-500", bg: "bg-blue-50", text: "text-blue-800" },
|
||||
{ border: "border-l-emerald-500", bg: "bg-emerald-50", text: "text-emerald-800" },
|
||||
{ border: "border-l-violet-500", bg: "bg-violet-50", text: "text-violet-800" },
|
||||
{ border: "border-l-orange-500", bg: "bg-orange-50", text: "text-orange-800" },
|
||||
{
|
||||
border: "border-l-emerald-500",
|
||||
bg: "bg-emerald-50",
|
||||
text: "text-emerald-800",
|
||||
},
|
||||
{
|
||||
border: "border-l-violet-500",
|
||||
bg: "bg-violet-50",
|
||||
text: "text-violet-800",
|
||||
},
|
||||
{
|
||||
border: "border-l-orange-500",
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-800",
|
||||
},
|
||||
{ border: "border-l-teal-500", bg: "bg-teal-50", text: "text-teal-800" },
|
||||
{ border: "border-l-pink-500", bg: "bg-pink-50", text: "text-pink-800" },
|
||||
];
|
||||
@@ -60,13 +72,16 @@ function ItemEditForm({
|
||||
function update(field: string, value: string | number) {
|
||||
const next = { ...form, [field]: value };
|
||||
// Recompute prices
|
||||
next.netPrice = (next.initPrice + next.initPrice * next.taxRatio) * next.quantity;
|
||||
next.myNetPrice = (next.myPrice + next.myPrice * next.taxRatio) * next.quantity;
|
||||
next.netPrice =
|
||||
(next.initPrice + next.initPrice * next.taxRatio) * next.quantity;
|
||||
next.myNetPrice =
|
||||
(next.myPrice + next.myPrice * next.taxRatio) * next.quantity;
|
||||
next.finalPrice = next.netPrice;
|
||||
setForm(next);
|
||||
}
|
||||
|
||||
const inputClass = "border-2 border-border rounded-lg px-2 py-1.5 text-sm bg-bg text-fg focus:outline-none focus:border-accent w-full";
|
||||
const inputClass =
|
||||
"border-2 border-border rounded-lg px-2 py-1.5 text-sm bg-bg text-fg focus:outline-none focus:border-accent w-full";
|
||||
const labelClass = "text-xs text-muted font-medium mb-0.5 block";
|
||||
|
||||
return (
|
||||
@@ -81,36 +96,82 @@ function ItemEditForm({
|
||||
>
|
||||
<option value="">None</option>
|
||||
{customers.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Item Name</label>
|
||||
<input type="text" value={form.itemName} onChange={(e) => update("itemName", e.target.value)} className={inputClass} />
|
||||
<input
|
||||
type="text"
|
||||
value={form.itemName}
|
||||
onChange={(e) => update("itemName", e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Init Price</label>
|
||||
<input type="number" step="0.01" min="0" value={form.initPrice} onChange={(e) => update("initPrice", parseFloat(e.target.value) || 0)} className={inputClass} />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={form.initPrice}
|
||||
onChange={(e) =>
|
||||
update("initPrice", parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>My Price</label>
|
||||
<input type="number" step="0.01" min="0" value={form.myPrice} onChange={(e) => update("myPrice", parseFloat(e.target.value) || 0)} className={inputClass} />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={form.myPrice}
|
||||
onChange={(e) => update("myPrice", parseFloat(e.target.value) || 0)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Tax Ratio</label>
|
||||
<input type="number" step="0.01" min="0" value={form.taxRatio} onChange={(e) => update("taxRatio", parseFloat(e.target.value) || 0)} className={inputClass} />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={form.taxRatio}
|
||||
onChange={(e) =>
|
||||
update("taxRatio", parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Quantity</label>
|
||||
<input type="number" min="1" value={form.quantity} onChange={(e) => update("quantity", parseInt(e.target.value) || 1)} className={inputClass} />
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={form.quantity}
|
||||
onChange={(e) => update("quantity", parseInt(e.target.value) || 1)}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button type="button" onClick={() => onSave(form)} className="bg-accent text-accent-on px-4 py-1.5 rounded-lg text-sm font-bold hover:bg-accent-hover cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSave(form)}
|
||||
className="bg-accent text-accent-on px-4 py-1.5 rounded-lg text-sm font-bold hover:bg-accent-hover cursor-pointer"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" onClick={onCancel} className="bg-surface border-2 border-border px-4 py-1.5 rounded-lg text-sm font-medium text-muted hover:text-fg cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="bg-surface border-2 border-border px-4 py-1.5 rounded-lg text-sm font-medium text-muted hover:text-fg cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -143,13 +204,16 @@ function AddItemForm({
|
||||
|
||||
function handleAdd() {
|
||||
if (!form.itemName.trim()) return;
|
||||
const netPrice = (form.initPrice + form.initPrice * form.taxRatio) * form.quantity;
|
||||
const myNetPrice = (form.myPrice + form.myPrice * form.taxRatio) * form.quantity;
|
||||
const netPrice =
|
||||
(form.initPrice + form.initPrice * form.taxRatio) * form.quantity;
|
||||
const myNetPrice =
|
||||
(form.myPrice + form.myPrice * form.taxRatio) * form.quantity;
|
||||
onAdd({
|
||||
id: crypto.randomUUID(),
|
||||
orderId: "",
|
||||
customerId: form.customerId || null,
|
||||
customerName: customers.find((c) => c.id === form.customerId)?.name ?? null,
|
||||
customerName:
|
||||
customers.find((c) => c.id === form.customerId)?.name ?? null,
|
||||
itemName: form.itemName,
|
||||
initPrice: form.initPrice,
|
||||
myPrice: form.myPrice,
|
||||
@@ -159,10 +223,18 @@ function AddItemForm({
|
||||
myNetPrice,
|
||||
finalPrice: netPrice,
|
||||
});
|
||||
setForm({ customerId: customers[0]?.id ?? "", itemName: "", initPrice: 0, myPrice: 0, taxRatio: 0.16, quantity: 1 });
|
||||
setForm({
|
||||
customerId: customers[0]?.id ?? "",
|
||||
itemName: "",
|
||||
initPrice: 0,
|
||||
myPrice: 0,
|
||||
taxRatio: 0.16,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
||||
const inputClass = "border-2 border-border rounded-lg px-2 py-1.5 text-sm bg-bg text-fg focus:outline-none focus:border-accent w-full";
|
||||
const inputClass =
|
||||
"border-2 border-border rounded-lg px-2 py-1.5 text-sm bg-bg text-fg focus:outline-none focus:border-accent w-full";
|
||||
const labelClass = "text-xs text-muted font-medium mb-0.5 block";
|
||||
|
||||
return (
|
||||
@@ -171,34 +243,90 @@ function AddItemForm({
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<div>
|
||||
<label className={labelClass}>Customer</label>
|
||||
<select value={form.customerId} onChange={(e) => update("customerId", e.target.value)} className={inputClass} disabled={disabled}>
|
||||
<select
|
||||
value={form.customerId}
|
||||
onChange={(e) => update("customerId", e.target.value)}
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="">None</option>
|
||||
{customers.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Item Name</label>
|
||||
<input type="text" value={form.itemName} onChange={(e) => update("itemName", e.target.value)} placeholder="Product name" className={inputClass} disabled={disabled} />
|
||||
<input
|
||||
type="text"
|
||||
value={form.itemName}
|
||||
onChange={(e) => update("itemName", e.target.value)}
|
||||
placeholder="Product name"
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Init Price</label>
|
||||
<input type="number" step="0.01" min="0" value={form.initPrice} onChange={(e) => update("initPrice", parseFloat(e.target.value) || 0)} className={inputClass} disabled={disabled} />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={form.initPrice}
|
||||
onChange={(e) =>
|
||||
update("initPrice", parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>My Price</label>
|
||||
<input type="number" step="0.01" min="0" value={form.myPrice} onChange={(e) => update("myPrice", parseFloat(e.target.value) || 0)} className={inputClass} disabled={disabled} />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={form.myPrice}
|
||||
onChange={(e) => update("myPrice", parseFloat(e.target.value) || 0)}
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Tax / Qty</label>
|
||||
<div className="flex gap-1">
|
||||
<input type="number" step="0.01" min="0" value={form.taxRatio} onChange={(e) => update("taxRatio", parseFloat(e.target.value) || 0)} className={inputClass} disabled={disabled} />
|
||||
<input type="number" min="1" value={form.quantity} onChange={(e) => update("quantity", parseInt(e.target.value) || 1)} className={inputClass} disabled={disabled} />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={form.taxRatio}
|
||||
onChange={(e) =>
|
||||
update("taxRatio", parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={form.quantity}
|
||||
onChange={(e) =>
|
||||
update("quantity", parseInt(e.target.value) || 1)
|
||||
}
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button type="button" onClick={handleAdd} disabled={disabled || !form.itemName.trim()} className="bg-accent text-accent-on px-4 py-1.5 rounded-lg text-sm font-bold hover:bg-accent-hover disabled:opacity-50 cursor-pointer w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={disabled || !form.itemName.trim()}
|
||||
className="bg-accent text-accent-on px-4 py-1.5 rounded-lg text-sm font-bold hover:bg-accent-hover disabled:opacity-50 cursor-pointer w-full"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
@@ -243,26 +371,54 @@ export function ItemTable({
|
||||
}
|
||||
|
||||
// Render a single item row (desktop table row)
|
||||
function TableRow({ item, colorIndex }: { item: OrderItemData; colorIndex: number | null }) {
|
||||
function TableRow({
|
||||
item,
|
||||
colorIndex,
|
||||
}: {
|
||||
item: OrderItemData;
|
||||
colorIndex: number | null;
|
||||
}) {
|
||||
const color = colorIndex !== null ? getColor(colorIndex) : null;
|
||||
const isEditing = editingId === item.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={`border-b border-border/50 hover:bg-surface/30 transition-colors ${color ? `border-l-4 ${color.border} ${color.bg}` : ""}`}>
|
||||
<td className="px-3 py-2 text-sm font-medium text-fg">{item.itemName || "—"}</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right">${item.initPrice.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right">${item.myPrice.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-center">{item.quantity}</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right font-medium">${item.netPrice.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right font-bold">${item.finalPrice.toFixed(2)}</td>
|
||||
<tr
|
||||
className={`border-b border-border/50 hover:bg-surface/30 transition-colors ${color ? `border-l-4 ${color.border} ${color.bg}` : ""}`}
|
||||
>
|
||||
<td className="px-3 py-2 text-sm font-medium text-fg">
|
||||
{item.itemName || "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right">
|
||||
${item.initPrice.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right">
|
||||
${item.myPrice.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-center">
|
||||
{item.quantity}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right font-medium">
|
||||
${item.myNetPrice.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm font-mono text-right font-bold">
|
||||
${item.finalPrice.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
{!disabled && (
|
||||
<div className="flex gap-1 justify-end">
|
||||
<button type="button" onClick={() => setEditingId(isEditing ? null : item.id)} className="text-xs text-accent hover:text-accent-hover font-medium px-2 py-1 rounded cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingId(isEditing ? null : item.id)}
|
||||
className="text-xs text-accent hover:text-accent-hover font-medium px-2 py-1 rounded cursor-pointer"
|
||||
>
|
||||
{isEditing ? "Cancel" : "Edit"}
|
||||
</button>
|
||||
<button type="button" onClick={() => handleRemove(item.id)} className="text-xs text-danger hover:text-red-700 font-medium px-2 py-1 rounded cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(item.id)}
|
||||
className="text-xs text-danger hover:text-red-700 font-medium px-2 py-1 rounded cursor-pointer"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
@@ -272,7 +428,12 @@ export function ItemTable({
|
||||
{isEditing && (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-3 py-0">
|
||||
<ItemEditForm item={item} customers={customers} onSave={handleEditSave} onCancel={() => setEditingId(null)} />
|
||||
<ItemEditForm
|
||||
item={item}
|
||||
customers={customers}
|
||||
onSave={handleEditSave}
|
||||
onCancel={() => setEditingId(null)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -281,23 +442,37 @@ export function ItemTable({
|
||||
}
|
||||
|
||||
// Render a single item card (mobile)
|
||||
function MobileCard({ item, colorIndex }: { item: OrderItemData; colorIndex: number | null }) {
|
||||
function MobileCard({
|
||||
item,
|
||||
colorIndex,
|
||||
}: {
|
||||
item: OrderItemData;
|
||||
colorIndex: number | null;
|
||||
}) {
|
||||
const color = colorIndex !== null ? getColor(colorIndex) : null;
|
||||
const isEditing = editingId === item.id;
|
||||
|
||||
return (
|
||||
<div className={`border-2 border-border rounded-xl overflow-hidden ${color ? `border-l-4 ${color.border}` : ""}`}>
|
||||
<div
|
||||
className={`border-2 border-border rounded-xl overflow-hidden ${color ? `border-l-4 ${color.border}` : ""}`}
|
||||
>
|
||||
<div className={`p-3 ${color?.bg ?? "bg-white"}`}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-bold text-fg truncate">{item.itemName || "—"}</p>
|
||||
<p className="text-sm font-bold text-fg truncate">
|
||||
{item.itemName || "—"}
|
||||
</p>
|
||||
{colorIndex !== null && (
|
||||
<p className={`text-xs font-medium ${color?.text ?? "text-muted"}`}>
|
||||
<p
|
||||
className={`text-xs font-medium ${color?.text ?? "text-muted"}`}
|
||||
>
|
||||
{getCustomerName(item.customerId!)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-bold font-mono text-fg whitespace-nowrap">${item.finalPrice.toFixed(2)}</p>
|
||||
<p className="text-sm font-bold font-mono text-fg whitespace-nowrap">
|
||||
${item.finalPrice.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 mt-2 text-xs">
|
||||
<div>
|
||||
@@ -315,10 +490,18 @@ export function ItemTable({
|
||||
</div>
|
||||
{!disabled && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button type="button" onClick={() => setEditingId(isEditing ? null : item.id)} className="text-xs text-accent hover:text-accent-hover font-medium cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingId(isEditing ? null : item.id)}
|
||||
className="text-xs text-accent hover:text-accent-hover font-medium cursor-pointer"
|
||||
>
|
||||
{isEditing ? "Cancel" : "Edit"}
|
||||
</button>
|
||||
<button type="button" onClick={() => handleRemove(item.id)} className="text-xs text-danger hover:text-red-700 font-medium cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(item.id)}
|
||||
className="text-xs text-danger hover:text-red-700 font-medium cursor-pointer"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
@@ -326,7 +509,12 @@ export function ItemTable({
|
||||
</div>
|
||||
{isEditing && (
|
||||
<div className="p-3 border-t border-border">
|
||||
<ItemEditForm item={item} customers={customers} onSave={handleEditSave} onCancel={() => setEditingId(null)} />
|
||||
<ItemEditForm
|
||||
item={item}
|
||||
customers={customers}
|
||||
onSave={handleEditSave}
|
||||
onCancel={() => setEditingId(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -334,39 +522,76 @@ export function ItemTable({
|
||||
}
|
||||
|
||||
// Render items for a customer group
|
||||
function CustomerGroup({ customerId, groupItems: gi }: { customerId: string; groupItems: OrderItemData[] }) {
|
||||
function CustomerGroup({
|
||||
customerId,
|
||||
groupItems: gi,
|
||||
}: {
|
||||
customerId: string;
|
||||
groupItems: OrderItemData[];
|
||||
}) {
|
||||
const colorIndex = colorMap[customerId] ?? 0;
|
||||
const color = getColor(colorIndex);
|
||||
const name = getCustomerName(customerId);
|
||||
const totalQty = gi.reduce((s, i) => s + i.quantity, 0);
|
||||
const totalNet = gi.reduce((s, i) => s + i.myNetPrice, 0);
|
||||
const totalFinal = gi.reduce((s, i) => s + i.finalPrice, 0);
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-t-lg ${color.bg} border-l-4 ${color.border}`}>
|
||||
<div
|
||||
className={`flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-1.5 rounded-t-lg ${color.bg} border-l-4 ${color.border}`}
|
||||
>
|
||||
<span className={`text-sm font-bold ${color.text}`}>{name}</span>
|
||||
<span className="text-xs text-muted">({gi.length} item{gi.length !== 1 ? "s" : ""})</span>
|
||||
<span className="text-xs text-muted">
|
||||
{totalQty} unit{totalQty !== 1 ? "s" : ""} ({gi.length} line{gi.length !== 1 ? "s" : ""})
|
||||
</span>
|
||||
<span className="text-xs font-mono text-muted">
|
||||
Net: <span className="font-medium text-fg">${totalNet.toFixed(2)}</span>
|
||||
</span>
|
||||
<span className="text-xs font-mono text-muted">
|
||||
Final: <span className="font-bold text-fg">${totalFinal.toFixed(2)}</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* Desktop table */}
|
||||
<div className="hidden sm:block border-x border-b border-border rounded-b-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-surface/50">
|
||||
<th className="text-left px-3 py-1.5 text-xs font-medium text-muted">Item</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Init Price</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">My Price</th>
|
||||
<th className="text-center px-3 py-1.5 text-xs font-medium text-muted">Qty</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Net</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Final</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Actions</th>
|
||||
<th className="text-left px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Item
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Init Price
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
My Price
|
||||
</th>
|
||||
<th className="text-center px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Qty
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Net
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Final
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{gi.map((item) => <TableRow key={item.id} item={item} colorIndex={colorIndex} />)}
|
||||
{gi.map((item) => (
|
||||
<TableRow key={item.id} item={item} colorIndex={colorIndex} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Mobile cards */}
|
||||
<div className="sm:hidden flex flex-col gap-2 mt-1">
|
||||
{gi.map((item) => <MobileCard key={item.id} item={item} colorIndex={colorIndex} />)}
|
||||
{gi.map((item) => (
|
||||
<MobileCard key={item.id} item={item} colorIndex={colorIndex} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -376,7 +601,11 @@ export function ItemTable({
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Add item form */}
|
||||
{!disabled && (
|
||||
<AddItemForm customers={customers} onAdd={handleAdd} disabled={disabled} />
|
||||
<AddItemForm
|
||||
customers={customers}
|
||||
onAdd={handleAdd}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Items grouped by customer */}
|
||||
@@ -388,39 +617,75 @@ export function ItemTable({
|
||||
<>
|
||||
{/* Customer groups */}
|
||||
{Object.entries(groups).map(([customerId, gi]) => (
|
||||
<CustomerGroup key={customerId} customerId={customerId} groupItems={gi} />
|
||||
<CustomerGroup
|
||||
key={customerId}
|
||||
customerId={customerId}
|
||||
groupItems={gi}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Ungrouped items */}
|
||||
{ungrouped.length > 0 && (
|
||||
{ungrouped.length > 0 && (() => {
|
||||
const uQty = ungrouped.reduce((s, i) => s + i.quantity, 0);
|
||||
const uNet = ungrouped.reduce((s, i) => s + i.netPrice, 0);
|
||||
const uFinal = ungrouped.reduce((s, i) => s + i.finalPrice, 0);
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-t-lg bg-gray-50 border-l-4 border-l-gray-400">
|
||||
<span className="text-sm font-bold text-gray-700">Unassigned</span>
|
||||
<span className="text-xs text-muted">({ungrouped.length} item{ungrouped.length !== 1 ? "s" : ""})</span>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-1.5 rounded-t-lg bg-gray-50 border-l-4 border-l-gray-400">
|
||||
<span className="text-sm font-bold text-gray-700">
|
||||
Unassigned
|
||||
</span>
|
||||
<span className="text-xs text-muted">
|
||||
{uQty} unit{uQty !== 1 ? "s" : ""} ({ungrouped.length} line{ungrouped.length !== 1 ? "s" : ""})
|
||||
</span>
|
||||
<span className="text-xs font-mono text-muted">
|
||||
Net: <span className="font-medium text-fg">${uNet.toFixed(2)}</span>
|
||||
</span>
|
||||
<span className="text-xs font-mono text-muted">
|
||||
Final: <span className="font-bold text-fg">${uFinal.toFixed(2)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden sm:block border-x border-b border-border rounded-b-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-surface/50">
|
||||
<th className="text-left px-3 py-1.5 text-xs font-medium text-muted">Item</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Init Price</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">My Price</th>
|
||||
<th className="text-center px-3 py-1.5 text-xs font-medium text-muted">Qty</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Net</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Final</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Actions</th>
|
||||
<th className="text-left px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Item
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Init Price
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
My Price
|
||||
</th>
|
||||
<th className="text-center px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Qty
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Net
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Final
|
||||
</th>
|
||||
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ungrouped.map((item) => <TableRow key={item.id} item={item} colorIndex={null} />)}
|
||||
{ungrouped.map((item) => (
|
||||
<TableRow key={item.id} item={item} colorIndex={null} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sm:hidden flex flex-col gap-2 mt-1">
|
||||
{ungrouped.map((item) => <MobileCard key={item.id} item={item} colorIndex={null} />)}
|
||||
{ungrouped.map((item) => (
|
||||
<MobileCard key={item.id} item={item} colorIndex={null} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
); })()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@ export function OrderForm({
|
||||
const [customerInput, setCustomerInput] = useState("");
|
||||
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
||||
const [actionSuccess, setActionSuccess] = useState(false);
|
||||
const [titleError, setTitleError] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleAddCustomer() {
|
||||
@@ -71,6 +72,10 @@ export function OrderForm({
|
||||
|
||||
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
if (!title.trim()) {
|
||||
setTitleError(true);
|
||||
return;
|
||||
}
|
||||
const formData = new FormData(e.currentTarget);
|
||||
formData.set("title", title);
|
||||
formData.set("status", status);
|
||||
@@ -118,7 +123,7 @@ export function OrderForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{/* Main content — left 2/3 on desktop */}
|
||||
<div className="lg:col-span-2 flex flex-col gap-6">
|
||||
{/* Title + Status row */}
|
||||
@@ -131,11 +136,25 @@ export function OrderForm({
|
||||
id="order-title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
if (titleError) setTitleError(false);
|
||||
}}
|
||||
placeholder="Order title"
|
||||
className={inputClass}
|
||||
className={`${inputClass} ${titleError ? "border-danger" : ""}`}
|
||||
disabled={isDisabled}
|
||||
aria-invalid={titleError}
|
||||
aria-describedby={titleError ? "title-error" : undefined}
|
||||
/>
|
||||
{titleError && (
|
||||
<p
|
||||
id="title-error"
|
||||
className="text-danger text-xs font-medium mt-1"
|
||||
role="alert"
|
||||
>
|
||||
Order title is required. Please enter a title before creating the order.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:w-48">
|
||||
<label htmlFor="order-status" className={labelClass}>
|
||||
|
||||
@@ -12,6 +12,7 @@ export function SummaryPanel({
|
||||
status: OrderStatus;
|
||||
customerCount: number;
|
||||
}) {
|
||||
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalOrderValue = items.reduce((sum, item) => sum + item.finalPrice, 0);
|
||||
const myNetTotal = items.reduce((sum, item) => sum + item.myNetPrice, 0);
|
||||
|
||||
@@ -34,7 +35,7 @@ export function SummaryPanel({
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted">Items</span>
|
||||
<span className="font-mono font-medium">{items.length}</span>
|
||||
<span className="font-mono font-medium">{totalItems}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted">Customers</span>
|
||||
|
||||
Reference in New Issue
Block a user