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 {
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<EditFormState>({
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<string, unknown>).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<string, unknown> = {
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<typeof updateItem.mutate>[0], {
onSuccess: () => setIsEditing(false),
});
}
function handleDuplicate() {
@@ -156,6 +171,7 @@ function ItemDetail() {
}
const imageUrl = item.imageUrl || null;
const isReference = !!(item as Record<string, unknown>).globalItemId;
return (
<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 */}
<div className="mb-4">
{isEditing ? (
<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"
/>
isReference ? (
/* Reference items: name/brand are read-only (from global catalog) */
<>
{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>
<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>
{/* Badges / Specs */}
{isEditing ? (
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">
Weight (g)
</label>
<input
type="number"
value={form.weightGrams}
onChange={(e) =>
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>
{isReference ? (
/* Reference items: weight/price are read-only (from catalog) */
<>
{item.weightGrams != null && (
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">
Weight (g)
</label>
<p className="py-2 px-3 bg-gray-50 border border-gray-100 rounded-lg text-sm text-gray-500">
{item.weightGrams}
</p>
</div>
)}
{item.priceCents != null && (
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">
MSRP
</label>
<p className="py-2 px-3 bg-gray-50 border border-gray-100 rounded-lg text-sm text-gray-500">
{price(item.priceCents)}
</p>
</div>
)}
</>
) : (
<>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">
Weight (g)
</label>
<input
type="number"
value={form.weightGrams}
onChange={(e) =>
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>
<label className="block text-xs font-medium text-gray-500 mb-1">
Quantity