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:
+10
-36
@@ -8,7 +8,7 @@ import {
|
||||
services,
|
||||
tags,
|
||||
} from "@/db/schema";
|
||||
import { getJsonPool, SETTINGS_KEYS } from "@/lib/settings";
|
||||
import { getPool } from "@/lib/taxonomy";
|
||||
import type { OfferMacro, OfferMicro, OfferService } from "@/db/schema";
|
||||
import { eq, and, asc, inArray, sql } from "drizzle-orm";
|
||||
|
||||
@@ -273,40 +273,14 @@ export type OfferFieldOptions = {
|
||||
};
|
||||
|
||||
// Shared option pools for the offer editor's select fields (Notion-style
|
||||
// dropdowns). Tipo, Obiettivo, and Categoria are merged from the managed
|
||||
// settings pools + any values already in use (retrocompatibility).
|
||||
// dropdowns). All four read from the centralized, persistent taxonomy pools
|
||||
// (src/lib/taxonomy.ts), seeded from existing values on first access.
|
||||
export async function getOfferFieldOptions(): Promise<OfferFieldOptions> {
|
||||
const [categoriaRows, ticketRows, tagRows, tipoPool, obiettivoPool, categoriaPool] =
|
||||
await Promise.all([
|
||||
db.selectDistinct({ value: offer_macros.category }).from(offer_macros),
|
||||
db.selectDistinct({ value: offer_macros.ticket }).from(offer_macros),
|
||||
db
|
||||
.selectDistinct({ name: tags.name, type: tags.entity_type })
|
||||
.from(tags)
|
||||
.where(inArray(tags.entity_type, [OFFER_TIPO_ENTITY, OFFER_OBIETTIVO_ENTITY])),
|
||||
getJsonPool(SETTINGS_KEYS.OFFER_TIPO_POOL),
|
||||
getJsonPool(SETTINGS_KEYS.OFFER_OBIETTIVO_POOL),
|
||||
getJsonPool(SETTINGS_KEYS.OFFER_CATEGORIA_POOL),
|
||||
]);
|
||||
|
||||
const sortUnique = (arr: (string | null)[]) =>
|
||||
Array.from(new Set(arr.filter((v): v is string => !!v && v.trim().length > 0))).sort(
|
||||
(a, b) => a.localeCompare(b, "it")
|
||||
);
|
||||
|
||||
return {
|
||||
categoria: sortUnique([
|
||||
...categoriaPool,
|
||||
...categoriaRows.map((r) => r.value),
|
||||
]),
|
||||
ticket: sortUnique(ticketRows.map((r) => r.value)),
|
||||
tipo: sortUnique([
|
||||
...tipoPool,
|
||||
...tagRows.filter((r) => r.type === OFFER_TIPO_ENTITY).map((r) => r.name),
|
||||
]),
|
||||
obiettivo: sortUnique([
|
||||
...obiettivoPool,
|
||||
...tagRows.filter((r) => r.type === OFFER_OBIETTIVO_ENTITY).map((r) => r.name),
|
||||
]),
|
||||
};
|
||||
const [categoria, ticket, tipo, obiettivo] = await Promise.all([
|
||||
getPool("offer_categoria"),
|
||||
getPool("offer_ticket"),
|
||||
getPool("offer_tipo"),
|
||||
getPool("offer_obiettivo"),
|
||||
]);
|
||||
return { categoria, ticket, tipo, obiettivo };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user