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 hasGlobalItem = !!(item as Record<string, unknown>).globalItemId;
// 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() const weightGrams = form.weightGrams.trim()
? Math.round(Number(form.weightGrams)) ? Math.round(Number(form.weightGrams))
: null; : null;
const priceCents = form.priceDollars.trim() const priceCents = form.priceDollars.trim()
? Math.round(Number(form.priceDollars) * 100) ? Math.round(Number(form.priceDollars) * 100)
: null; : null;
const brandTrimmed = form.brand.trim();
const nameTrimmed = form.name.trim();
const fullName = brandTrimmed
? `${brandTrimmed} ${nameTrimmed}`
: nameTrimmed;
updateItem.mutate( payload.name = fullName;
{ payload.brand = brandTrimmed || undefined;
id: item.id, payload.weightGrams = weightGrams ?? undefined;
name: form.name.trim(), payload.priceCents = priceCents ?? undefined;
weightGrams, }
priceCents,
quantity: form.quantity, updateItem.mutate(payload as Parameters<typeof updateItem.mutate>[0], {
categoryId: form.categoryId,
notes: form.notes.trim() || null,
productUrl: form.productUrl.trim() || null,
imageFilename: form.imageFilename,
},
{
onSuccess: () => setIsEditing(false), 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,21 +264,88 @@ function ItemDetail() {
{/* Header / Name */} {/* Header / Name */}
<div className="mb-4"> <div className="mb-4">
{isEditing ? ( {isEditing ? (
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 <input
type="text" type="text"
value={form.name} value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} 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" 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" 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">
{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> <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">
Weight (g) Weight (g)
@@ -298,6 +381,8 @@ function ItemDetail() {
placeholder="0.00" placeholder="0.00"
/> />
</div> </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