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}