--- 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