fix: resolve merge conflict in admin-queries.ts — keep both quote_items and service_catalog imports
This commit is contained in:
@@ -5,6 +5,7 @@ import { PhasesTab } from "@/components/admin/tabs/PhasesTab";
|
||||
import { PaymentsTab } from "@/components/admin/tabs/PaymentsTab";
|
||||
import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab";
|
||||
import { CommentsTab } from "@/components/admin/tabs/CommentsTab";
|
||||
import { QuoteTab } from "@/components/admin/tabs/QuoteTab";
|
||||
import { PhasesViewToggle } from "@/components/admin/kanban/PhasesViewToggle";
|
||||
import { ClientActions } from "@/components/admin/ClientActions";
|
||||
import Link from "next/link";
|
||||
@@ -20,7 +21,7 @@ export default async function ClientDetailPage({
|
||||
const detail = await getClientFullDetail(id);
|
||||
if (!detail) notFound();
|
||||
|
||||
const { client, phases, payments, documents, comments } = detail;
|
||||
const { client, phases, payments, documents, comments, quoteItems, activeServices } = detail;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -59,6 +60,7 @@ export default async function ClientDetailPage({
|
||||
<TabsTrigger value="payments">Pagamenti</TabsTrigger>
|
||||
<TabsTrigger value="documents">Documenti</TabsTrigger>
|
||||
<TabsTrigger value="comments">Commenti</TabsTrigger>
|
||||
<TabsTrigger value="quote">Preventivo</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="phases">
|
||||
@@ -81,6 +83,14 @@ export default async function ClientDetailPage({
|
||||
<TabsContent value="comments">
|
||||
<CommentsTab comments={comments} phases={phases} clientId={client.id} />
|
||||
</TabsContent>
|
||||
<TabsContent value="quote">
|
||||
<QuoteTab
|
||||
clientId={client.id}
|
||||
items={quoteItems}
|
||||
activeServices={activeServices}
|
||||
acceptedTotal={client.accepted_total ?? "0"}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"use server";
|
||||
|
||||
// quote_items NEVER exposed — security constraint from Phase 1 (CLAUDE.md)
|
||||
// Only clients.accepted_total is visible to client-facing routes
|
||||
|
||||
import { db } from "@/db";
|
||||
import { quote_items, clients, service_catalog } from "@/db/schema";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
|
||||
async function requireAdmin() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new Error("Non autorizzato");
|
||||
}
|
||||
|
||||
const quoteItemSchema = z.object({
|
||||
service_id: z.string().nullable(),
|
||||
custom_label: z.string().nullable(),
|
||||
quantity: z.coerce.number().min(0.01, "Quantità deve essere > 0"),
|
||||
unit_price: z.coerce.number().min(0.01, "Prezzo deve essere > 0"),
|
||||
});
|
||||
|
||||
export async function addQuoteItem(clientId: string, formData: FormData) {
|
||||
await requireAdmin();
|
||||
|
||||
const rawServiceId = formData.get("service_id") as string | null;
|
||||
const rawCustomLabel = formData.get("custom_label") as string | null;
|
||||
|
||||
const parsed = quoteItemSchema.safeParse({
|
||||
service_id: rawServiceId && rawServiceId !== "" ? rawServiceId : null,
|
||||
custom_label: rawCustomLabel && rawCustomLabel !== "" ? rawCustomLabel : null,
|
||||
quantity: formData.get("quantity"),
|
||||
unit_price: formData.get("unit_price"),
|
||||
});
|
||||
|
||||
if (!parsed.success) throw new Error(parsed.error.issues[0].message);
|
||||
|
||||
// Validate: either service_id or custom_label must be present
|
||||
if (!parsed.data.service_id && !parsed.data.custom_label) {
|
||||
throw new Error(
|
||||
"Seleziona un servizio dal catalogo o inserisci il nome di una voce libera"
|
||||
);
|
||||
}
|
||||
|
||||
const { service_id, custom_label, quantity, unit_price } = parsed.data;
|
||||
const subtotal = (quantity * unit_price).toFixed(2);
|
||||
|
||||
await db.insert(quote_items).values({
|
||||
client_id: clientId,
|
||||
service_id: service_id ?? null,
|
||||
custom_label: custom_label ?? null,
|
||||
quantity: quantity.toFixed(2),
|
||||
unit_price: unit_price.toFixed(2),
|
||||
subtotal,
|
||||
});
|
||||
|
||||
revalidatePath(`/admin/clients/${clientId}`);
|
||||
}
|
||||
|
||||
export async function removeQuoteItem(quoteItemId: string, clientId: string) {
|
||||
await requireAdmin();
|
||||
await db.delete(quote_items).where(eq(quote_items.id, quoteItemId));
|
||||
revalidatePath(`/admin/clients/${clientId}`);
|
||||
}
|
||||
|
||||
export async function updateAcceptedTotal(clientId: string, formData: FormData) {
|
||||
await requireAdmin();
|
||||
const raw = (formData.get("accepted_total") as string)?.trim();
|
||||
const val = parseFloat(raw);
|
||||
if (isNaN(val) || val < 0) throw new Error("Importo non valido");
|
||||
await db
|
||||
.update(clients)
|
||||
.set({ accepted_total: val.toFixed(2) })
|
||||
.where(eq(clients.id, clientId));
|
||||
revalidatePath(`/admin/clients/${clientId}`);
|
||||
}
|
||||
Reference in New Issue
Block a user