diff --git a/.openclaude/settings.local.json b/.openclaude/settings.local.json new file mode 100644 index 0000000..8051d3c --- /dev/null +++ b/.openclaude/settings.local.json @@ -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)" + ] + } +} diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..985b5fa 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/components/ItemTable.tsx b/components/ItemTable.tsx index 452cc73..d8468fd 100644 --- a/components/ItemTable.tsx +++ b/components/ItemTable.tsx @@ -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({ > {customers.map((c) => ( - + ))}
- update("itemName", e.target.value)} className={inputClass} /> + update("itemName", e.target.value)} + className={inputClass} + />
- update("initPrice", parseFloat(e.target.value) || 0)} className={inputClass} /> + + update("initPrice", parseFloat(e.target.value) || 0) + } + className={inputClass} + />
- update("myPrice", parseFloat(e.target.value) || 0)} className={inputClass} /> + update("myPrice", parseFloat(e.target.value) || 0)} + className={inputClass} + />
- update("taxRatio", parseFloat(e.target.value) || 0)} className={inputClass} /> + + update("taxRatio", parseFloat(e.target.value) || 0) + } + className={inputClass} + />
- update("quantity", parseInt(e.target.value) || 1)} className={inputClass} /> + update("quantity", parseInt(e.target.value) || 1)} + className={inputClass} + />
- -
@@ -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({
- update("customerId", e.target.value)} + className={inputClass} + disabled={disabled} + > {customers.map((c) => ( - + ))}
- update("itemName", e.target.value)} placeholder="Product name" className={inputClass} disabled={disabled} /> + update("itemName", e.target.value)} + placeholder="Product name" + className={inputClass} + disabled={disabled} + />
- update("initPrice", parseFloat(e.target.value) || 0)} className={inputClass} disabled={disabled} /> + + update("initPrice", parseFloat(e.target.value) || 0) + } + className={inputClass} + disabled={disabled} + />
- update("myPrice", parseFloat(e.target.value) || 0)} className={inputClass} disabled={disabled} /> + update("myPrice", parseFloat(e.target.value) || 0)} + className={inputClass} + disabled={disabled} + />
- update("taxRatio", parseFloat(e.target.value) || 0)} className={inputClass} disabled={disabled} /> - update("quantity", parseInt(e.target.value) || 1)} className={inputClass} disabled={disabled} /> + + update("taxRatio", parseFloat(e.target.value) || 0) + } + className={inputClass} + disabled={disabled} + /> + + update("quantity", parseInt(e.target.value) || 1) + } + className={inputClass} + disabled={disabled} + />
-
@@ -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 ( <> - - {item.itemName || "—"} - ${item.initPrice.toFixed(2)} - ${item.myPrice.toFixed(2)} - {item.quantity} - ${item.netPrice.toFixed(2)} - ${item.finalPrice.toFixed(2)} + + + {item.itemName || "—"} + + + ${item.initPrice.toFixed(2)} + + + ${item.myPrice.toFixed(2)} + + + {item.quantity} + + + ${item.myNetPrice.toFixed(2)} + + + ${item.finalPrice.toFixed(2)} + {!disabled && (
- -
@@ -272,7 +428,12 @@ export function ItemTable({ {isEditing && ( - setEditingId(null)} /> + setEditingId(null)} + /> )} @@ -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 ( -
+
-

{item.itemName || "—"}

+

+ {item.itemName || "—"} +

{colorIndex !== null && ( -

+

{getCustomerName(item.customerId!)}

)}
-

${item.finalPrice.toFixed(2)}

+

+ ${item.finalPrice.toFixed(2)} +

@@ -315,10 +490,18 @@ export function ItemTable({
{!disabled && (
- -
@@ -326,7 +509,12 @@ export function ItemTable({
{isEditing && (
- setEditingId(null)} /> + setEditingId(null)} + />
)}
@@ -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 (
-
+
{name} - ({gi.length} item{gi.length !== 1 ? "s" : ""}) + + {totalQty} unit{totalQty !== 1 ? "s" : ""} ({gi.length} line{gi.length !== 1 ? "s" : ""}) + + + Net: ${totalNet.toFixed(2)} + + + Final: ${totalFinal.toFixed(2)} +
{/* Desktop table */}
- - - - - - - + + + + + + + - {gi.map((item) => )} + {gi.map((item) => ( + + ))}
ItemInit PriceMy PriceQtyNetFinalActions + Item + + Init Price + + My Price + + Qty + + Net + + Final + + Actions +
{/* Mobile cards */}
- {gi.map((item) => )} + {gi.map((item) => ( + + ))}
); @@ -376,7 +601,11 @@ export function ItemTable({
{/* Add item form */} {!disabled && ( - + )} {/* Items grouped by customer */} @@ -388,39 +617,75 @@ export function ItemTable({ <> {/* Customer groups */} {Object.entries(groups).map(([customerId, 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 (
-
- Unassigned - ({ungrouped.length} item{ungrouped.length !== 1 ? "s" : ""}) +
+ + Unassigned + + + {uQty} unit{uQty !== 1 ? "s" : ""} ({ungrouped.length} line{ungrouped.length !== 1 ? "s" : ""}) + + + Net: ${uNet.toFixed(2)} + + + Final: ${uFinal.toFixed(2)} +
- - - - - - - + + + + + + + - {ungrouped.map((item) => )} + {ungrouped.map((item) => ( + + ))}
ItemInit PriceMy PriceQtyNetFinalActions + Item + + Init Price + + My Price + + Qty + + Net + + Final + + Actions +
- {ungrouped.map((item) => )} + {ungrouped.map((item) => ( + + ))}
- )} + ); })()} )}
diff --git a/components/OrderForm.tsx b/components/OrderForm.tsx index 39f07d7..be3a555 100644 --- a/components/OrderForm.tsx +++ b/components/OrderForm.tsx @@ -47,6 +47,7 @@ export function OrderForm({ const [customerInput, setCustomerInput] = useState(""); const [actionMessage, setActionMessage] = useState(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) { 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({
)} -
+
{/* Main content — left 2/3 on desktop */}
{/* 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 && ( + + )}
Items - {items.length} + {totalItems}
Customers diff --git a/package-lock.json b/package-lock.json index 1ba11ea..0b911e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1058,9 +1058,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1077,9 +1074,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1096,9 +1090,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1115,9 +1106,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1134,9 +1122,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1153,9 +1138,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1172,9 +1154,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1191,9 +1170,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1210,9 +1186,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1235,9 +1208,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1260,9 +1230,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1285,9 +1252,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1310,9 +1274,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1335,9 +1296,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1360,9 +1318,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1385,9 +1340,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1603,9 +1555,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1622,9 +1571,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1641,9 +1587,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1660,9 +1603,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1992,9 +1932,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2012,9 +1949,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2032,9 +1966,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2052,9 +1983,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2605,9 +2533,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2622,9 +2547,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2639,9 +2561,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2656,9 +2575,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2673,9 +2589,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2690,9 +2603,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2707,9 +2617,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2724,9 +2631,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2741,9 +2645,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2758,9 +2659,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5619,9 +5517,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5643,9 +5538,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5667,9 +5559,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5691,9 +5580,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/prisma/dev.db b/prisma/dev.db index 9513487..143ebe7 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..985b5fa Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/ol-logo.png b/public/ol-logo.png new file mode 100644 index 0000000..3873a15 Binary files /dev/null and b/public/ol-logo.png differ