fix: flag A/B/C persistence, catalog sort+reorder, offer category pools
- Fix duplicate tier INSERT bug (useState not syncing after router.refresh) - Add column sort by clicking headers in service catalog - Add drag-and-drop column reordering (persisted in localStorage) - Add Categorie Offerta section in Impostazioni (tipo/obiettivo/categoria pools) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowUp, ArrowDown, ArrowUpDown, GripVertical } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { EditableCell } from "@/components/ui/editable-cell";
|
||||
import { OptionSelect } from "@/components/ui/option-select";
|
||||
@@ -15,28 +16,42 @@ import {
|
||||
} from "@/app/admin/catalog/actions";
|
||||
import type { ServiceWithTags, CatalogFieldOptions } from "@/lib/admin-queries";
|
||||
|
||||
export type ColumnKey =
|
||||
| "nome"
|
||||
| "descrizione"
|
||||
| "fase"
|
||||
| "offerta"
|
||||
| "pacchetto"
|
||||
| "prezzo"
|
||||
| "stato";
|
||||
|
||||
export const COLUMN_DEFS: Record<ColumnKey, { header: string; sortable: boolean; minWidth: string }> = {
|
||||
nome: { header: "Nome", sortable: true, minWidth: "160px" },
|
||||
descrizione: { header: "Descrizione", sortable: false, minWidth: "200px" },
|
||||
fase: { header: "Fase", sortable: true, minWidth: "140px" },
|
||||
offerta: { header: "Offerta", sortable: true, minWidth: "180px" },
|
||||
pacchetto: { header: "Pacchetto", sortable: true, minWidth: "180px" },
|
||||
prezzo: { header: "Prezzo", sortable: true, minWidth: "100px" },
|
||||
stato: { header: "Stato", sortable: false, minWidth: "100px" },
|
||||
};
|
||||
|
||||
export const DEFAULT_COL_ORDER: ColumnKey[] = [
|
||||
"nome", "descrizione", "fase", "offerta", "pacchetto", "prezzo", "stato",
|
||||
];
|
||||
|
||||
function formatPrice(raw: string): string {
|
||||
const num = parseFloat(raw);
|
||||
return `€${(isNaN(num) ? 0 : num).toLocaleString("it-IT", { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
const COLUMN_HEADERS = [
|
||||
"Nome",
|
||||
"Descrizione",
|
||||
"Fase",
|
||||
"Offerta",
|
||||
"Pacchetto",
|
||||
"Prezzo",
|
||||
"Stato",
|
||||
];
|
||||
const COL_COUNT = COLUMN_HEADERS.length;
|
||||
|
||||
function ServiceRow({
|
||||
service,
|
||||
options,
|
||||
colOrder,
|
||||
}: {
|
||||
service: ServiceWithTags;
|
||||
options: CatalogFieldOptions;
|
||||
colOrder: ColumnKey[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [, startTransition] = useTransition();
|
||||
@@ -54,83 +69,116 @@ function ServiceRow({
|
||||
});
|
||||
}
|
||||
|
||||
function renderCell(key: ColumnKey) {
|
||||
switch (key) {
|
||||
case "nome":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 font-medium text-[#1a1a1a] min-w-[160px]">
|
||||
<EditableCell
|
||||
value={service.name}
|
||||
type="text"
|
||||
required
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "name", v))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "descrizione":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 text-[#71717a] min-w-[200px]">
|
||||
<EditableCell
|
||||
value={service.description ?? ""}
|
||||
type="textarea"
|
||||
placeholder="Descrizione..."
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "description", v))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "fase":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 min-w-[140px]">
|
||||
<OptionSelect
|
||||
value={service.fase}
|
||||
options={options.fase}
|
||||
onChange={(v) => run(() => updateServiceField(service.id, "fase", v ?? ""))}
|
||||
onRename={(o, n) => run(() => renameServiceOption("fase", o, n))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "offerta":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 min-w-[180px]">
|
||||
<OptionMultiSelect
|
||||
values={service.tags}
|
||||
options={options.tag}
|
||||
onAdd={(v) => run(() => addServiceOption("tag", service.id, v))}
|
||||
onRemove={(v) => run(() => removeServiceOption("tag", service.id, v))}
|
||||
onRename={(o, n) => run(() => renameServiceOption("tag", o, n))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "pacchetto":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 min-w-[180px]">
|
||||
<OptionMultiSelect
|
||||
values={service.pacchetto}
|
||||
options={options.pacchetto}
|
||||
onAdd={(v) => run(() => addServiceOption("pacchetto", service.id, v))}
|
||||
onRemove={(v) => run(() => removeServiceOption("pacchetto", service.id, v))}
|
||||
onRename={(o, n) => run(() => renameServiceOption("pacchetto", o, n))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "prezzo":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 tabular-nums min-w-[100px]">
|
||||
<EditableCell
|
||||
value={service.unit_price}
|
||||
type="number"
|
||||
formatDisplay={formatPrice}
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "unit_price", v))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "stato":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 min-w-[100px]">
|
||||
<EditableCell
|
||||
value={service.active ? "true" : "false"}
|
||||
type="toggle"
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "active", v))}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
className={`border-b border-[#e5e7eb] hover:bg-[#f9f9f9] transition-colors duration-150 ${
|
||||
!service.active ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
<td className="py-2 px-3 font-medium text-[#1a1a1a] min-w-[160px]">
|
||||
<EditableCell
|
||||
value={service.name}
|
||||
type="text"
|
||||
required
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "name", v))}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-[#71717a] min-w-[200px]">
|
||||
<EditableCell
|
||||
value={service.description ?? ""}
|
||||
type="textarea"
|
||||
placeholder="Descrizione..."
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "description", v))}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 min-w-[140px]">
|
||||
<OptionSelect
|
||||
value={service.fase}
|
||||
options={options.fase}
|
||||
onChange={(v) => run(() => updateServiceField(service.id, "fase", v ?? ""))}
|
||||
onRename={(o, n) => run(() => renameServiceOption("fase", o, n))}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 min-w-[180px]">
|
||||
<OptionMultiSelect
|
||||
values={service.tags}
|
||||
options={options.tag}
|
||||
onAdd={(v) => run(() => addServiceOption("tag", service.id, v))}
|
||||
onRemove={(v) => run(() => removeServiceOption("tag", service.id, v))}
|
||||
onRename={(o, n) => run(() => renameServiceOption("tag", o, n))}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 min-w-[180px]">
|
||||
<OptionMultiSelect
|
||||
values={service.pacchetto}
|
||||
options={options.pacchetto}
|
||||
onAdd={(v) => run(() => addServiceOption("pacchetto", service.id, v))}
|
||||
onRemove={(v) => run(() => removeServiceOption("pacchetto", service.id, v))}
|
||||
onRename={(o, n) => run(() => renameServiceOption("pacchetto", o, n))}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 tabular-nums min-w-[100px]">
|
||||
<EditableCell
|
||||
value={service.unit_price}
|
||||
type="number"
|
||||
formatDisplay={formatPrice}
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "unit_price", v))}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 min-w-[100px]">
|
||||
<EditableCell
|
||||
value={service.active ? "true" : "false"}
|
||||
type="toggle"
|
||||
onSave={(v) => run(() => updateServiceField(service.id, "active", v))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{error && (
|
||||
<tr>
|
||||
<td colSpan={COL_COUNT} className="py-1 px-3 text-xs text-red-600">
|
||||
{error}
|
||||
</td>
|
||||
<tr
|
||||
className={`border-b border-[#e5e7eb] hover:bg-[#f9f9f9] transition-colors duration-150 ${
|
||||
!service.active ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
{colOrder.map((key) => renderCell(key))}
|
||||
</tr>
|
||||
)}
|
||||
{error && (
|
||||
<tr>
|
||||
<td colSpan={colOrder.length} className="py-1 px-3 text-xs text-red-600">
|
||||
{error}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAddRow({ options }: { options: CatalogFieldOptions }) {
|
||||
function QuickAddRow({
|
||||
options,
|
||||
colOrder,
|
||||
}: {
|
||||
options: CatalogFieldOptions;
|
||||
colOrder: ColumnKey[];
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [fase, setFase] = useState("");
|
||||
@@ -172,77 +220,120 @@ function QuickAddRow({ options }: { options: CatalogFieldOptions }) {
|
||||
const cellInput =
|
||||
"border-0 bg-transparent shadow-none h-8 text-sm placeholder:text-[#71717a] focus-visible:ring-1 focus-visible:ring-primary";
|
||||
|
||||
function renderCell(key: ColumnKey) {
|
||||
switch (key) {
|
||||
case "nome":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="+ Aggiungi servizio"
|
||||
className={cellInput}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "descrizione":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3">
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Descrizione..."
|
||||
className={cellInput}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "fase":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3">
|
||||
<Input
|
||||
value={fase}
|
||||
onChange={(e) => setFase(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Fase..."
|
||||
list="catalog-fase-pool"
|
||||
className={cellInput}
|
||||
/>
|
||||
<datalist id="catalog-fase-pool">
|
||||
{options.fase.map((f) => (
|
||||
<option key={f} value={f} />
|
||||
))}
|
||||
</datalist>
|
||||
</td>
|
||||
);
|
||||
case "prezzo":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3">
|
||||
<Input
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="0,00"
|
||||
inputMode="decimal"
|
||||
className={`${cellInput} tabular-nums`}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "stato":
|
||||
return (
|
||||
<td key={key} className="py-2 px-3 text-xs text-[#a1a1aa]">
|
||||
Invio ↵
|
||||
</td>
|
||||
);
|
||||
default:
|
||||
return <td key={key} className="py-2 px-3 text-xs text-[#a1a1aa]" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className="border-b border-[#e5e7eb] bg-[#f9f9f9]">
|
||||
<td className="py-2 px-3">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="+ Aggiungi servizio"
|
||||
className={cellInput}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Descrizione..."
|
||||
className={cellInput}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<Input
|
||||
value={fase}
|
||||
onChange={(e) => setFase(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Fase..."
|
||||
list="catalog-fase-pool"
|
||||
className={cellInput}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-xs text-[#a1a1aa]" colSpan={2}>
|
||||
Offerta e pacchetto dopo la creazione
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<Input
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="0,00"
|
||||
inputMode="decimal"
|
||||
className={`${cellInput} tabular-nums`}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-xs text-[#a1a1aa]">Invio ↵</td>
|
||||
<datalist id="catalog-fase-pool">
|
||||
{options.fase.map((f) => (
|
||||
<option key={f} value={f} />
|
||||
))}
|
||||
</datalist>
|
||||
</tr>
|
||||
{error && (
|
||||
<tr>
|
||||
<td colSpan={COL_COUNT} className="py-1 px-3 text-xs text-red-600">
|
||||
{error}
|
||||
</td>
|
||||
<tr className="border-b border-[#e5e7eb] bg-[#f9f9f9]">
|
||||
{colOrder.map((key) => renderCell(key))}
|
||||
</tr>
|
||||
)}
|
||||
{error && (
|
||||
<tr>
|
||||
<td colSpan={colOrder.length} className="py-1 px-3 text-xs text-red-600">
|
||||
{error}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServiceTable({
|
||||
services,
|
||||
activeServices,
|
||||
inactiveServices,
|
||||
options,
|
||||
colOrder,
|
||||
sortKey,
|
||||
sortDir,
|
||||
onSortClick,
|
||||
onColDragStart,
|
||||
onColDragOver,
|
||||
onColDrop,
|
||||
}: {
|
||||
services: ServiceWithTags[];
|
||||
activeServices: ServiceWithTags[];
|
||||
inactiveServices: ServiceWithTags[];
|
||||
options: CatalogFieldOptions;
|
||||
colOrder: ColumnKey[];
|
||||
sortKey: ColumnKey | null;
|
||||
sortDir: "asc" | "desc";
|
||||
onSortClick: (key: ColumnKey) => void;
|
||||
onColDragStart: (key: ColumnKey) => void;
|
||||
onColDragOver: (e: React.DragEvent, key: ColumnKey) => void;
|
||||
onColDrop: (key: ColumnKey) => void;
|
||||
}) {
|
||||
const activeServices = services.filter((s) => s.active);
|
||||
const inactiveServices = services.filter((s) => !s.active);
|
||||
function SortIcon({ colKey }: { colKey: ColumnKey }) {
|
||||
if (!COLUMN_DEFS[colKey].sortable) return null;
|
||||
if (sortKey !== colKey) return <ArrowUpDown className="inline ml-1 h-3 w-3 opacity-40" />;
|
||||
return sortDir === "asc"
|
||||
? <ArrowUp className="inline ml-1 h-3 w-3 text-[#1A463C]" />
|
||||
: <ArrowDown className="inline ml-1 h-3 w-3 text-[#1A463C]" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
|
||||
@@ -250,33 +341,48 @@ export function ServiceTable({
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[#f9f9f9] border-b border-[#e5e7eb] sticky top-0 z-[1]">
|
||||
<tr>
|
||||
{COLUMN_HEADERS.map((header) => (
|
||||
<th
|
||||
key={header}
|
||||
className="text-left py-2 px-3 font-semibold text-[#71717a]"
|
||||
>
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
{colOrder.map((key) => {
|
||||
const def = COLUMN_DEFS[key];
|
||||
return (
|
||||
<th
|
||||
key={key}
|
||||
draggable
|
||||
onDragStart={() => onColDragStart(key)}
|
||||
onDragOver={(e) => onColDragOver(e, key)}
|
||||
onDrop={() => onColDrop(key)}
|
||||
onClick={() => def.sortable && onSortClick(key)}
|
||||
className={`text-left py-2 px-3 font-semibold text-[#71717a] select-none whitespace-nowrap group ${
|
||||
def.sortable ? "cursor-pointer hover:text-[#1a1a1a]" : "cursor-grab"
|
||||
}`}
|
||||
style={{ minWidth: def.minWidth }}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<GripVertical className="h-3 w-3 opacity-0 group-hover:opacity-30 transition-opacity shrink-0" />
|
||||
{def.header}
|
||||
<SortIcon colKey={key} />
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeServices.map((service) => (
|
||||
<ServiceRow key={service.id} service={service} options={options} />
|
||||
<ServiceRow key={service.id} service={service} options={options} colOrder={colOrder} />
|
||||
))}
|
||||
<QuickAddRow options={options} />
|
||||
<QuickAddRow options={options} colOrder={colOrder} />
|
||||
{inactiveServices.length > 0 && (
|
||||
<>
|
||||
<tr className="bg-[#fafafa]">
|
||||
<td
|
||||
colSpan={COL_COUNT}
|
||||
colSpan={colOrder.length}
|
||||
className="py-1.5 px-3 text-xs font-medium uppercase tracking-wide text-[#a1a1aa]"
|
||||
>
|
||||
Servizi disattivati
|
||||
</td>
|
||||
</tr>
|
||||
{inactiveServices.map((service) => (
|
||||
<ServiceRow key={service.id} service={service} options={options} />
|
||||
<ServiceRow key={service.id} service={service} options={options} colOrder={colOrder} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { PoolManager } from "./PoolManager";
|
||||
import { addPoolValue, removePoolValue } from "@/app/admin/impostazioni/actions";
|
||||
|
||||
export function OfferPoolsSection({
|
||||
tipoPool,
|
||||
obiettivoPool,
|
||||
categoriaPool,
|
||||
}: {
|
||||
tipoPool: string[];
|
||||
obiettivoPool: string[];
|
||||
categoriaPool: string[];
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
|
||||
<h2 className="text-base font-semibold text-[#1a1a1a] mb-1">Categorie Offerta</h2>
|
||||
<p className="text-xs text-[#71717a] mb-5">
|
||||
Valori disponibili nei campi Tipo, Obiettivo e Categoria dell'editor offerte.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<PoolManager
|
||||
label="Tipo offerta"
|
||||
pool={tipoPool}
|
||||
onAdd={(v) => addPoolValue("tipo", v)}
|
||||
onRemove={(v) => removePoolValue("tipo", v)}
|
||||
/>
|
||||
<PoolManager
|
||||
label="Obiettivo"
|
||||
pool={obiettivoPool}
|
||||
onAdd={(v) => addPoolValue("obiettivo", v)}
|
||||
onRemove={(v) => removePoolValue("obiettivo", v)}
|
||||
/>
|
||||
<PoolManager
|
||||
label="Categoria"
|
||||
pool={categoriaPool}
|
||||
onAdd={(v) => addPoolValue("categoria", v)}
|
||||
onRemove={(v) => removePoolValue("categoria", v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export function PoolManager({
|
||||
label,
|
||||
pool,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: {
|
||||
label: string;
|
||||
pool: string[];
|
||||
onAdd: (value: string) => Promise<void>;
|
||||
onRemove: (value: string) => Promise<void>;
|
||||
}) {
|
||||
const [input, setInput] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleAdd() {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return;
|
||||
if (pool.includes(trimmed)) {
|
||||
setError("Valore già presente");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
await onAdd(trimmed);
|
||||
setInput("");
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemove(value: string) {
|
||||
startTransition(() => onRemove(value));
|
||||
}
|
||||
|
||||
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleAdd();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-[180px]">
|
||||
<p className="text-sm font-medium text-[#1a1a1a] mb-2">{label}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 mb-3 min-h-[28px]">
|
||||
{pool.length === 0 && (
|
||||
<span className="text-xs text-[#a1a1aa]">Nessun valore</span>
|
||||
)}
|
||||
{pool.map((v) => (
|
||||
<span
|
||||
key={v}
|
||||
className="inline-flex items-center gap-1 bg-[#f4f4f5] text-[#1a1a1a] text-xs px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{v}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(v)}
|
||||
disabled={isPending}
|
||||
className="text-[#71717a] hover:text-red-500 transition-colors disabled:opacity-50"
|
||||
aria-label={`Rimuovi ${v}`}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => { setInput(e.target.value); setError(null); }}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Aggiungi..."
|
||||
disabled={isPending}
|
||||
className="border border-[#e5e7eb] rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20 flex-1 min-w-0 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={isPending || !input.trim()}
|
||||
className="bg-[#1A463C] text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors disabled:opacity-40"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-red-500 mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useTransition } from "react";
|
||||
import { useState, useMemo, useTransition, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -86,6 +86,13 @@ export function OfferEditorClient({
|
||||
const [tipoTags, setTipoTags] = useState<string[]>(data.tipoTags);
|
||||
const [obiettivoTags, setObiettivoTags] = useState<string[]>(data.obiettivoTags);
|
||||
|
||||
// After first draft save router.refresh() assigns real DB IDs to new tiers.
|
||||
// Re-sync tiers state when those IDs arrive so subsequent saves UPDATE instead of INSERT.
|
||||
const tierIdKey = data.tiers.map((t) => t.id).sort().join(",");
|
||||
useEffect(() => {
|
||||
setTiers(padTiers(data.tiers));
|
||||
}, [tierIdKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const [categoriaOptions, setCategoriaOptions] = useState<string[]>(fieldOptions.categoria);
|
||||
const [ticketOptions, setTicketOptions] = useState<string[]>(fieldOptions.ticket);
|
||||
const [tipoOptions, setTipoOptions] = useState<string[]>(fieldOptions.tipo);
|
||||
|
||||
Reference in New Issue
Block a user