feat(10-03): add LogActivityModal component and logActivity server action

- Created LogActivityModal.tsx with form for registering activities (call/email/meeting/note)
- Added type dropdown, date picker, optional duration, notes textarea
- Integrated createActivitySchema validation from lead-validators
- Added logActivity server action to create activity and auto-update lead.last_contact_date
- Updated LeadDetail.tsx to import and render LogActivityModal button
- Revalidates lead detail page on successful activity creation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:36:46 +02:00
parent a453df5a8e
commit ec81a76f51
2 changed files with 247 additions and 2 deletions
+86 -2
View File
@@ -2,10 +2,11 @@
import { z } from "zod";
import { db } from "@/db";
import { leads } from "@/db/schema";
import { leads, quotes } from "@/db/schema";
import { eq } from "drizzle-orm";
import { createLeadSchema, updateLeadSchema } from "@/lib/lead-validators";
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);
@@ -80,3 +81,86 @@ export async function deleteLead(id: string) {
return { success: false, error: "Errore nell'eliminazione" };
}
}
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" };
}
}
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 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" };
}
}