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:
@@ -7,6 +7,7 @@ import { eq, and } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { addPoolValue, renamePoolValue } from "@/lib/taxonomy";
|
||||
|
||||
const serviceSchema = z.object({
|
||||
name: z.string().min(1, "Nome richiesto"),
|
||||
@@ -95,6 +96,7 @@ export async function updateServiceField(
|
||||
} else if (fieldName === "fase") {
|
||||
const s = String(value).trim();
|
||||
await db.update(services).set({ fase: s || null }).where(eq(services.id, serviceId));
|
||||
if (s) await addPoolValue("service_fase", s);
|
||||
} else if (fieldName === "unit_price") {
|
||||
// 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
|
||||
@@ -143,6 +145,9 @@ export async function addServiceOption(
|
||||
.values({ entity_type: MULTI_ENTITY[field], entity_id: serviceId, name: trimmed })
|
||||
.onConflictDoNothing();
|
||||
|
||||
// Register into the persistent pool so it survives unassignment / shows in settings.
|
||||
await addPoolValue(field === "tag" ? "service_offerta" : "service_pacchetto", trimmed);
|
||||
|
||||
revalidatePath("/admin/catalog");
|
||||
}
|
||||
|
||||
@@ -187,10 +192,12 @@ export async function renameServiceOption(
|
||||
.update(tags)
|
||||
.set({ name: next })
|
||||
.where(and(eq(tags.entity_type, MULTI_ENTITY[field]), eq(tags.name, oldValue)));
|
||||
await renamePoolValue(field === "tag" ? "service_offerta" : "service_pacchetto", oldValue, next);
|
||||
} else if (field === "categoria") {
|
||||
await db.update(services).set({ category: next }).where(eq(services.category, oldValue));
|
||||
} else if (field === "fase") {
|
||||
await db.update(services).set({ fase: next }).where(eq(services.fase, oldValue));
|
||||
await renamePoolValue("service_fase", oldValue, next);
|
||||
} else {
|
||||
throw new Error(`Campo non valido: ${field}`);
|
||||
}
|
||||
@@ -227,14 +234,16 @@ export async function quickAddService(payload: QuickAddPayload | string) {
|
||||
unit_price = num.toFixed(2);
|
||||
}
|
||||
|
||||
const fase = data.fase?.trim() || null;
|
||||
await db.insert(services).values({
|
||||
name,
|
||||
description: data.description?.trim() || null,
|
||||
category: data.category?.trim() || null,
|
||||
fase: data.fase?.trim() || null,
|
||||
fase,
|
||||
unit_price,
|
||||
active: true,
|
||||
});
|
||||
if (fase) await addPoolValue("service_fase", fase);
|
||||
|
||||
revalidatePath("/admin/catalog");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user