ad no order title msg

This commit is contained in:
2026-06-04 01:05:21 +03:00
parent 0c87eaef46
commit 3f2aeeab40
9 changed files with 391 additions and 192 deletions

View File

@@ -0,0 +1,28 @@
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(npm install:*)",
"Bash(node:*)",
"Bash(npm approve-scripts:*)",
"Bash(npx prisma:*)",
"Bash(node -e \"console.log\\(require\\(''crypto''\\).randomBytes\\(32\\).toString\\(''hex''\\)\\)\")",
"WebSearch",
"WebFetch(domain:www.prisma.org)",
"Bash(find C:UsersyuccDocumentsRepositoriesprojects-2026nextjsiherb-openclaudenode_modulesprisma -name *.md -maxdepth 2)",
"Bash(tasklist)",
"Bash(rm -rf \"C:\\\\Users\\\\yucc\\\\Documents\\\\Repositories\\\\projects-2026\\\\nextjs\\\\iherb-openclaude/.next\")",
"Bash(npm run:*)",
"Bash(npx next:*)",
"Bash(tee /tmp/build-output.txt)",
"Bash(echo \"EXIT: $?\")",
"Bash(echo \"EXIT_CODE=$?\")",
"Bash(taskkill /F /IM \"node.exe\")",
"Bash(taskkill /F /IM \"nxnode.bin\")",
"Bash(taskkill //F //PID 40132)",
"Bash(taskkill //F //PID 35372)",
"Bash(taskkill //F //PID 21080)",
"Bash(taskkill //F //PID 29760)"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -6,9 +6,21 @@ import type { OrderItemData } from "@/lib/types";
// 6 color presets for customer grouping // 6 color presets for customer grouping
const CUSTOMER_COLORS = [ const CUSTOMER_COLORS = [
{ border: "border-l-blue-500", bg: "bg-blue-50", text: "text-blue-800" }, { 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-emerald-500",
{ border: "border-l-orange-500", bg: "bg-orange-50", text: "text-orange-800" }, 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-teal-500", bg: "bg-teal-50", text: "text-teal-800" },
{ border: "border-l-pink-500", bg: "bg-pink-50", text: "text-pink-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) { function update(field: string, value: string | number) {
const next = { ...form, [field]: value }; const next = { ...form, [field]: value };
// Recompute prices // Recompute prices
next.netPrice = (next.initPrice + next.initPrice * next.taxRatio) * next.quantity; next.netPrice =
next.myNetPrice = (next.myPrice + next.myPrice * next.taxRatio) * next.quantity; (next.initPrice + next.initPrice * next.taxRatio) * next.quantity;
next.myNetPrice =
(next.myPrice + next.myPrice * next.taxRatio) * next.quantity;
next.finalPrice = next.netPrice; next.finalPrice = next.netPrice;
setForm(next); 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"; const labelClass = "text-xs text-muted font-medium mb-0.5 block";
return ( return (
@@ -81,36 +96,82 @@ function ItemEditForm({
> >
<option value="">None</option> <option value="">None</option>
{customers.map((c) => ( {customers.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option> <option key={c.id} value={c.id}>
{c.name}
</option>
))} ))}
</select> </select>
</div> </div>
<div> <div>
<label className={labelClass}>Item Name</label> <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>
<div> <div>
<label className={labelClass}>Init Price</label> <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>
<div> <div>
<label className={labelClass}>My Price</label> <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>
<div> <div>
<label className={labelClass}>Tax Ratio</label> <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>
<div> <div>
<label className={labelClass}>Quantity</label> <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> </div>
<div className="flex gap-2 mt-3"> <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 Save
</button> </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 Cancel
</button> </button>
</div> </div>
@@ -143,13 +204,16 @@ function AddItemForm({
function handleAdd() { function handleAdd() {
if (!form.itemName.trim()) return; if (!form.itemName.trim()) return;
const netPrice = (form.initPrice + form.initPrice * form.taxRatio) * form.quantity; const netPrice =
const myNetPrice = (form.myPrice + form.myPrice * form.taxRatio) * form.quantity; (form.initPrice + form.initPrice * form.taxRatio) * form.quantity;
const myNetPrice =
(form.myPrice + form.myPrice * form.taxRatio) * form.quantity;
onAdd({ onAdd({
id: crypto.randomUUID(), id: crypto.randomUUID(),
orderId: "", orderId: "",
customerId: form.customerId || null, 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, itemName: form.itemName,
initPrice: form.initPrice, initPrice: form.initPrice,
myPrice: form.myPrice, myPrice: form.myPrice,
@@ -159,10 +223,18 @@ function AddItemForm({
myNetPrice, myNetPrice,
finalPrice: netPrice, 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"; const labelClass = "text-xs text-muted font-medium mb-0.5 block";
return ( return (
@@ -171,34 +243,90 @@ function AddItemForm({
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
<div> <div>
<label className={labelClass}>Customer</label> <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> <option value="">None</option>
{customers.map((c) => ( {customers.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option> <option key={c.id} value={c.id}>
{c.name}
</option>
))} ))}
</select> </select>
</div> </div>
<div> <div>
<label className={labelClass}>Item Name</label> <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>
<div> <div>
<label className={labelClass}>Init Price</label> <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>
<div> <div>
<label className={labelClass}>My Price</label> <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>
<div> <div>
<label className={labelClass}>Tax / Qty</label> <label className={labelClass}>Tax / Qty</label>
<div className="flex gap-1"> <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
<input type="number" min="1" value={form.quantity} onChange={(e) => update("quantity", parseInt(e.target.value) || 1)} className={inputClass} disabled={disabled} /> 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> </div>
<div className="flex items-end"> <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 + Add
</button> </button>
</div> </div>
@@ -243,26 +371,54 @@ export function ItemTable({
} }
// Render a single item row (desktop table row) // 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 color = colorIndex !== null ? getColor(colorIndex) : null;
const isEditing = editingId === item.id; const isEditing = editingId === item.id;
return ( return (
<> <>
<tr className={`border-b border-border/50 hover:bg-surface/30 transition-colors ${color ? `border-l-4 ${color.border} ${color.bg}` : ""}`}> <tr
<td className="px-3 py-2 text-sm font-medium text-fg">{item.itemName || ""}</td> 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-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-medium text-fg">
<td className="px-3 py-2 text-sm font-mono text-center">{item.quantity}</td> {item.itemName || "—"}
<td className="px-3 py-2 text-sm font-mono text-right font-medium">${item.netPrice.toFixed(2)}</td> </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-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"> <td className="px-3 py-2 text-right">
{!disabled && ( {!disabled && (
<div className="flex gap-1 justify-end"> <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"} {isEditing ? "Cancel" : "Edit"}
</button> </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 Remove
</button> </button>
</div> </div>
@@ -272,7 +428,12 @@ export function ItemTable({
{isEditing && ( {isEditing && (
<tr> <tr>
<td colSpan={7} className="px-3 py-0"> <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> </td>
</tr> </tr>
)} )}
@@ -281,23 +442,37 @@ export function ItemTable({
} }
// Render a single item card (mobile) // 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 color = colorIndex !== null ? getColor(colorIndex) : null;
const isEditing = editingId === item.id; const isEditing = editingId === item.id;
return ( 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={`p-3 ${color?.bg ?? "bg-white"}`}>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0"> <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 && ( {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!)} {getCustomerName(item.customerId!)}
</p> </p>
)} )}
</div> </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>
<div className="grid grid-cols-3 gap-2 mt-2 text-xs"> <div className="grid grid-cols-3 gap-2 mt-2 text-xs">
<div> <div>
@@ -315,10 +490,18 @@ export function ItemTable({
</div> </div>
{!disabled && ( {!disabled && (
<div className="flex gap-2 mt-2"> <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"} {isEditing ? "Cancel" : "Edit"}
</button> </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 Remove
</button> </button>
</div> </div>
@@ -326,7 +509,12 @@ export function ItemTable({
</div> </div>
{isEditing && ( {isEditing && (
<div className="p-3 border-t border-border"> <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>
)} )}
</div> </div>
@@ -334,39 +522,76 @@ export function ItemTable({
} }
// Render items for a customer group // 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 colorIndex = colorMap[customerId] ?? 0;
const color = getColor(colorIndex); const color = getColor(colorIndex);
const name = getCustomerName(customerId); 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 ( return (
<div className="mb-4"> <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-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> </div>
{/* Desktop table */} {/* Desktop table */}
<div className="hidden sm:block border-x border-b border-border rounded-b-lg overflow-hidden"> <div className="hidden sm:block border-x border-b border-border rounded-b-lg overflow-hidden">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="bg-surface/50"> <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-left px-3 py-1.5 text-xs font-medium text-muted">
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Init Price</th> Item
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">My Price</th> </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">
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Net</th> Init Price
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Final</th> </th>
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Actions</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> </tr>
</thead> </thead>
<tbody> <tbody>
{gi.map((item) => <TableRow key={item.id} item={item} colorIndex={colorIndex} />)} {gi.map((item) => (
<TableRow key={item.id} item={item} colorIndex={colorIndex} />
))}
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Mobile cards */} {/* Mobile cards */}
<div className="sm:hidden flex flex-col gap-2 mt-1"> <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>
</div> </div>
); );
@@ -376,7 +601,11 @@ export function ItemTable({
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Add item form */} {/* Add item form */}
{!disabled && ( {!disabled && (
<AddItemForm customers={customers} onAdd={handleAdd} disabled={disabled} /> <AddItemForm
customers={customers}
onAdd={handleAdd}
disabled={disabled}
/>
)} )}
{/* Items grouped by customer */} {/* Items grouped by customer */}
@@ -388,39 +617,75 @@ export function ItemTable({
<> <>
{/* Customer groups */} {/* Customer groups */}
{Object.entries(groups).map(([customerId, gi]) => ( {Object.entries(groups).map(([customerId, gi]) => (
<CustomerGroup key={customerId} customerId={customerId} groupItems={gi} /> <CustomerGroup
key={customerId}
customerId={customerId}
groupItems={gi}
/>
))} ))}
{/* Ungrouped items */} {/* 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="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"> <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-sm font-bold text-gray-700">
<span className="text-xs text-muted">({ungrouped.length} item{ungrouped.length !== 1 ? "s" : ""})</span> 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>
<div className="hidden sm:block border-x border-b border-border rounded-b-lg overflow-hidden"> <div className="hidden sm:block border-x border-b border-border rounded-b-lg overflow-hidden">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="bg-surface/50"> <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-left px-3 py-1.5 text-xs font-medium text-muted">
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Init Price</th> Item
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">My Price</th> </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">
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Net</th> Init Price
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Final</th> </th>
<th className="text-right px-3 py-1.5 text-xs font-medium text-muted">Actions</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> </tr>
</thead> </thead>
<tbody> <tbody>
{ungrouped.map((item) => <TableRow key={item.id} item={item} colorIndex={null} />)} {ungrouped.map((item) => (
<TableRow key={item.id} item={item} colorIndex={null} />
))}
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="sm:hidden flex flex-col gap-2 mt-1"> <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> </div>
)} ); })()}
</> </>
)} )}
</div> </div>

View File

@@ -47,6 +47,7 @@ export function OrderForm({
const [customerInput, setCustomerInput] = useState(""); const [customerInput, setCustomerInput] = useState("");
const [actionMessage, setActionMessage] = useState<string | null>(null); const [actionMessage, setActionMessage] = useState<string | null>(null);
const [actionSuccess, setActionSuccess] = useState(false); const [actionSuccess, setActionSuccess] = useState(false);
const [titleError, setTitleError] = useState(false);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
function handleAddCustomer() { function handleAddCustomer() {
@@ -71,6 +72,10 @@ export function OrderForm({
function handleSubmit(e: FormEvent<HTMLFormElement>) { function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault(); e.preventDefault();
if (!title.trim()) {
setTitleError(true);
return;
}
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
formData.set("title", title); formData.set("title", title);
formData.set("status", status); formData.set("status", status);
@@ -118,7 +123,7 @@ export function OrderForm({
</div> </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 */} {/* Main content — left 2/3 on desktop */}
<div className="lg:col-span-2 flex flex-col gap-6"> <div className="lg:col-span-2 flex flex-col gap-6">
{/* Title + Status row */} {/* Title + Status row */}
@@ -131,11 +136,25 @@ export function OrderForm({
id="order-title" id="order-title"
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => {
setTitle(e.target.value);
if (titleError) setTitleError(false);
}}
placeholder="Order title" placeholder="Order title"
className={inputClass} className={`${inputClass} ${titleError ? "border-danger" : ""}`}
disabled={isDisabled} 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>
<div className="flex flex-col sm:w-48"> <div className="flex flex-col sm:w-48">
<label htmlFor="order-status" className={labelClass}> <label htmlFor="order-status" className={labelClass}>

View File

@@ -12,6 +12,7 @@ export function SummaryPanel({
status: OrderStatus; status: OrderStatus;
customerCount: number; customerCount: number;
}) { }) {
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
const totalOrderValue = items.reduce((sum, item) => sum + item.finalPrice, 0); const totalOrderValue = items.reduce((sum, item) => sum + item.finalPrice, 0);
const myNetTotal = items.reduce((sum, item) => sum + item.myNetPrice, 0); const myNetTotal = items.reduce((sum, item) => sum + item.myNetPrice, 0);
@@ -34,7 +35,7 @@ export function SummaryPanel({
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted">Items</span> <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>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted">Customers</span> <span className="text-sm text-muted">Customers</span>

114
package-lock.json generated
View File

@@ -1058,9 +1058,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1077,9 +1074,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1096,9 +1090,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1115,9 +1106,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1134,9 +1122,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1153,9 +1138,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1172,9 +1154,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1191,9 +1170,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1210,9 +1186,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1235,9 +1208,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1260,9 +1230,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1285,9 +1252,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1310,9 +1274,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1335,9 +1296,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1360,9 +1318,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1385,9 +1340,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1603,9 +1555,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1622,9 +1571,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1641,9 +1587,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1660,9 +1603,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1992,9 +1932,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2012,9 +1949,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2032,9 +1966,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2052,9 +1983,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2605,9 +2533,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2622,9 +2547,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2639,9 +2561,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2656,9 +2575,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2673,9 +2589,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2690,9 +2603,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2707,9 +2617,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2724,9 +2631,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2741,9 +2645,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2758,9 +2659,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5619,9 +5517,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5643,9 +5538,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5667,9 +5559,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5691,9 +5580,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [

Binary file not shown.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/ol-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB