97f58d23d2
- /admin/leads list page with LeadTable component (all leads with status badges) - /admin/leads/[id] detail page with full lead profile and activity log - LeadForm component for creating/editing leads (modal dialogs) - Server actions: createLead, updateLead, deleteLead with Zod validation - Admin sidebar updated to include Lead link - Added missing UI dependencies: @radix-ui/react-dialog, date-fns, Form component Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
"use server";
|
|
|
|
import { z } from "zod";
|
|
import { db } from "@/db";
|
|
import { leads, quotes } from "@/db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { createLeadSchema, updateLeadSchema, createActivitySchema } from "@/lib/lead-validators";
|
|
import { revalidatePath } from "next/cache";
|
|
import { createActivity, updateLeadStage } from "@/lib/lead-service";
|
|
|
|
export async function createLead(data: z.infer<typeof createLeadSchema>) {
|
|
const parsed = createLeadSchema.safeParse(data);
|
|
|
|
if (!parsed.success) {
|
|
return { success: false, error: parsed.error.issues[0].message };
|
|
}
|
|
|
|
try {
|
|
const [lead] = await db
|
|
.insert(leads)
|
|
.values({
|
|
name: parsed.data.name,
|
|
email: parsed.data.email || null,
|
|
phone: parsed.data.phone || null,
|
|
company: parsed.data.company || null,
|
|
status: parsed.data.status || "contacted",
|
|
notes: parsed.data.notes || null,
|
|
})
|
|
.returning();
|
|
|
|
revalidatePath("/admin/leads");
|
|
return { success: true, lead };
|
|
} catch (error) {
|
|
console.error("createLead error:", error);
|
|
return { success: false, error: "Errore nella creazione del lead" };
|
|
}
|
|
}
|
|
|
|
export async function updateLead(id: string, data: z.infer<typeof updateLeadSchema>) {
|
|
const parsed = updateLeadSchema.safeParse(data);
|
|
|
|
if (!parsed.success) {
|
|
return { success: false, error: parsed.error.issues[0].message };
|
|
}
|
|
|
|
try {
|
|
const updateData: Record<string, any> = { updated_at: new Date() };
|
|
|
|
if (parsed.data.name !== undefined) updateData.name = parsed.data.name;
|
|
if (parsed.data.email !== undefined) updateData.email = parsed.data.email || null;
|
|
if (parsed.data.phone !== undefined) updateData.phone = parsed.data.phone || null;
|
|
if (parsed.data.company !== undefined) updateData.company = parsed.data.company || null;
|
|
if (parsed.data.status !== undefined) updateData.status = parsed.data.status;
|
|
if (parsed.data.notes !== undefined) updateData.notes = parsed.data.notes || null;
|
|
|
|
const [updated] = await db
|
|
.update(leads)
|
|
.set(updateData)
|
|
.where(eq(leads.id, id))
|
|
.returning();
|
|
|
|
revalidatePath("/admin/leads");
|
|
revalidatePath(`/admin/leads/${id}`);
|
|
return { success: true, lead: updated };
|
|
} catch (error) {
|
|
console.error("updateLead error:", error);
|
|
return { success: false, error: "Errore nell'aggiornamento" };
|
|
}
|
|
}
|
|
|
|
export async function deleteLead(id: string) {
|
|
try {
|
|
await db.delete(leads).where(eq(leads.id, id));
|
|
revalidatePath("/admin/leads");
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error("deleteLead error:", error);
|
|
return { success: false, error: "Errore nell'eliminazione" };
|
|
}
|
|
}
|
|
|
|
// Activity logging
|
|
export async function logActivity(data: z.infer<typeof createActivitySchema>) {
|
|
const parsed = createActivitySchema.safeParse(data);
|
|
|
|
if (!parsed.success) {
|
|
return { success: false, error: parsed.error.issues[0].message };
|
|
}
|
|
|
|
try {
|
|
const activity = await createActivity({
|
|
lead_id: parsed.data.lead_id,
|
|
type: parsed.data.type,
|
|
activity_date: new Date(parsed.data.activity_date),
|
|
duration_minutes: parsed.data.duration_minutes,
|
|
notes: parsed.data.notes,
|
|
});
|
|
|
|
revalidatePath(`/admin/leads/${parsed.data.lead_id}`);
|
|
return { success: true, activity };
|
|
} catch (error) {
|
|
console.error("logActivity error:", error);
|
|
return { success: false, error: "Errore nella registrazione" };
|
|
}
|
|
}
|
|
|
|
// Quote assignment
|
|
const assignQuoteSchema = z.object({
|
|
lead_id: z.string().min(1),
|
|
quote_token: z.string().optional(),
|
|
generate_new: z.boolean(),
|
|
});
|
|
|
|
export async function assignQuoteToLead(data: z.infer<typeof assignQuoteSchema>) {
|
|
const parsed = assignQuoteSchema.safeParse(data);
|
|
|
|
if (!parsed.success) {
|
|
return { success: false, error: parsed.error.issues[0].message };
|
|
}
|
|
|
|
try {
|
|
if (parsed.data.generate_new) {
|
|
// Redirect handled on client; this is a no-op
|
|
return { success: true };
|
|
}
|
|
|
|
// Link quote to lead and update lead status
|
|
const token = parsed.data.quote_token;
|
|
const leadId = parsed.data.lead_id;
|
|
|
|
if (!token) {
|
|
return { success: false, error: "Token preventivo richiesto" };
|
|
}
|
|
|
|
// Find quote by token
|
|
const [quote] = await db
|
|
.select()
|
|
.from(quotes)
|
|
.where(eq(quotes.token, token))
|
|
.limit(1);
|
|
|
|
if (!quote) {
|
|
return { success: false, error: "Preventivo non trovato" };
|
|
}
|
|
|
|
// Update quote to link to lead (if not already linked)
|
|
await db
|
|
.update(quotes)
|
|
.set({ lead_id: leadId })
|
|
.where(eq(quotes.id, quote.id));
|
|
|
|
// Update lead status to "proposal_sent"
|
|
await updateLeadStage(leadId, "proposal_sent");
|
|
|
|
revalidatePath(`/admin/leads/${leadId}`);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error("assignQuoteToLead error:", error);
|
|
return { success: false, error: "Errore nell'assegnazione" };
|
|
}
|
|
}
|