From ab7fa62059d49f4ef6a7314233aaf5d536d53f89 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sun, 14 Jun 2026 13:21:55 +0200 Subject: [PATCH] feat(14-02): add LeadsSearch, rewire leads pages, surface tags in LeadDetail (CRM-08/09) - New LeadsSearch.tsx: client-side instant filter across name/email/ company/status/tags (mirrors CatalogSearch.tsx) - page.tsx: fetch via getLeadsWithTags()/getLeadFieldOptions(), render LeadsSearch + CreateLeadModal, revalidate=0 - [id]/page.tsx: fetch getLeadsWithTags()/getLeadFieldOptions(), find lead by id, pass tags/tagOptions to LeadDetail - LeadDetail.tsx: add OptionMultiSelect for lead tags in the "Profilo" card, wired to addLeadTag/removeLeadTag/renameLeadTag with error display --- src/app/admin/leads/LeadsSearch.tsx | 46 +++++++++++++++++++++++ src/app/admin/leads/[id]/page.tsx | 21 +++++++++-- src/app/admin/leads/page.tsx | 24 ++++++------ src/components/admin/leads/LeadDetail.tsx | 35 +++++++++++++++++ 4 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 src/app/admin/leads/LeadsSearch.tsx diff --git a/src/app/admin/leads/LeadsSearch.tsx b/src/app/admin/leads/LeadsSearch.tsx new file mode 100644 index 0000000..912c478 --- /dev/null +++ b/src/app/admin/leads/LeadsSearch.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Input } from "@/components/ui/input"; +import { Search } from "lucide-react"; +import { LeadTable } from "@/components/admin/leads/LeadTable"; +import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries"; + +export function LeadsSearch({ + leads, + options, +}: { + leads: LeadWithTags[]; + options: LeadFieldOptions; +}) { + const [query, setQuery] = useState(""); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return leads; + return leads.filter((l) => { + if (l.name.toLowerCase().includes(q)) return true; + if (l.email?.toLowerCase().includes(q)) return true; + if (l.company?.toLowerCase().includes(q)) return true; + if (l.status.toLowerCase().includes(q)) return true; + if (l.tags.some((t) => t.toLowerCase().includes(q))) return true; + return false; + }); + }, [leads, query]); + + return ( +
+
+ + setQuery(e.target.value)} + className="pl-9 h-9" + /> +
+ +
+ ); +} diff --git a/src/app/admin/leads/[id]/page.tsx b/src/app/admin/leads/[id]/page.tsx index 161f577..f9aee2f 100644 --- a/src/app/admin/leads/[id]/page.tsx +++ b/src/app/admin/leads/[id]/page.tsx @@ -1,10 +1,17 @@ import { notFound } from "next/navigation"; -import { getLeadById, getActivityLog, getUpcomingReminders } from "@/lib/lead-service"; +import { getActivityLog, getUpcomingReminders } from "@/lib/lead-service"; +import { getLeadsWithTags, getLeadFieldOptions } from "@/lib/admin-queries"; import { LeadDetail } from "@/components/admin/leads/LeadDetail"; export default async function LeadDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - const lead = await getLeadById(id); + + const [leads, options] = await Promise.all([ + getLeadsWithTags(), + getLeadFieldOptions(), + ]); + + const lead = leads.find((l) => l.id === id); if (!lead) { notFound(); @@ -13,5 +20,13 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: const activities = await getActivityLog(id); const reminders = await getUpcomingReminders(id); - return ; + return ( + + ); } diff --git a/src/app/admin/leads/page.tsx b/src/app/admin/leads/page.tsx index 1e3ba87..59158f4 100644 --- a/src/app/admin/leads/page.tsx +++ b/src/app/admin/leads/page.tsx @@ -1,25 +1,23 @@ -import { Suspense } from "react"; -import { getAllLeads } from "@/lib/lead-service"; -import { LeadTable } from "@/components/admin/leads/LeadTable"; +import { getLeadsWithTags, getLeadFieldOptions } from "@/lib/admin-queries"; +import { LeadsSearch } from "./LeadsSearch"; import { CreateLeadModal } from "@/components/admin/leads/LeadForm"; -async function LeadsList() { - const leads = await getAllLeads(); +export const revalidate = 0; + +export default async function LeadsPage() { + const [leads, options] = await Promise.all([ + getLeadsWithTags(), + getLeadFieldOptions(), + ]); return (
-

Lead Pipeline

+

Lead Pipeline

- Loading...
}> - - + ); } - -export default function LeadsPage() { - return ; -} diff --git a/src/components/admin/leads/LeadDetail.tsx b/src/components/admin/leads/LeadDetail.tsx index 2dec91b..df23b9e 100644 --- a/src/components/admin/leads/LeadDetail.tsx +++ b/src/components/admin/leads/LeadDetail.tsx @@ -1,9 +1,13 @@ "use client"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; 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 { OptionMultiSelect } from "@/components/ui/option-multi-select"; +import { addLeadTag, removeLeadTag, renameLeadTag } from "@/app/admin/leads/actions"; import { formatDistanceToNow, format } from "date-fns"; import { it } from "date-fns/locale"; import { LogActivityModal } from "./LogActivityModal"; @@ -30,11 +34,31 @@ export function LeadDetail({ lead, activities, reminders, + tags, + tagOptions, }: { lead: Lead; activities: Activity[]; reminders: Reminder[]; + tags: string[]; + tagOptions: string[]; }) { + const router = useRouter(); + const [, startTransition] = useTransition(); + const [tagError, setTagError] = useState(null); + + function run(fn: () => Promise) { + setTagError(null); + startTransition(async () => { + try { + await fn(); + router.refresh(); + } catch (e) { + setTagError(e instanceof Error ? e.message : "Errore nel salvataggio"); + } + }); + } + return (
{/* Header + Actions */} @@ -63,6 +87,17 @@ export function LeadDetail({ {lead.status.replace(/_/g, " ")}
+
+ + run(() => addLeadTag(lead.id, v))} + onRemove={(v) => run(() => removeLeadTag(lead.id, v))} + onRename={(o, n) => run(() => renameLeadTag(o, n))} + /> + {tagError &&

{tagError}

} +

{lead.email || "—"}