diff --git a/src/app/quote/[token]/actions.ts b/src/app/quote/[token]/actions.ts new file mode 100644 index 0000000..c782b5f --- /dev/null +++ b/src/app/quote/[token]/actions.ts @@ -0,0 +1,119 @@ +"use server"; + +import { z } from "zod"; +import { eq } from "drizzle-orm"; +import { db } from "@/db"; +import { quotes } from "@/db/schema"; +import { acceptQuoteSchema } from "@/lib/quote-validators"; +import { revalidatePath } from "next/cache"; + +export async function acceptQuote( + token: string, + email?: string, + notes?: string +) { + // Validate input + const parsed = acceptQuoteSchema.safeParse({ token, email, notes }); + if (!parsed.success) { + return { + success: false, + error: parsed.error.issues[0]?.message || "Dati invalidi", + }; + } + + const { token: validatedToken, email: validatedEmail, notes: validatedNotes } = parsed.data; + + try { + // Fetch quote + const [quote] = await db + .select() + .from(quotes) + .where(eq(quotes.token, validatedToken)) + .limit(1); + + if (!quote) { + return { success: false, error: "Preventivo non trovato" }; + } + + // Check if already accepted (immutability) + if (quote.accepted_at !== null) { + return { success: false, error: "Preventivo già accettato" }; + } + + // Atomic update: set accepted_at (immutable) + optional email/notes + const updated = await db + .update(quotes) + .set({ + accepted_at: new Date(), + state: "accepted", + client_email: validatedEmail || null, + client_notes: validatedNotes || null, + updated_at: new Date(), + }) + .where(eq(quotes.token, validatedToken)) + .returning(); + + if (!updated[0]) { + return { success: false, error: "Errore nel salvataggio" }; + } + + // Revalidate page to show accepted state + revalidatePath(`/quote/${validatedToken}`); + + // Phase 11 will hook into this event for auto-provisioning + // For now, just return success + return { + success: true, + message: "Preventivo accettato con successo!", + quoteId: quote.id, + }; + } catch (error) { + console.error("acceptQuote error:", error); + return { success: false, error: "Errore del server" }; + } +} + +export async function rejectQuote(token: string, notes?: string) { + // Validate token format + if (!token || typeof token !== "string" || token.length !== 21) { + return { success: false, error: "Token invalido" }; + } + + try { + // Fetch quote + const [quote] = await db + .select() + .from(quotes) + .where(eq(quotes.token, token)) + .limit(1); + + if (!quote) { + return { success: false, error: "Preventivo non trovato" }; + } + + // Update state to rejected + const updated = await db + .update(quotes) + .set({ + state: "rejected", + client_notes: notes || null, + updated_at: new Date(), + }) + .where(eq(quotes.token, token)) + .returning(); + + if (!updated[0]) { + return { success: false, error: "Errore nel salvataggio" }; + } + + revalidatePath(`/quote/${token}`); + + return { + success: true, + message: "Preventivo rifiutato", + }; + } catch (error) { + console.error("rejectQuote error:", error); + return { success: false, error: "Errore del server" }; + } +} diff --git a/src/app/quote/[token]/layout.tsx b/src/app/quote/[token]/layout.tsx new file mode 100644 index 0000000..ea7330a --- /dev/null +++ b/src/app/quote/[token]/layout.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from "react"; + +export default function QuoteLayout({ children }: { children: ReactNode }) { + return ( +
Accetta il tuo preventivo personalizzato
++ Questo preventivo è stato accettato il{" "} + + {new Date(quote.accepted_at).toLocaleDateString("it-IT", { + year: "numeric", + month: "long", + day: "numeric", + })} + +
++ Hai rifiutato questo preventivo. Se desideri discutere una proposta diversa, + contattaci direttamente. +
+