From 69308e293f7eb0bcf41522d7fba6728d9d41e1ae Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 6 Apr 2026 19:22:09 +0200 Subject: [PATCH] fix: restrict edit mode for reference items to personal fields only Reference items (linked to global catalog) now show name, brand, weight, and MSRP as read-only in edit mode with "from the catalog" hint. Only personal fields (notes, category, quantity, image, product URL) are editable. Standalone items retain full edit access. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/routes/items/$itemId.tsx | 217 +++++++++++++++++++--------- 1 file changed, 151 insertions(+), 66 deletions(-) diff --git a/src/client/routes/items/$itemId.tsx b/src/client/routes/items/$itemId.tsx index f86e0a2..59de5c8 100644 --- a/src/client/routes/items/$itemId.tsx +++ b/src/client/routes/items/$itemId.tsx @@ -13,6 +13,7 @@ export const Route = createFileRoute("/items/$itemId")({ interface EditFormState { name: string; + brand: string; weightGrams: string; priceDollars: string; quantity: number; @@ -45,6 +46,7 @@ function ItemDetail() { const [isEditing, setIsEditing] = useState(false); const [form, setForm] = useState({ name: "", + brand: "", weightGrams: "", priceDollars: "", quantity: 1, @@ -57,7 +59,8 @@ function ItemDetail() { function enterEditMode() { if (!item) return; setForm({ - name: item.name, + name: item.brand ? item.name.replace(`${item.brand} `, "") : item.name, + brand: item.brand || "", weightGrams: item.weightGrams != null ? String(item.weightGrams) : "", priceDollars: item.priceCents != null ? (item.priceCents / 100).toFixed(2) : "", @@ -76,29 +79,41 @@ function ItemDetail() { function handleSave() { if (!item) return; - const weightGrams = form.weightGrams.trim() - ? Math.round(Number(form.weightGrams)) - : null; - const priceCents = form.priceDollars.trim() - ? Math.round(Number(form.priceDollars) * 100) - : null; + const hasGlobalItem = !!(item as Record).globalItemId; - updateItem.mutate( - { - id: item.id, - name: form.name.trim(), - weightGrams, - priceCents, - quantity: form.quantity, - categoryId: form.categoryId, - notes: form.notes.trim() || null, - productUrl: form.productUrl.trim() || null, - imageFilename: form.imageFilename, - }, - { - onSuccess: () => setIsEditing(false), - }, - ); + // Personal fields (always editable) + const payload: Record = { + id: item.id, + quantity: form.quantity, + categoryId: form.categoryId, + notes: form.notes.trim() || undefined, + productUrl: form.productUrl.trim() || undefined, + imageFilename: form.imageFilename ?? undefined, + }; + + // Global fields (only editable for standalone items) + if (!hasGlobalItem) { + const weightGrams = form.weightGrams.trim() + ? Math.round(Number(form.weightGrams)) + : null; + const priceCents = form.priceDollars.trim() + ? Math.round(Number(form.priceDollars) * 100) + : null; + const brandTrimmed = form.brand.trim(); + const nameTrimmed = form.name.trim(); + const fullName = brandTrimmed + ? `${brandTrimmed} ${nameTrimmed}` + : nameTrimmed; + + payload.name = fullName; + payload.brand = brandTrimmed || undefined; + payload.weightGrams = weightGrams ?? undefined; + payload.priceCents = priceCents ?? undefined; + } + + updateItem.mutate(payload as Parameters[0], { + onSuccess: () => setIsEditing(false), + }); } function handleDuplicate() { @@ -156,6 +171,7 @@ function ItemDetail() { } const imageUrl = item.imageUrl || null; + const isReference = !!(item as Record).globalItemId; return (
@@ -248,56 +264,125 @@ function ItemDetail() { {/* Header / Name */}
{isEditing ? ( - setForm((f) => ({ ...f, name: e.target.value }))} - className="w-full text-2xl font-bold text-gray-900 border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" - placeholder="Item name" - /> + isReference ? ( + /* Reference items: name/brand are read-only (from global catalog) */ + <> + {item.brand && ( +

+ {item.brand} +

+ )} +

+ {item.brand + ? item.name.replace(`${item.brand} `, "") + : item.name} +

+

+ Name and brand are from the catalog +

+ + ) : ( +
+ + setForm((f) => ({ ...f, brand: e.target.value })) + } + className="w-full text-sm font-medium text-gray-500 border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="Brand / Manufacturer (optional)" + /> + + setForm((f) => ({ ...f, name: e.target.value })) + } + className="w-full text-2xl font-bold text-gray-900 border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="Item name / Model" + /> +
+ ) ) : ( -

{item.name}

+ <> + {item.brand && ( +

+ {item.brand} +

+ )} +

+ {item.brand ? item.name.replace(`${item.brand} `, "") : item.name} +

+ )}
{/* Badges / Specs */} {isEditing ? (
-
- - - setForm((f) => ({ - ...f, - weightGrams: e.target.value, - })) - } - className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" - placeholder="0" - /> -
-
- - - setForm((f) => ({ - ...f, - priceDollars: e.target.value, - })) - } - className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" - placeholder="0.00" - /> -
+ {isReference ? ( + /* Reference items: weight/price are read-only (from catalog) */ + <> + {item.weightGrams != null && ( +
+ +

+ {item.weightGrams} +

+
+ )} + {item.priceCents != null && ( +
+ +

+ {price(item.priceCents)} +

+
+ )} + + ) : ( + <> +
+ + + setForm((f) => ({ + ...f, + weightGrams: e.target.value, + })) + } + className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="0" + /> +
+
+ + + setForm((f) => ({ + ...f, + priceDollars: e.target.value, + })) + } + className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="0.00" + /> +
+ + )}