diff --git a/src/app/admin/clients/[id]/page.tsx b/src/app/admin/clients/[id]/page.tsx
index cc0d23e..32447ac 100644
--- a/src/app/admin/clients/[id]/page.tsx
+++ b/src/app/admin/clients/[id]/page.tsx
@@ -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 (
@@ -59,6 +60,7 @@ export default async function ClientDetailPage({
Pagamenti
Documenti
Commenti
+ Preventivo
@@ -81,6 +83,14 @@ export default async function ClientDetailPage({
+
+
+
);
diff --git a/src/components/admin/tabs/QuoteTab.tsx b/src/components/admin/tabs/QuoteTab.tsx
new file mode 100644
index 0000000..2674e1a
--- /dev/null
+++ b/src/components/admin/tabs/QuoteTab.tsx
@@ -0,0 +1,333 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ addQuoteItem,
+ removeQuoteItem,
+ updateAcceptedTotal,
+} from "@/app/admin/clients/[id]/quote-actions";
+import type { QuoteItemWithLabel } from "@/lib/admin-queries";
+import type { ServiceCatalog } from "@/db/schema";
+
+type Props = {
+ clientId: string;
+ items: QuoteItemWithLabel[];
+ activeServices: ServiceCatalog[];
+ acceptedTotal: string;
+};
+
+export function QuoteTab({ clientId, items, activeServices, acceptedTotal }: Props) {
+ const [showCustom, setShowCustom] = useState(false);
+ const [addError, setAddError] = useState(null);
+ const [totalError, setTotalError] = useState(null);
+ // For catalog mode: pre-fill unit_price when service is selected
+ const [selectedServicePrice, setSelectedServicePrice] = useState("");
+ const [, startTransition] = useTransition();
+ const router = useRouter();
+
+ const calculatedTotal = items.reduce(
+ (sum, item) => sum + parseFloat(item.subtotal),
+ 0
+ );
+
+ function handleAddItem(fd: FormData) {
+ setAddError(null);
+ startTransition(async () => {
+ try {
+ await addQuoteItem(clientId, fd);
+ router.refresh();
+ } catch (e) {
+ setAddError(e instanceof Error ? e.message : "Errore nell'aggiunta");
+ }
+ });
+ }
+
+ function handleRemove(quoteItemId: string) {
+ startTransition(async () => {
+ await removeQuoteItem(quoteItemId, clientId);
+ router.refresh();
+ });
+ }
+
+ function handleSaveTotal(fd: FormData) {
+ setTotalError(null);
+ startTransition(async () => {
+ try {
+ await updateAcceptedTotal(clientId, fd);
+ router.refresh();
+ } catch (e) {
+ setTotalError(
+ e instanceof Error ? e.message : "Errore nel salvataggio"
+ );
+ }
+ });
+ }
+
+ return (
+
+
+ {/* Section 1: Add items */}
+
+
+ Aggiungi voci
+
+
+ {!showCustom ? (
+ /* Catalog mode */
+
+ ) : (
+ /* Freeform mode */
+
+ )}
+
+
+ {/* Section 2: Quote items table + calculated total */}
+
+
+ Voci preventivo
+
+ {items.length === 0 ? (
+
+ Nessuna voce aggiunta. Seleziona dal catalogo per iniziare.
+
+ ) : (
+ <>
+
+
+
+
+ |
+ Voce
+ |
+
+ Qty
+ |
+
+ Prezzo unit.
+ |
+
+ Subtotale
+ |
+ |
+
+
+
+ {items.map((item) => (
+
+ | {item.label} |
+
+ {parseFloat(item.quantity).toLocaleString("it-IT", {
+ minimumFractionDigits: 2,
+ })}
+ |
+
+ €
+ {parseFloat(item.unit_price).toLocaleString("it-IT", {
+ minimumFractionDigits: 2,
+ })}
+ |
+
+ €
+ {parseFloat(item.subtotal).toLocaleString("it-IT", {
+ minimumFractionDigits: 2,
+ })}
+ |
+
+
+ |
+
+ ))}
+
+
+
+
+
+ Totale calcolato: €
+ {calculatedTotal.toLocaleString("it-IT", {
+ minimumFractionDigits: 2,
+ })}
+
+
+ >
+ )}
+
+
+ {/* Section 3: Accepted total */}
+
+
+ Totale accettato dal cliente
+
+
+ {totalError &&
{totalError}
}
+
+ Il cliente vede solo questo importo, non le singole voci del preventivo.
+
+
+
+
+ );
+}