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) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 19:22:09 +02:00
parent 56b81ee8ab
commit 69308e293f

View File

@@ -13,6 +13,7 @@ export const Route = createFileRoute("/items/$itemId")({
interface EditFormState { interface EditFormState {
name: string; name: string;
brand: string;
weightGrams: string; weightGrams: string;
priceDollars: string; priceDollars: string;
quantity: number; quantity: number;
@@ -45,6 +46,7 @@ function ItemDetail() {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [form, setForm] = useState<EditFormState>({ const [form, setForm] = useState<EditFormState>({
name: "", name: "",
brand: "",
weightGrams: "", weightGrams: "",
priceDollars: "", priceDollars: "",
quantity: 1, quantity: 1,
@@ -57,7 +59,8 @@ function ItemDetail() {
function enterEditMode() { function enterEditMode() {
if (!item) return; if (!item) return;
setForm({ setForm({
name: item.name, name: item.brand ? item.name.replace(`${item.brand} `, "") : item.name,
brand: item.brand || "",
weightGrams: item.weightGrams != null ? String(item.weightGrams) : "", weightGrams: item.weightGrams != null ? String(item.weightGrams) : "",
priceDollars: priceDollars:
item.priceCents != null ? (item.priceCents / 100).toFixed(2) : "", item.priceCents != null ? (item.priceCents / 100).toFixed(2) : "",
@@ -76,29 +79,41 @@ function ItemDetail() {
function handleSave() { function handleSave() {
if (!item) return; if (!item) return;
const weightGrams = form.weightGrams.trim() const hasGlobalItem = !!(item as Record<string, unknown>).globalItemId;
? Math.round(Number(form.weightGrams))
: null;
const priceCents = form.priceDollars.trim()
? Math.round(Number(form.priceDollars) * 100)
: null;
updateItem.mutate( // Personal fields (always editable)
{ const payload: Record<string, unknown> = {
id: item.id, id: item.id,
name: form.name.trim(), quantity: form.quantity,
weightGrams, categoryId: form.categoryId,
priceCents, notes: form.notes.trim() || undefined,
quantity: form.quantity, productUrl: form.productUrl.trim() || undefined,
categoryId: form.categoryId, imageFilename: form.imageFilename ?? undefined,
notes: form.notes.trim() || null, };
productUrl: form.productUrl.trim() || null,
imageFilename: form.imageFilename, // Global fields (only editable for standalone items)
}, if (!hasGlobalItem) {
{ const weightGrams = form.weightGrams.trim()
onSuccess: () => setIsEditing(false), ? 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<typeof updateItem.mutate>[0], {
onSuccess: () => setIsEditing(false),
});
} }
function handleDuplicate() { function handleDuplicate() {
@@ -156,6 +171,7 @@ function ItemDetail() {
} }
const imageUrl = item.imageUrl || null; const imageUrl = item.imageUrl || null;
const isReference = !!(item as Record<string, unknown>).globalItemId;
return ( return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
@@ -248,56 +264,125 @@ function ItemDetail() {
{/* Header / Name */} {/* Header / Name */}
<div className="mb-4"> <div className="mb-4">
{isEditing ? ( {isEditing ? (
<input isReference ? (
type="text" /* Reference items: name/brand are read-only (from global catalog) */
value={form.name} <>
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} {item.brand && (
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" <p className="text-sm font-medium text-gray-400 uppercase tracking-wide mb-1">
placeholder="Item name" {item.brand}
/> </p>
)}
<h1 className="text-2xl font-bold text-gray-900">
{item.brand
? item.name.replace(`${item.brand} `, "")
: item.name}
</h1>
<p className="text-xs text-gray-400 mt-1">
Name and brand are from the catalog
</p>
</>
) : (
<div className="space-y-2">
<input
type="text"
value={form.brand}
onChange={(e) =>
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)"
/>
<input
type="text"
value={form.name}
onChange={(e) =>
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"
/>
</div>
)
) : ( ) : (
<h1 className="text-2xl font-bold text-gray-900">{item.name}</h1> <>
{item.brand && (
<p className="text-sm font-medium text-gray-400 uppercase tracking-wide mb-1">
{item.brand}
</p>
)}
<h1 className="text-2xl font-bold text-gray-900">
{item.brand ? item.name.replace(`${item.brand} `, "") : item.name}
</h1>
</>
)} )}
</div> </div>
{/* Badges / Specs */} {/* Badges / Specs */}
{isEditing ? ( {isEditing ? (
<div className="grid grid-cols-2 gap-4 mb-6"> <div className="grid grid-cols-2 gap-4 mb-6">
<div> {isReference ? (
<label className="block text-xs font-medium text-gray-500 mb-1"> /* Reference items: weight/price are read-only (from catalog) */
Weight (g) <>
</label> {item.weightGrams != null && (
<input <div>
type="number" <label className="block text-xs font-medium text-gray-500 mb-1">
value={form.weightGrams} Weight (g)
onChange={(e) => </label>
setForm((f) => ({ <p className="py-2 px-3 bg-gray-50 border border-gray-100 rounded-lg text-sm text-gray-500">
...f, {item.weightGrams}
weightGrams: e.target.value, </p>
})) </div>
} )}
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" {item.priceCents != null && (
placeholder="0" <div>
/> <label className="block text-xs font-medium text-gray-500 mb-1">
</div> MSRP
<div> </label>
<label className="block text-xs font-medium text-gray-500 mb-1"> <p className="py-2 px-3 bg-gray-50 border border-gray-100 rounded-lg text-sm text-gray-500">
Price ($) {price(item.priceCents)}
</label> </p>
<input </div>
type="number" )}
step="0.01" </>
value={form.priceDollars} ) : (
onChange={(e) => <>
setForm((f) => ({ <div>
...f, <label className="block text-xs font-medium text-gray-500 mb-1">
priceDollars: e.target.value, Weight (g)
})) </label>
} <input
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" type="number"
placeholder="0.00" value={form.weightGrams}
/> onChange={(e) =>
</div> 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"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">
Price ($)
</label>
<input
type="number"
step="0.01"
value={form.priceDollars}
onChange={(e) =>
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"
/>
</div>
</>
)}
<div> <div>
<label className="block text-xs font-medium text-gray-500 mb-1"> <label className="block text-xs font-medium text-gray-500 mb-1">
Quantity Quantity