feat: centralized Notion-style taxonomy management in settings

Single persistent option pool per taxonomy (7 fields: offer
categoria/ticket/tipo/obiettivo + catalog fase/offerta/pacchetto),
stored as JSON in the settings table (no DB migration).

- src/lib/taxonomy.ts: pools with lazy seed from in-use values,
  add/remove(cascade)/rename helpers
- Inline creation anywhere registers into the pool (save offer,
  addServiceOption, updateServiceField fase, quickAdd, create macro)
- Deselecting on a row never touches the pool; global delete only
  from settings (cascade-strips tags / nulls columns)
- getOfferFieldOptions + getCatalogFieldOptions read from pools
- Settings: TaxonomyManager (offer + catalog groups), confirm on delete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:24:47 +02:00
parent 320827e13a
commit e80c95f838
11 changed files with 363 additions and 161 deletions
@@ -1,43 +0,0 @@
"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&apos;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>
);
}
@@ -33,6 +33,10 @@ export function PoolManager({
}
function handleRemove(value: string) {
const ok = window.confirm(
`Eliminare "${value}" da «${label}»?\n\nVerrà rimosso ovunque: sparirà dal menu e da tutte le offerte/servizi che lo usano.`
);
if (!ok) return;
startTransition(() => onRemove(value));
}
@@ -0,0 +1,68 @@
"use client";
import { PoolManager } from "./PoolManager";
import { addTaxonomyValue, removeTaxonomyValue } from "@/app/admin/impostazioni/actions";
type FieldDef = { id: string; label: string };
const OFFER_FIELDS: FieldDef[] = [
{ id: "offer_categoria", label: "Categoria" },
{ id: "offer_ticket", label: "Ticket" },
{ id: "offer_tipo", label: "Tipo" },
{ id: "offer_obiettivo", label: "Obiettivo" },
];
const CATALOG_FIELDS: FieldDef[] = [
{ id: "service_fase", label: "Fase" },
{ id: "service_offerta", label: "Offerta" },
{ id: "service_pacchetto", label: "Pacchetto" },
];
function Section({
title,
subtitle,
fields,
pools,
}: {
title: string;
subtitle: string;
fields: FieldDef[];
pools: Record<string, string[]>;
}) {
return (
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
<h2 className="text-base font-semibold text-[#1a1a1a] mb-1">{title}</h2>
<p className="text-xs text-[#71717a] mb-5">{subtitle}</p>
<div className="flex flex-wrap gap-6">
{fields.map((f) => (
<PoolManager
key={f.id}
label={f.label}
pool={pools[f.id] ?? []}
onAdd={(v) => addTaxonomyValue(f.id, v)}
onRemove={(v) => removeTaxonomyValue(f.id, v)}
/>
))}
</div>
</div>
);
}
export function TaxonomyManager({ pools }: { pools: Record<string, string[]> }) {
return (
<>
<Section
title="Tassonomie Offerte"
subtitle="Valori disponibili nei campi Categoria, Ticket, Tipo e Obiettivo dell'editor offerte."
fields={OFFER_FIELDS}
pools={pools}
/>
<Section
title="Tassonomie Catalogo"
subtitle="Valori disponibili nei campi Fase, Offerta e Pacchetto del catalogo servizi."
fields={CATALOG_FIELDS}
pools={pools}
/>
</>
);
}