From 4ed2f8b105ac9e64c0d6b11cd46a80aa771788cc Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 11 Jun 2026 06:26:03 +0200 Subject: [PATCH] feat(07-unified-service-catalog): rewire /admin/catalog CRUD + query layer to services table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task 1: Update getAllServices() and activeServices queries to read from services table instead of service_catalog - Both ClientFullDetail and ProjectFullDetail now return Service[] (not ServiceCatalog[]) - Quote items label join remains unchanged — still references service_catalog for historical quote_items.service_id FK integrity - Task 2: Rewire /admin/catalog actions and components to operate on services table - src/app/admin/catalog/actions.ts: createService/updateService/toggleServiceActive now write to services with optional category field - ServiceTable.tsx: Updated to Service[] type, added category column rendering - ServiceForm.tsx: Added optional category input field - QuoteTab.tsx: Updated activeServices type annotation to Service[] - New services created via /admin/catalog have migrated_from=null, migrated_id=null (not migrated) - TypeScript compiles successfully - npm run build: PASS Co-Authored-By: Claude Haiku 4.5 --- src/app/admin/catalog/actions.ts | 19 ++++++++++-------- src/components/admin/catalog/ServiceForm.tsx | 8 ++++++++ src/components/admin/catalog/ServiceTable.tsx | 18 ++++++++++++++--- src/components/admin/tabs/QuoteTab.tsx | 4 ++-- src/lib/admin-queries.ts | 20 ++++++++++--------- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/app/admin/catalog/actions.ts b/src/app/admin/catalog/actions.ts index 0a8a5fc..6f9f7e5 100644 --- a/src/app/admin/catalog/actions.ts +++ b/src/app/admin/catalog/actions.ts @@ -1,7 +1,7 @@ "use server"; import { db } from "@/db"; -import { service_catalog } from "@/db/schema"; +import { services } from "@/db/schema"; import { revalidatePath } from "next/cache"; import { eq } from "drizzle-orm"; import { z } from "zod"; @@ -12,6 +12,7 @@ const serviceSchema = z.object({ name: z.string().min(1, "Nome richiesto"), description: z.string().optional(), unit_price: z.coerce.number().min(0.01, "Prezzo deve essere maggiore di 0"), + category: z.string().optional(), }); async function requireAdmin() { @@ -25,12 +26,15 @@ export async function createService(formData: FormData) { name: formData.get("name"), description: formData.get("description") ?? "", unit_price: formData.get("unit_price"), + category: formData.get("category") ?? "", }); if (!parsed.success) throw new Error(parsed.error.issues[0].message); - await db.insert(service_catalog).values({ + // New rows created from /admin/catalog are NOT migrated — migrated_from/migrated_id stay null + await db.insert(services).values({ name: parsed.data.name, description: parsed.data.description ?? null, unit_price: parsed.data.unit_price.toFixed(2), + category: parsed.data.category || null, }); revalidatePath("/admin/catalog"); } @@ -41,24 +45,23 @@ export async function updateService(serviceId: string, formData: FormData) { name: formData.get("name"), description: formData.get("description") ?? "", unit_price: formData.get("unit_price"), + category: formData.get("category") ?? "", }); if (!parsed.success) throw new Error(parsed.error.issues[0].message); await db - .update(service_catalog) + .update(services) .set({ name: parsed.data.name, description: parsed.data.description ?? null, unit_price: parsed.data.unit_price.toFixed(2), + category: parsed.data.category || null, }) - .where(eq(service_catalog.id, serviceId)); + .where(eq(services.id, serviceId)); revalidatePath("/admin/catalog"); } export async function toggleServiceActive(serviceId: string, active: boolean) { await requireAdmin(); - await db - .update(service_catalog) - .set({ active }) - .where(eq(service_catalog.id, serviceId)); + await db.update(services).set({ active }).where(eq(services.id, serviceId)); revalidatePath("/admin/catalog"); } diff --git a/src/components/admin/catalog/ServiceForm.tsx b/src/components/admin/catalog/ServiceForm.tsx index 9f08df3..ab77282 100644 --- a/src/components/admin/catalog/ServiceForm.tsx +++ b/src/components/admin/catalog/ServiceForm.tsx @@ -55,6 +55,14 @@ export function ServiceForm() { placeholder="es. Incluso: analisi competitor, posizionamento" /> +
+ + +
(null); const [, startTransition] = useTransition(); @@ -72,6 +72,14 @@ function ServiceRow({ service }: { service: ServiceCatalog }) { required />
+
+ + +
{error &&

{error}

}
@@ -110,6 +118,9 @@ function ServiceRow({ service }: { service: ServiceCatalog }) { {service.description ?? "—"} + + {service.category ?? "—"} + €{parseFloat(service.unit_price).toLocaleString("it-IT", { minimumFractionDigits: 2 })} @@ -138,7 +149,7 @@ function ServiceRow({ service }: { service: ServiceCatalog }) { ); } -export function ServiceTable({ services }: { services: ServiceCatalog[] }) { +export function ServiceTable({ services }: { services: Service[] }) { return (
@@ -146,6 +157,7 @@ export function ServiceTable({ services }: { services: ServiceCatalog[] }) { + diff --git a/src/components/admin/tabs/QuoteTab.tsx b/src/components/admin/tabs/QuoteTab.tsx index 2674e1a..c517917 100644 --- a/src/components/admin/tabs/QuoteTab.tsx +++ b/src/components/admin/tabs/QuoteTab.tsx @@ -11,12 +11,12 @@ import { updateAcceptedTotal, } from "@/app/admin/clients/[id]/quote-actions"; import type { QuoteItemWithLabel } from "@/lib/admin-queries"; -import type { ServiceCatalog } from "@/db/schema"; +import type { Service } from "@/db/schema"; type Props = { clientId: string; items: QuoteItemWithLabel[]; - activeServices: ServiceCatalog[]; + activeServices: Service[]; acceptedTotal: string; }; diff --git a/src/lib/admin-queries.ts b/src/lib/admin-queries.ts index 5cfab24..215961d 100644 --- a/src/lib/admin-queries.ts +++ b/src/lib/admin-queries.ts @@ -12,6 +12,7 @@ import { time_entries, quote_items, service_catalog, + services, settings, offer_micros, project_offers, @@ -28,6 +29,7 @@ import type { Note, Comment, ServiceCatalog, + Service, OfferMicro, ProjectOffer, } from "@/db/schema"; @@ -221,7 +223,7 @@ export type ClientFullDetail = { notes: Note[]; comments: Comment[]; quoteItems: QuoteItemWithLabel[]; - activeServices: ServiceCatalog[]; + activeServices: Service[]; }; export async function getClientFullDetail(id: string): Promise { @@ -238,9 +240,9 @@ export async function getClientFullDetail(id: string): Promise { +export async function getAllServices(): Promise { return db .select() - .from(service_catalog) - .orderBy(asc(service_catalog.name)); + .from(services) + .orderBy(asc(services.name)); } // ── ProjectWithPayments — used by /admin/projects list ─────────────────────── @@ -456,7 +458,7 @@ export type ProjectFullDetail = { notes: Note[]; comments: Comment[]; quoteItems: QuoteItemWithLabel[]; - activeServices: ServiceCatalog[]; + activeServices: Service[]; activeTimerEntryId: string | null; activeTimerStartedAt: Date | null; totalTrackedSeconds: number; @@ -527,7 +529,7 @@ export async function getProjectFullDetail(id: string): Promise
Nome DescrizioneCategoria Prezzo Stato