From 9ea905cb4515043e7588f08d53388697094d1f10 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 11 Jun 2026 07:20:37 +0200 Subject: [PATCH] =?UTF-8?q?docs(10):=20phase=2010=20planning=20complete=20?= =?UTF-8?q?=E2=80=94=203=20plans=20(CRM=20leads=20+=20activity=20logging?= =?UTF-8?q?=20+=20follow-ups)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/ROADMAP.md | 8 +- .../10-01-PLAN.md | 438 ++++++++++ .../10-02-PLAN.md | 772 ++++++++++++++++++ .../10-03-PLAN.md | 639 +++++++++++++++ 4 files changed, 1855 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/10-crm-pipeline-activity-logging/10-01-PLAN.md create mode 100644 .planning/phases/10-crm-pipeline-activity-logging/10-02-PLAN.md create mode 100644 .planning/phases/10-crm-pipeline-activity-logging/10-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 273977b..8407b05 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -219,7 +219,11 @@ Decimal phases appear between their surrounding integers in numeric order. 7. "Send Quote" button: select/create quote → update lead.status → "Proposal Sent" **Plans**: 3 (lead CRUD + activity logging + follow-up widget) **Durata**: 3–4 giorni -**Status**: Pending planning +**Plan list**: + - [ ] 10-01-PLAN.md — Schema foundation (leads, activities, reminders tables + query layer) + - [ ] 10-02-PLAN.md — Lead CRUD UI (/admin/leads list + detail page + forms) + - [ ] 10-03-PLAN.md — Activity logging + send quote + follow-up reminders widget +**Status**: ✅ Planning complete ### Phase 11: Auto-Provisioning on Win **Goal**: Lead → quote accept → auto-crea client + progetto (copia fasi) + 1-4 pagamenti; lancia dashboard cliente @@ -265,7 +269,7 @@ Phases 7 → 8 → 9 → 10 → 11 planned for sequential execution. Research co | 7. Unified Service Catalog | 2 | ✅ Planning complete | 3–4 giorni | Foundation | | 8. Offer Phases & Quote Templates | 1 | ✅ Planning complete | 1–2 giorni | Foundation | | 9. Quote Builder & Public Routes | 3 | Pending planning | 4–5 giorni | RHF compat test | -| 10. CRM Pipeline & Activity Logging | 3 | Pending planning | 3–4 giorni | No | +| 10. CRM Pipeline & Activity Logging | 3 | ✅ Planning complete | 3–4 giorni | No | | 11. Auto-Provisioning on Win | 2 | Pending planning | 2–3 giorni | BullMQ ops (optional) | | **Total v2.0 MVP** | **11** | Pending | **13–18 giorni** | — | diff --git a/.planning/phases/10-crm-pipeline-activity-logging/10-01-PLAN.md b/.planning/phases/10-crm-pipeline-activity-logging/10-01-PLAN.md new file mode 100644 index 0000000..0f428f7 --- /dev/null +++ b/.planning/phases/10-crm-pipeline-activity-logging/10-01-PLAN.md @@ -0,0 +1,438 @@ +--- +phase: 10 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/db/schema.ts + - src/lib/lead-validators.ts + - src/lib/lead-service.ts +autonomous: true +requirements: + - CRM-01 + - CRM-02 + - CRM-03 + - CRM-04 +--- + + +Expand Phase 10 CRM Foundation: Complete the `leads`, `activities`, and `reminders` tables in the database schema. Establish pipeline stage enums, activity type definitions, and query layer patterns for lead management and activity logging. + +Purpose: Create the schema foundation required by Phase 10 UI (Lead CRUD, pipeline view, activity log, follow-up widget). These tables enable full-featured CRM operations including lead tracking, interaction history, and reminder scheduling. + +Output: +- Complete `leads` table with CRM-required fields (name, email, phone, company, status/stage, last_contact_date, next_action) +- `activities` table (timestamped records of interactions: calls, emails, meetings, notes) +- `reminders` table (follow-up reminders with due dates and completion state) +- TypeScript types and query layer (Lead, Activity, Reminder; stage constants) +- Database relations wired to leads from quotes (many quotes per lead) + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md + + + + + + Task 1: Expand leads table and add activities, reminders tables to schema + src/db/schema.ts + +Update the database schema by expanding the `leads` table and adding two new tables for CRM operations. + +**1. Expand `leads` table** (after current definition, line 363): + +Replace the placeholder definition with full CRM fields: +```typescript +export const leads = pgTable("leads", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + email: text("email"), + phone: text("phone"), + company: text("company"), + status: text("status") + .notNull() + .default("contacted"), // contacted | qualified | proposal_sent | negotiating | won | lost + last_contact_date: timestamp("last_contact_date", { withTimezone: true }), + next_action: text("next_action"), + next_action_date: timestamp("next_action_date", { withTimezone: true }), + notes: text("notes"), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +``` + +**2. Add `activities` table** (after leads table): +```typescript +export const activities = pgTable("activities", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + lead_id: text("lead_id") + .notNull() + .references(() => leads.id, { onDelete: "cascade" }), + type: text("type") + .notNull(), // call | email | meeting | note + duration_minutes: integer("duration_minutes"), // optional, for calls/meetings + notes: text("notes").notNull(), + activity_date: timestamp("activity_date", { withTimezone: true }) + .notNull(), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +``` + +**3. Add `reminders` table** (after activities table): +```typescript +export const reminders = pgTable("reminders", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + lead_id: text("lead_id") + .notNull() + .references(() => leads.id, { onDelete: "cascade" }), + title: text("title").notNull(), + description: text("description"), + due_date: timestamp("due_date", { withTimezone: true }) + .notNull(), + completed_at: timestamp("completed_at", { withTimezone: true }), + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +``` + +**4. Update `quotes` table relation to lead** (already exists, verify at line 341): + +Ensure `lead_id` is present and FK to leads.id. The placeholder was already added in Phase 8; just verify it remains. + +**5. Add relations** (after existing relations, before closing exports): + +```typescript +export const leadsRelations = relations(leads, ({ many }) => ({ + quotes: many(quotes), + activities: many(activities), + reminders: many(reminders), +})); + +export const activitiesRelations = relations(activities, ({ one }) => ({ + lead: one(leads, { fields: [activities.lead_id], references: [leads.id] }), +})); + +export const remindersRelations = relations(reminders, ({ one }) => ({ + lead: one(leads, { fields: [reminders.lead_id], references: [leads.id] }), +})); + +export const quotesRelations = relations(quotes, ({ one, many }) => ({ + lead: one(leads, { fields: [quotes.lead_id], references: [leads.id] }), + client: one(clients, { fields: [quotes.client_id], references: [clients.id] }), + offerMicro: one(offer_micros, { fields: [quotes.offer_micro_id], references: [offer_micros.id] }), + items: many(quote_items), +})); +``` + +**Data Safety:** The placeholder leads table already exists; this task expands it in-place with new columns. No existing data is deleted — backwards compatible. + + + npm run build 2>&1 | grep -c "error" | grep -q 0 && echo "TypeScript compile pass" + + Schema file updated with expanded leads table, activities and reminders tables, and all relations properly configured. npm run build passes. No placeholder data lost — fully backward compatible. + + + + Task 2: Create Drizzle migration for CRM tables + src/db/migrations + +Run drizzle-kit to auto-generate migration files for the new CRM tables and schema changes: + +```bash +npx drizzle-kit generate --name=phase-10-crm-leads-activities-reminders +``` + +This creates a new migration file in `src/db/migrations/` with SQL for the expanded leads table and new activities/reminders tables. The migration is NOT applied yet (Phase 10 execution will push to DB). + +Validate the generated migration: +- Ensure leads table ALTER statement (adding phone, company, status, last_contact_date, etc.) is correct +- Ensure activities table has FK to leads with CASCADE delete +- Ensure reminders table has FK to leads with CASCADE delete +- Ensure quotes table lead_id FK exists (may already be present from Phase 8) +- Ensure all NOT NULL constraints match schema + +If migration looks incorrect, manually edit the SQL in `src/db/migrations/{migration_file}.sql` to fix. + +Check for syntax errors: +```bash +head -50 src/db/migrations/phase-10-crm-leads-activities-reminders.sql +``` + + + ls src/db/migrations/ | grep phase-10 | head -1 + + Migration file generated and located in src/db/migrations/. SQL is syntactically valid and includes all table and relation changes. + + + + Task 3: Add lead validators (Zod schemas) and activity/reminder query layer + src/lib/lead-validators.ts, src/lib/lead-service.ts + +Create two new library files for lead operations: + +**src/lib/lead-validators.ts** — Zod schemas for CRM operations: +```typescript +import { z } from "zod"; + +// Pipeline stages — enum match database default values +export const LEAD_STAGES = ["contacted", "qualified", "proposal_sent", "negotiating", "won", "lost"] as const; +export const ACTIVITY_TYPES = ["call", "email", "meeting", "note"] as const; + +// Lead creation (admin) +export const createLeadSchema = z.object({ + name: z.string().min(1, "Nome richiesto").max(100), + email: z.string().email("Email valida").optional().or(z.literal("")), + phone: z.string().optional().or(z.literal("")), + company: z.string().optional().or(z.literal("")), + status: z.enum(LEAD_STAGES).default("contacted"), + notes: z.string().optional().or(z.literal("")), +}); + +// Lead update +export const updateLeadSchema = createLeadSchema.partial().required({ status: false }); + +// Activity logging +export const createActivitySchema = z.object({ + lead_id: z.string().min(1, "Lead richiesto"), + type: z.enum(ACTIVITY_TYPES), + activity_date: z.date().or(z.string()), + duration_minutes: z.number().min(0).optional(), + notes: z.string().min(1, "Note richieste").max(1000), +}); + +// Reminder creation +export const createReminderSchema = z.object({ + lead_id: z.string().min(1, "Lead richiesto"), + title: z.string().min(1, "Titolo richiesto").max(200), + description: z.string().optional(), + due_date: z.date().or(z.string()), +}); + +// Change lead stage +export const updateLeadStageSchema = z.object({ + lead_id: z.string().min(1), + stage: z.enum(LEAD_STAGES), +}); +``` + +**src/lib/lead-service.ts** — Query layer for leads, activities, and reminders: +```typescript +import { eq, and, isNull, desc, gte, lte, ilike } from "drizzle-orm"; +import { db } from "@/db"; +import { leads, activities, reminders, quotes } from "@/db/schema"; + +// Get all leads with counts of quotes and upcoming reminders +export async function getAllLeads() { + return await db + .select() + .from(leads) + .orderBy(desc(leads.updated_at)); +} + +// Get lead by ID with related quotes and activity count +export async function getLeadById(id: string) { + const [lead] = await db + .select() + .from(leads) + .where(eq(leads.id, id)); + + return lead; +} + +// Get leads by stage (for pipeline view) +export async function getLeadsByStage(stage: string) { + return await db + .select() + .from(leads) + .where(eq(leads.status, stage)) + .orderBy(desc(leads.last_contact_date)); +} + +// Get leads that need follow-up (last contact > 7 days ago) +export async function getLeadsNeedingFollowUp(daysAgo: number = 7) { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - daysAgo); + + return await db + .select() + .from(leads) + .where( + and( + isNull(leads.last_contact_date), + gte(leads.created_at, cutoffDate) + ) + ) + .orderBy(leads.created_at); +} + +// Get activity log for a lead (reverse chronological) +export async function getActivityLog(leadId: string) { + return await db + .select() + .from(activities) + .where(eq(activities.lead_id, leadId)) + .orderBy(desc(activities.activity_date)); +} + +// Get upcoming reminders for a lead +export async function getUpcomingReminders(leadId: string) { + return await db + .select() + .from(reminders) + .where( + and( + eq(reminders.lead_id, leadId), + isNull(reminders.completed_at) + ) + ) + .orderBy(reminders.due_date); +} + +// Get all overdue reminders (admin widget) +export async function getOverdueReminders() { + const now = new Date(); + return await db + .select() + .from(reminders) + .innerJoin(leads, eq(reminders.lead_id, leads.id)) + .where( + and( + lte(reminders.due_date, now), + isNull(reminders.completed_at) + ) + ) + .orderBy(reminders.due_date); +} + +// Create activity and auto-update lead.last_contact_date +export async function createActivity(data: { + lead_id: string; + type: string; + activity_date: Date; + duration_minutes?: number; + notes: string; +}) { + const [activity] = await db + .insert(activities) + .values(data) + .returning(); + + // Auto-update lead.last_contact_date + await db + .update(leads) + .set({ last_contact_date: data.activity_date, updated_at: new Date() }) + .where(eq(leads.id, data.lead_id)); + + return activity; +} + +// Update lead stage +export async function updateLeadStage(leadId: string, stage: string) { + const [updated] = await db + .update(leads) + .set({ status: stage, updated_at: new Date() }) + .where(eq(leads.id, leadId)) + .returning(); + + return updated; +} + +// Search leads by name, email, or company +export async function searchLeads(query: string) { + return await db + .select() + .from(leads) + .where( + or( + ilike(leads.name, `%${query}%`), + ilike(leads.email, `%${query}%`), + ilike(leads.company, `%${query}%`) + ) + ) + .orderBy(desc(leads.updated_at)); +} +``` + +These skeleton implementations provide the foundation. Phase 10 Tasks 2-3 will flesh out UI integration and form handling. + + + npm run build 2>&1 | grep -c "error" | grep -q 0 && echo "Validators compile" + + Lead validators and service layer created. Schemas handle stage/type enums, activity logging, and reminder queries. Query layer is paginated for scalability. npm run build passes. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Admin → Lead Data | Session-authenticated via Auth.js; admin actions return full lead details and activity history | +| Lead ID references | All lead operations (activities, reminders) checked via lead_id FK; cascade delete prevents orphaned records | +| Activity date validation | Activity dates must be <= current date (no future backdating); server-side timestamp immutability | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-10-01 | Tampering | Activity history modification | mitigate | Activity records immutable after creation; no update endpoint; deletion requires admin auth + audit log | +| T-10-02 | Spoofing | Lead creation with fake company/contact info | mitigate | Email/phone not validated until win (Phase 11); accept unverified input at lead stage; verify on quote accept | +| T-10-03 | Information Disclosure | Leaking lead contact details (email, phone) via API | mitigate | Lead queries require Auth.js session; no public API for leads; admin-only access | +| T-10-04 | Denial of Service | Bulk activity creation spam | mitigate | No rate limit in Phase 10 MVP; add rate limiting per admin session if needed (Phase 12) | +| T-10-05 | Elevation | Non-admin user updating lead stage | mitigate | All lead mutations require Auth.js session; middleware guards /admin/leads routes | +| T-10-06 | Tampering | Backdating activities (future activity_date) | accept | No validation in Phase 10; assume admin is honest; add validation if abuse occurs | + + + + +After Phase 10 Plan 1 execution: + +1. Schema compiles: `npm run build` passes with no TS errors +2. Drizzle migration file generated: `src/db/migrations/` contains new migration +3. Lead validators import cleanly: `import { createLeadSchema, LEAD_STAGES } from '@/lib/lead-validators'` works +4. Query layer exports all functions: `getLeadById`, `getActivityLog`, `createActivity`, `updateLeadStage`, etc. +5. Relations updated: leads → activities (cascade), leads → reminders (cascade), quotes → leads (optional FK) + +Data safety check: +- Placeholder leads table expanded: NOT deleted, just new columns added +- New activities/reminders tables created from scratch: no existing data at risk +- quotes table lead_id FK already present from Phase 8: no schema conflict + + + +- `leads`, `activities`, `reminders` tables properly defined with all CRM fields +- TypeScript compiles cleanly; Lead/Activity/Reminder types exported +- Drizzle migration file generated (not yet applied to DB) +- Lead validators (Zod) handle stage enums, activity types, date validation +- Query layer provides CRUD + search operations for leads, activities, reminders +- Relations properly configured: leads ← activities, leads ← reminders, leads ← quotes +- Database schema is ready for Phase 10 UI (pipeline view, activity log, follow-up widget) + + + +After execution, create `.planning/phases/10-crm-pipeline-activity-logging/10-01-SUMMARY.md` + diff --git a/.planning/phases/10-crm-pipeline-activity-logging/10-02-PLAN.md b/.planning/phases/10-crm-pipeline-activity-logging/10-02-PLAN.md new file mode 100644 index 0000000..fc985f4 --- /dev/null +++ b/.planning/phases/10-crm-pipeline-activity-logging/10-02-PLAN.md @@ -0,0 +1,772 @@ +--- +phase: 10 +plan: 02 +type: execute +wave: 2 +depends_on: [10-01] +files_modified: + - src/app/admin/leads/page.tsx + - src/app/admin/leads/[id]/page.tsx + - src/components/admin/leads/LeadTable.tsx + - src/components/admin/leads/LeadForm.tsx + - src/components/admin/leads/PipelineKanban.tsx + - src/app/admin/leads/actions.ts +autonomous: true +requirements: + - CRM-01 + - CRM-02 + - CRM-03 +--- + + +Phase 10 CRM UI (Part 1): Implement Lead CRUD pages (/admin/leads) and Kanban pipeline view showing leads by stage. Establish form patterns for lead creation/editing and stage transitions via drag-and-drop. + +Purpose: Create the admin-facing lead management interface and pipeline view. Admins can create leads, view full lead details, and transition leads between pipeline stages visually. This enables the core CRM workflow: Contacted → Qualified → Proposal Sent → Negotiating → Won/Lost. + +Output: +- `/admin/leads` list page with table (name, email, company, status, last_contact_date, next_action) +- `/admin/leads/[id]` detail page with profilo, action menu (log activity, send quote, mark won) +- `/admin/leads/kanban` Kanban board view (drag-drop leads between 6 stages) +- LeadForm component (create + edit modal) +- Server actions for createLead, updateLead, updateLeadStage, deleteLead + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md +@.planning/phases/10-crm-pipeline-activity-logging/10-01-SUMMARY.md + + + +From src/lib/lead-validators.ts (created in Plan 1): +```typescript +export const LEAD_STAGES = ["contacted", "qualified", "proposal_sent", "negotiating", "won", "lost"] as const; +export const ACTIVITY_TYPES = ["call", "email", "meeting", "note"] as const; +export const createLeadSchema: ZodType<{ name, email?, phone?, company?, status?, notes? }>; +export const updateLeadSchema: ZodType>; +``` + +From src/lib/lead-service.ts (created in Plan 1): +```typescript +export async function getAllLeads(): Promise; +export async function getLeadById(id: string): Promise; +export async function getLeadsByStage(stage: string): Promise; +export async function createActivity(...): Promise; +export async function updateLeadStage(leadId, stage): Promise; +``` + +From src/db/schema.ts (expanded in Plan 1): +```typescript +export type Lead = typeof leads.$inferSelect; +export type NewLead = typeof leads.$inferInsert; +export type Activity = typeof activities.$inferSelect; +``` + + + + + + Task 1: Create /admin/leads list page and LeadTable component + src/app/admin/leads/page.tsx, src/components/admin/leads/LeadTable.tsx + +**Step 1: Create `/admin/leads` list page** + +File: `src/app/admin/leads/page.tsx` + +```typescript +import { Suspense } from "react"; +import { getAllLeads } from "@/lib/lead-service"; +import { LeadTable } from "@/components/admin/leads/LeadTable"; +import { CreateLeadModal } from "@/components/admin/leads/LeadForm"; +import { Button } from "@/components/ui/button"; + +async function LeadsList() { + const leads = await getAllLeads(); + + return ( +
+
+

Lead Pipeline

+ +
+ + Loading...
}> + + + + ); +} + +export default function LeadsPage() { + return ; +} +``` + +**Step 2: Create LeadTable component** + +File: `src/components/admin/leads/LeadTable.tsx` + +```typescript +"use client"; + +import Link from "next/link"; +import { Lead } from "@/db/schema"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { formatDistanceToNow } from "date-fns"; +import { LEAD_STAGES } from "@/lib/lead-validators"; + +const STAGE_COLOR = { + contacted: "bg-blue-100 text-blue-800", + qualified: "bg-purple-100 text-purple-800", + proposal_sent: "bg-amber-100 text-amber-800", + negotiating: "bg-orange-100 text-orange-800", + won: "bg-green-100 text-green-800", + lost: "bg-red-100 text-red-800", +}; + +export function LeadTable({ leads }: { leads: Lead[] }) { + return ( +
+ + + + Nome + Email + Azienda + Stato + Ultimo Contatto + Prossima Azione + + + + + {leads.map((lead) => ( + + + + {lead.name} + + + {lead.email || "—"} + {lead.company || "—"} + + + {lead.status.replace("_", " ")} + + + + {lead.last_contact_date + ? formatDistanceToNow(new Date(lead.last_contact_date), { addSuffix: true }) + : "—"} + + {lead.next_action || "—"} + + + + + ))} + +
+ {leads.length === 0 && ( +
Nessun lead trovato
+ )} +
+ ); +} +``` + +**Design notes:** +- Table shows all required fields per CRM-01 (name, email, company, status, last_contact_date, next_action) +- Status badge color-coded by stage (blue=contacted, purple=qualified, amber=proposal_sent, orange=negotiating, green=won, red=lost) +- Click row to navigate to lead detail page +- "Create Lead" button opens modal form +
+ + npm run build 2>&1 | grep -c "error" | grep -q 0 && echo "Leads list page compiles" + + LeadTable component and /admin/leads page created. Table displays all leads with status badge coloring, last contact date, and navigation links to detail page. +
+ + + Task 2: Create /admin/leads/[id] detail page with activity log and action menu + src/app/admin/leads/[id]/page.tsx, src/components/admin/leads/LeadDetail.tsx + +File: `src/app/admin/leads/[id]/page.tsx` + +```typescript +import { notFound } from "next/navigation"; +import { getLeadById, getActivityLog, getUpcomingReminders } from "@/lib/lead-service"; +import { LeadDetail } from "@/components/admin/leads/LeadDetail"; + +export default async function LeadDetailPage({ params }: { params: { id: string } }) { + const lead = await getLeadById(params.id); + + if (!lead) { + notFound(); + } + + const activities = await getActivityLog(params.id); + const reminders = await getUpcomingReminders(params.id); + + return ( + + ); +} +``` + +File: `src/components/admin/leads/LeadDetail.tsx` + +```typescript +"use client"; + +import { Lead, Activity, Reminder } from "@/db/schema"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { formatDistanceToNow, format } from "date-fns"; +import { LogActivityModal } from "./LogActivityModal"; +import { SendQuoteModal } from "./SendQuoteModal"; +import { it } from "date-fns/locale"; + +const ACTIVITY_ICON = { + call: "📞", + email: "📧", + meeting: "📅", + note: "📝", +}; + +export function LeadDetail({ + lead, + activities, + reminders, +}: { + lead: Lead; + activities: Activity[]; + reminders: Reminder[]; +}) { + return ( +
+ {/* Header + Actions */} +
+
+

{lead.name}

+

{lead.company || "Azienda non specificata"}

+
+
+ + + +
+
+ +
+ {/* Lead Profile Card */} + + + Profilo + + +
+ + {lead.status.replace("_", " ")} +
+
+ +

{lead.email || "—"}

+
+
+ +

{lead.phone || "—"}

+
+
+ +

+ {lead.last_contact_date + ? formatDistanceToNow(new Date(lead.last_contact_date), { + addSuffix: true, + locale: it, + }) + : "—"} +

+
+
+ +

{lead.next_action || "—"}

+
+
+
+ + {/* Upcoming Reminders Card */} + + + Prossimi Follow-up + + + {reminders.length > 0 ? ( +
    + {reminders.map((r) => ( +
  • +

    {r.title}

    +

    + {format(new Date(r.due_date), "dd MMM yyyy", { locale: it })} +

    +
  • + ))} +
+ ) : ( +

Nessun reminder

+ )} +
+
+ + {/* Notes Card */} + + + Note + + +

{lead.notes || "Nessuna nota"}

+
+
+
+ + {/* Activity Log */} + + + Storico Attività + + + {activities.length > 0 ? ( +
+ {activities.map((activity) => ( +
+
+ + {ACTIVITY_ICON[activity.type as keyof typeof ACTIVITY_ICON]} + + + {activity.type} + + + {formatDistanceToNow(new Date(activity.activity_date), { + addSuffix: true, + locale: it, + })} + +
+

{activity.notes}

+ {activity.duration_minutes && ( +

+ Durata: {activity.duration_minutes} minuti +

+ )} +
+ ))} +
+ ) : ( +

Nessuna attività registrata

+ )} +
+
+
+ ); +} +``` + +**Design notes:** +- Full lead profile on left (name, email, phone, company, status, next_action) +- Activity log in reverse chronological order with type icons (📞📧📅📝) +- Action buttons for log activity, send quote, mark as won +- Upcoming reminders list +- Notes section for free-form notes +
+ + npm run build 2>&1 | grep -c "error" | grep -q 0 && echo "Lead detail page compiles" + + Lead detail page and LeadDetail component created. Shows full lead profile, activity log (reverse chronological), upcoming reminders, and action menu (log activity, send quote, mark as won). +
+ + + Task 3: Create LeadForm component (create/edit modal) and server actions + src/components/admin/leads/LeadForm.tsx, src/app/admin/leads/actions.ts + +File: `src/components/admin/leads/LeadForm.tsx` + +```typescript +"use client"; + +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { createLeadSchema, LEAD_STAGES } from "@/lib/lead-validators"; +import { Lead } from "@/db/schema"; +import { createLead, updateLead } from "@/app/admin/leads/actions"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; + +type CreateLeadInput = z.infer; + +export function CreateLeadModal() { + const [open, setOpen] = useState(false); + const form = useForm({ + resolver: zodResolver(createLeadSchema), + defaultValues: { status: "contacted" }, + }); + + async function onSubmit(data: CreateLeadInput) { + const result = await createLead(data); + if (result.success) { + setOpen(false); + form.reset(); + // Toast success + } + } + + return ( + + + + + + + Nuovo Lead + +
+ + ( + + Nome + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + ( + + Telefono + + + + + + )} + /> + + ( + + Azienda + + + + + + )} + /> + + ( + + Stato + + + + )} + /> + + ( + + Note + +