diff --git a/src/app/admin/catalog/actions.ts b/src/app/admin/catalog/actions.ts index ca792de..079588b 100644 --- a/src/app/admin/catalog/actions.ts +++ b/src/app/admin/catalog/actions.ts @@ -93,8 +93,16 @@ export async function updateServiceField( const s = String(value).trim(); await db.update(services).set({ category: s || null }).where(eq(services.id, serviceId)); } else if (fieldName === "unit_price") { - const num = parseFloat(String(value)); - if (isNaN(num) || num < 0) throw new Error("Prezzo invalido"); + // Normalize locale-formatted input (WR-04): the cell displays it-IT (€1.234,50), + // so an admin may type "1.234,50". When a comma is present, treat "." as thousands + // separators and "," as the decimal mark; otherwise parse "." as the decimal mark. + // Number() (not parseFloat) rejects trailing garbage like "12abc". + const rawInput = String(value).trim(); + const normalized = rawInput.includes(",") + ? rawInput.replace(/\./g, "").replace(",", ".") + : rawInput; + const num = Number(normalized); + if (!Number.isFinite(num) || num < 0) throw new Error("Prezzo invalido"); await db.update(services).set({ unit_price: num.toFixed(2) }).where(eq(services.id, serviceId)); } else if (fieldName === "active") { const boolValue = typeof value === "boolean" ? value : value === "true"; diff --git a/src/components/admin/catalog/ServiceTable.tsx b/src/components/admin/catalog/ServiceTable.tsx index 5252658..4dd276d 100644 --- a/src/components/admin/catalog/ServiceTable.tsx +++ b/src/components/admin/catalog/ServiceTable.tsx @@ -34,6 +34,7 @@ function ServiceRow({ service }: { service: ServiceWithTags }) { } return ( + <> saveField("active", v)} /> - {error && ( + + {error && ( + {error} - )} - + + )} + ); } diff --git a/src/components/ui/editable-cell.tsx b/src/components/ui/editable-cell.tsx index 4dea4c2..2812d7a 100644 --- a/src/components/ui/editable-cell.tsx +++ b/src/components/ui/editable-cell.tsx @@ -46,6 +46,12 @@ export function EditableCell({ } function commit() { + // WR-02: skip redundant server action when nothing changed + if (tempValue === String(value)) { + setError(null); + setIsEditing(false); + return; + } if (required && tempValue.trim().length === 0) { setError("Campo richiesto"); return; @@ -55,6 +61,23 @@ export function EditableCell({ setIsEditing(false); } + // Blur path: never leave the cell stuck. No-op if unchanged (WR-02); + // revert to the last valid value instead of persisting/holding an + // invalid required field while focus has already moved away (WR-03). + function commitOnBlur() { + if (tempValue === String(value)) { + cancel(); + return; + } + if (required && tempValue.trim().length === 0) { + cancel(); + return; + } + setError(null); + onSave(tempValue); + setIsEditing(false); + } + function cancel() { setTempValue(String(value)); setError(null); @@ -103,7 +126,7 @@ export function EditableCell({ ref={inputRef as React.Ref} value={tempValue} onChange={(e) => setTempValue(e.target.value)} - onBlur={commit} + onBlur={commitOnBlur} onKeyDown={handleKeyDown} placeholder={placeholder} className={cn("ring-1 ring-primary resize-none text-sm", error && "ring-2 ring-red-500")} @@ -120,8 +143,9 @@ export function EditableCell({ onSave(next); setIsEditing(false); }} - onBlur={commit} - onKeyDown={handleKeyDown} + // WR-01: onChange already saves + closes — a blur-commit here would + // fire a second (possibly stale) updateServiceField for the same field. + onKeyDown={(e) => { if (e.key === "Escape") cancel(); }} className="h-4 w-4 cursor-pointer accent-[#1A463C]" /> ) : ( @@ -130,7 +154,7 @@ export function EditableCell({ type={type} value={tempValue} onChange={(e) => setTempValue(e.target.value)} - onBlur={commit} + onBlur={commitOnBlur} onKeyDown={handleKeyDown} placeholder={placeholder} step={type === "number" ? "0.01" : undefined}