feat: lead → client conversion + client contact fields (email/phone)

Blocco A+B (milestone v2.3). Migration 0011 is additive (ADD COLUMN only):
clients.email, clients.phone, leads.archived.

- createClientCore() extracted from createClient and reused by conversion
- clients.email/phone added to create + edit forms and shown on client header
  (never exposed on the public /client/[token] page)
- convertLeadToClient(): reuses core, carries over lead transcripts
  (client_transcripts.client_id), links project.created_from_lead_id,
  archives the lead while keeping its "won" status; idempotent
- "Converti in cliente" button on won leads; "Convertito → Apri cliente"
  once converted (clientId resolved via created_from_lead_id)
- archived leads hidden from list/kanban; detail page fetches by id incl. archived

Migration must be applied to the prod DB BEFORE this is deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 08:18:41 +02:00
parent e1b3e8c3d5
commit f5f90cd643
11 changed files with 273 additions and 44 deletions
+37 -1
View File
@@ -7,7 +7,13 @@ 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, deleteTranscript } from "@/app/admin/leads/actions";
import {
addLeadTag,
removeLeadTag,
renameLeadTag,
deleteTranscript,
convertLeadToClient,
} from "@/app/admin/leads/actions";
import { formatDistanceToNow, format } from "date-fns";
import { it } from "date-fns/locale";
import Link from "next/link";
@@ -39,6 +45,7 @@ export function LeadDetail({
tags,
tagOptions,
transcripts,
convertedClientId,
}: {
lead: Lead;
activities: Activity[];
@@ -46,13 +53,23 @@ export function LeadDetail({
tags: string[];
tagOptions: string[];
transcripts: ClientTranscript[];
convertedClientId?: string | null;
}) {
const router = useRouter();
const [, startTransition] = useTransition();
const [converting, startConvert] = useTransition();
const [tagError, setTagError] = useState<string | null>(null);
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [deletingId, setDeletingId] = useState<string | null>(null);
function handleConvert() {
const ok = window.confirm(
`Convertire "${lead.name}" in cliente?\n\nVerrà creato un cliente (con email, telefono e transcript del lead) e il lead verrà archiviato mantenendo lo stato "vinto".`
);
if (!ok) return;
startConvert(() => convertLeadToClient(lead.id));
}
function run(fn: () => Promise<unknown>) {
setTagError(null);
startTransition(async () => {
@@ -106,6 +123,25 @@ export function LeadDetail({
>
Genera preventivo
</Link>
{convertedClientId ? (
<Link
href={`/admin/clients/${convertedClientId}`}
className="inline-flex items-center gap-1.5 px-3 py-2 bg-[#1A463C]/10 text-[#1A463C] rounded-md text-sm font-semibold hover:bg-[#1A463C]/15 transition-colors"
>
Convertito Apri cliente
</Link>
) : (
lead.status === "won" && (
<button
type="button"
onClick={handleConvert}
disabled={converting}
className="inline-flex items-center gap-1.5 px-3 py-2 bg-[#1A463C] text-white rounded-md text-sm font-semibold hover:bg-[#1A463C]/90 transition-colors disabled:opacity-50"
>
{converting ? "Conversione..." : "Converti in cliente"}
</button>
)
)}
<EditLeadModal lead={lead} />
</div>
</div>