feat: Lead Details redesign to Quiet Luxury + unified interaction timeline
Rewrite /admin/pipeline/[id] LeadDetail with token-based dual-theme layout: bespoke header with inline StatusBadge, asymmetric 1/3 profile + 2/3 content columns, and a unified reverse-chronological timeline merging activities and transcripts (transcript node highlighted + expandable + deletable). Restyle modal triggers (Registra Attività, Aggiungi Transcript, Modifica-as-icon) to the mock outline style. Remove the confusing "Invia Preventivo" button (it only linked a token + advanced stage, no email). Keep Converti in cliente for won leads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,15 @@ across all future admin pages.
|
|||||||
| `LeadsKanbanBoard` | `src/components/admin/leads/LeadsKanbanBoard.tsx` | Kanban restyle: `bg-muted/60` columns with count pills, `bg-card` cards with `hover:border-primary/30`. Uses `@dnd-kit`, keeps all 6 lead stages. |
|
| `LeadsKanbanBoard` | `src/components/admin/leads/LeadsKanbanBoard.tsx` | Kanban restyle: `bg-muted/60` columns with count pills, `bg-card` cards with `hover:border-primary/30`. Uses `@dnd-kit`, keeps all 6 lead stages. |
|
||||||
| `LeadsViewToggle` | `src/components/admin/leads/LeadsViewToggle.tsx` | Wires `SearchInput` + `SegmentedToggle` together with the table/kanban views. |
|
| `LeadsViewToggle` | `src/components/admin/leads/LeadsViewToggle.tsx` | Wires `SearchInput` + `SegmentedToggle` together with the table/kanban views. |
|
||||||
| `MilestoneStepper` | `src/components/client/MilestoneStepper.tsx` | Client-portal horizontal milestone stepper: one node per real phase, connecting line filled by `global_progress_pct`. Done = filled `bg-primary` ✓, active = `border-primary` outline, upcoming = `bg-muted`. |
|
| `MilestoneStepper` | `src/components/client/MilestoneStepper.tsx` | Client-portal horizontal milestone stepper: one node per real phase, connecting line filled by `global_progress_pct`. Done = filled `bg-primary` ✓, active = `border-primary` outline, upcoming = `bg-muted`. |
|
||||||
|
| `LeadDetail` | `src/components/admin/leads/LeadDetail.tsx` | Lead detail page (`/admin/pipeline/[id]`): bespoke header (name + inline `StatusBadge` + action bar), asymmetric `lg:grid-cols-3` layout (1/3 profile card w/ icon-chip rows + tags, 2/3 Note/Follow-up + unified timeline). |
|
||||||
|
|
||||||
|
**Unified timeline pattern**: activities + transcripts merged into one
|
||||||
|
reverse-chronological "Storico delle Interazioni". Vertical connector = absolute
|
||||||
|
`w-px bg-border` at `left-[19px]` behind `w-10 h-10 rounded-full` nodes.
|
||||||
|
Transcript node is highlighted (`bg-primary text-primary-foreground`) and
|
||||||
|
expandable (`line-clamp-2` → "Mostra tutto/meno"); activity nodes are muted
|
||||||
|
(`bg-muted border-border`). Dates are `font-mono` — activities relative
|
||||||
|
(`formatDistanceToNow`), transcripts absolute (`d MMMM yyyy`).
|
||||||
|
|
||||||
### Client Portal restyle notes
|
### Client Portal restyle notes
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dettagli Lead — Rossi Inc</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde scuro richiesto */
|
||||||
|
darkHover: '#13342D', /* Variante scura per hover */
|
||||||
|
active: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar-transition {
|
||||||
|
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.timeline-line::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 19px;
|
||||||
|
width: 1.5px;
|
||||||
|
background-color: #E2E8F0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<!-- Logo e Intestazione -->
|
||||||
|
<div class="mb-10 px-2 flex items-center justify-between">
|
||||||
|
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigazione Principale -->
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Area Inferiore Sidebar -->
|
||||||
|
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||||
|
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
<!-- BARRA DI INTESTAZIONE SUPERIORE -->
|
||||||
|
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Pulsante Apri/Chiudi Sidebar -->
|
||||||
|
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400 font-medium">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Demo Account</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- AREA DEL CONTENUTO DETTAGLIO -->
|
||||||
|
<main class="flex-1 p-8 lg:p-10 max-w-[1400px] w-full mx-auto flex flex-col gap-8 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- INTESTAZIONE DETTAGLIO LEAD -->
|
||||||
|
<div class="flex flex-col lg:flex-row justify-between lg:items-start gap-6 border-b border-slate-100 pb-6">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h1 class="text-3xl font-bold text-slate-900 tracking-tight">Nome Cognome</h1>
|
||||||
|
<span class="px-2.5 py-1 rounded text-[10px] font-semibold bg-purple-50 text-purple-600 border border-purple-100 uppercase tracking-wide">Qualified</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-medium text-slate-500 mt-1">Azienda Srl</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsantiera Azioni Riorganizzata -->
|
||||||
|
<div class="flex flex-wrap gap-2.5">
|
||||||
|
<button class="px-4 py-2.5 border border-slate-200 text-slate-600 hover:text-slate-900 hover:bg-slate-50 text-xs font-semibold rounded-lg transition-colors flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||||
|
Registra Attività
|
||||||
|
</button>
|
||||||
|
<button class="px-4 py-2.5 border border-slate-200 text-slate-600 hover:text-slate-900 hover:bg-slate-50 text-xs font-semibold rounded-lg transition-colors flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"/></svg>
|
||||||
|
Aggiungi Transcript
|
||||||
|
</button>
|
||||||
|
<!-- Azione Chiave Evidenziata -->
|
||||||
|
<button class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-medium tracking-wide px-5 py-2.5 rounded-lg flex items-center gap-2 transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9.813 15.904L9 21l5.096-.813a2 2 0 001.414-.586l5.086-5.086a2 2 0 000-2.828l-3.212-3.212a2 2 0 00-2.828 0l-5.086 5.086a2 2 0 00-.586 1.414zM16.5 7.5L19.5 10.5"/></svg>
|
||||||
|
Genera preventivo
|
||||||
|
</button>
|
||||||
|
<!-- Azioni di Sistema Discrete -->
|
||||||
|
<button class="p-2 border border-slate-200 text-slate-400 hover:text-slate-700 hover:bg-slate-50 rounded-lg transition-colors" title="Modifica">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CORPO DEL DETTAGLIO: COLONNE ASIMMETRICHE -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
|
||||||
|
|
||||||
|
<!-- COLONNA DI SINISTRA (1/3): Scheda Anagrafica / Profilo -->
|
||||||
|
<div class="lg:col-span-1 bg-white border border-slate-100 rounded-xl p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)] flex flex-col gap-6">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400">Profilo Lead</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Email -->
|
||||||
|
<div class="flex items-center gap-3.5 text-xs">
|
||||||
|
<div class="p-2 bg-slate-50 text-slate-400 rounded-lg">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-400 font-medium">Email</p>
|
||||||
|
<a href="mailto:test@gmail.com" class="font-semibold text-slate-800 hover:text-brand-dark">test@gmail.com</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telefono -->
|
||||||
|
<div class="flex items-center gap-3.5 text-xs">
|
||||||
|
<div class="p-2 bg-slate-50 text-slate-400 rounded-lg">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-2.824-1.802-5.14-4.118-6.944-6.94l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-400 font-medium">Telefono</p>
|
||||||
|
<span class="font-semibold text-slate-800 font-mono">+39 444 3322111</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ultimo Contatto -->
|
||||||
|
<div class="flex items-center gap-3.5 text-xs">
|
||||||
|
<div class="p-2 bg-slate-50 text-slate-400 rounded-lg">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-400 font-medium">Ultimo Contatto</p>
|
||||||
|
<span class="font-semibold text-slate-800">23 giorni fa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prossima Azione -->
|
||||||
|
<div class="flex items-center gap-3.5 text-xs">
|
||||||
|
<div class="p-2 bg-slate-50 text-slate-400 rounded-lg">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-400 font-medium">Prossima Azione programmata</p>
|
||||||
|
<span class="font-semibold text-slate-400">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gestione Tag -->
|
||||||
|
<div class="border-t border-slate-100 pt-5">
|
||||||
|
<p class="text-[10px] uppercase font-bold text-slate-400 tracking-wider mb-2.5">Tag Lead</p>
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<span class="px-2.5 py-1 bg-slate-100 text-slate-600 rounded-md text-[11px] font-medium">B2B</span>
|
||||||
|
<span class="px-2.5 py-1 bg-slate-100 text-slate-600 rounded-md text-[11px] font-medium">Design</span>
|
||||||
|
<button class="inline-flex items-center justify-center w-6 h-6 rounded-full border border-dashed border-slate-200 text-slate-400 hover:text-slate-600 hover:border-slate-400 transition-colors">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COLONNA DI DESTRA (2/3): Timeline, Note, Prossimi Step -->
|
||||||
|
<div class="lg:col-span-2 flex flex-col gap-6">
|
||||||
|
|
||||||
|
<!-- SEZIONE NOTA RAPIDA & PROSSIMI APPUNTAMENTI -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
<!-- Box: Note -->
|
||||||
|
<div class="bg-white border border-slate-100 rounded-xl p-5 shadow-[0_4px_20px_rgba(0,0,0,0.01)]">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-3">Note</h3>
|
||||||
|
<div class="text-xs text-slate-600 leading-relaxed min-h-[80px] p-2.5 bg-slate-50/50 rounded-lg border border-slate-100">
|
||||||
|
Brand di fashion design di lusso milanese interessato ad una ristrutturazione completa del canale ecommerce.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Box: Prossimo Reminder -->
|
||||||
|
<div class="bg-white border border-slate-100 rounded-xl p-5 shadow-[0_4px_20px_rgba(0,0,0,0.01)]">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-3">Prossimo Follow-up</h3>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-[80px] border border-dashed border-slate-200 rounded-lg text-slate-400 p-3">
|
||||||
|
<svg class="w-5 h-5 mb-1.5 text-slate-300" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
<span class="text-[11px] font-medium">Nessun reminder impostato</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEZIONE UNIFICATA: STORICO CRONOLOGICO (TIMELINE DELLE INTERAZIONI) -->
|
||||||
|
<div class="bg-white border border-slate-100 rounded-xl p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)]">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-6">Storico delle Interazioni</h3>
|
||||||
|
|
||||||
|
<!-- Griglia Timeline -->
|
||||||
|
<div class="relative timeline-line space-y-6">
|
||||||
|
|
||||||
|
<!-- Elemento Timeline 1: Transcript Chiamata AI (In Evidenza) -->
|
||||||
|
<div class="relative z-10 pl-10 flex gap-4 items-start group">
|
||||||
|
<div class="absolute left-0 w-10 h-10 rounded-full bg-brand-dark text-white flex items-center justify-center shadow-md smooth-transition">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 bg-slate-50 border border-slate-100 rounded-xl p-4 hover:border-slate-200 transition-all">
|
||||||
|
<div class="flex flex-wrap justify-between items-center gap-2 mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] font-bold text-brand-dark uppercase tracking-wide">Transcript Chiamata AI</span>
|
||||||
|
<h4 class="text-xs font-bold text-slate-800">Discovery Call - Progetto "SaaS Networker"</h4>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-slate-400 font-mono">26 Giugno 2026</span>
|
||||||
|
</div>
|
||||||
|
<!-- Sotto-contenuto del transcript -->
|
||||||
|
<div class="text-xs text-slate-600 leading-relaxed pt-2 border-t border-slate-200/60">
|
||||||
|
<p class="font-semibold text-slate-800 mb-1">1. Apertura e Contesto Strategico</p>
|
||||||
|
<p id="transcript-excerpt" class="line-clamp-2">"Tu: Alessandro, grazie per la disponibilità. Sappiamo che hai 3 mesi per lanciare questo SaaS dedicato al Network Marketing. Il tempo è poco, quindi dobbiamo essere precisi. L'integrazione con i sistemi di pagamento e la sitemap sono la nostra prima priorità per..."</p>
|
||||||
|
<p id="transcript-full" class="hidden">"Tu: Alessandro, grazie per la disponibilità. Sappiamo che hai 3 mesi per lanciare questo SaaS dedicato al Network Marketing. Il tempo è poco, quindi dobbiamo essere precisi. L'integrazione con i sistemi di pagamento e la sitemap sono la nostra prima priorità per sbloccare la fase di programmazione. Serviranno grafiche ad hoc e un sistema di onboarding utente rapido."</p>
|
||||||
|
<button onclick="toggleTranscript()" id="btn-transcript-toggle" class="text-[11px] font-bold text-brand-dark hover:text-brand-darkHover mt-2 inline-flex items-center gap-1">
|
||||||
|
Mostra tutto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Elemento Timeline 2: Invio Email -->
|
||||||
|
<div class="relative z-10 pl-10 flex gap-4 items-start group">
|
||||||
|
<div class="absolute left-0 w-10 h-10 rounded-full bg-slate-100 text-slate-500 border border-slate-200 flex items-center justify-center smooth-transition">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 border border-slate-100 rounded-xl p-4 hover:border-slate-200 transition-all">
|
||||||
|
<div class="flex justify-between items-center gap-2 mb-1.5">
|
||||||
|
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-wide">E-mail inviata</span>
|
||||||
|
<span class="text-[10px] text-slate-400 font-mono">23 giorni fa</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-xs font-bold text-slate-800 mb-1">Mail di onboarding</h4>
|
||||||
|
<p class="text-xs text-slate-500 leading-relaxed">Inviato pacchetto di benvenuto e richiesta accessi per iniziare l'analisi competitiva del brand.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Elemento Timeline 3: Telefonata -->
|
||||||
|
<div class="relative z-10 pl-10 flex gap-4 items-start group">
|
||||||
|
<div class="absolute left-0 w-10 h-10 rounded-full bg-slate-100 text-slate-500 border border-slate-200 flex items-center justify-center smooth-transition">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-2.824-1.802-5.14-4.118-6.944-6.94l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 border border-slate-100 rounded-xl p-4 hover:border-slate-200 transition-all">
|
||||||
|
<div class="flex justify-between items-center gap-2 mb-1.5">
|
||||||
|
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-wide">Chiamata effettuata</span>
|
||||||
|
<span class="text-[10px] text-slate-400 font-mono">25 giorni fa</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-xs font-bold text-slate-800 mb-1">Telefonata di follow-up</h4>
|
||||||
|
<p class="text-xs text-slate-500 leading-relaxed">Chiamata conoscitiva di test per allineamento sulle tempistiche di consegna.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT DI LOGICA (Gestione Sidebar e Timeline) -->
|
||||||
|
<script>
|
||||||
|
// Toggle Sidebar
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
const logoText = document.getElementById('sidebar-logo-text');
|
||||||
|
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
const isCollapsed = sidebar.classList.contains('w-20');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
sidebar.classList.remove('w-20');
|
||||||
|
sidebar.classList.add('w-64');
|
||||||
|
setTimeout(() => {
|
||||||
|
logoText.classList.remove('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
logoText.classList.add('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||||
|
sidebar.classList.remove('w-64');
|
||||||
|
sidebar.classList.add('w-20');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funzione interattiva per mostrare / nascondere il transcript completo
|
||||||
|
function toggleTranscript() {
|
||||||
|
const excerpt = document.getElementById('transcript-excerpt');
|
||||||
|
const full = document.getElementById('transcript-full');
|
||||||
|
const btn = document.getElementById('btn-transcript-toggle');
|
||||||
|
|
||||||
|
if (full.classList.contains('hidden')) {
|
||||||
|
full.classList.remove('hidden');
|
||||||
|
excerpt.classList.add('hidden');
|
||||||
|
btn.innerText = 'Mostra meno';
|
||||||
|
} else {
|
||||||
|
full.classList.add('hidden');
|
||||||
|
excerpt.classList.remove('hidden');
|
||||||
|
btn.innerText = 'Mostra tutto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Lead, Activity, Reminder, ClientTranscript } from "@/db/schema";
|
import { Lead, Activity, Reminder, ClientTranscript } from "@/db/schema";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { StatusBadge } from "@/components/ui/StatusBadge";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { OptionMultiSelect } from "@/components/ui/option-multi-select";
|
import { OptionMultiSelect } from "@/components/ui/option-multi-select";
|
||||||
import {
|
import {
|
||||||
addLeadTag,
|
addLeadTag,
|
||||||
@@ -17,26 +15,32 @@ import {
|
|||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
Clock,
|
||||||
|
CalendarDays,
|
||||||
|
Calendar,
|
||||||
|
StickyNote,
|
||||||
|
Mic,
|
||||||
|
Trash2,
|
||||||
|
Sparkles,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { LogActivityModal } from "./LogActivityModal";
|
import { LogActivityModal } from "./LogActivityModal";
|
||||||
import { SendQuoteModal } from "./SendQuoteModal";
|
|
||||||
import { EditLeadModal } from "./LeadForm";
|
|
||||||
import { TranscriptModal } from "./TranscriptModal";
|
import { TranscriptModal } from "./TranscriptModal";
|
||||||
|
import { EditLeadModal } from "./LeadForm";
|
||||||
|
|
||||||
const ACTIVITY_ICON: Record<string, string> = {
|
const ACTIVITY_META: Record<string, { label: string; Icon: LucideIcon }> = {
|
||||||
call: "📞",
|
call: { label: "Chiamata effettuata", Icon: Phone },
|
||||||
email: "📧",
|
email: { label: "E-mail inviata", Icon: Mail },
|
||||||
meeting: "📅",
|
meeting: { label: "Meeting", Icon: Calendar },
|
||||||
note: "📝",
|
note: { label: "Nota", Icon: StickyNote },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STAGE_COLOR: Record<string, string> = {
|
type TimelineItem =
|
||||||
contacted: "bg-blue-100 text-blue-800",
|
| { kind: "transcript"; date: Date; data: ClientTranscript }
|
||||||
qualified: "bg-purple-100 text-purple-800",
|
| { kind: "activity"; date: Date; data: Activity };
|
||||||
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 LeadDetail({
|
export function LeadDetail({
|
||||||
lead,
|
lead,
|
||||||
@@ -105,28 +109,44 @@ export function LeadDetail({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unified, reverse-chronological timeline of activities + transcripts.
|
||||||
|
const timeline: TimelineItem[] = [
|
||||||
|
...activities.map((a) => ({
|
||||||
|
kind: "activity" as const,
|
||||||
|
date: new Date(a.activity_date),
|
||||||
|
data: a,
|
||||||
|
})),
|
||||||
|
...transcripts.map((t) => ({
|
||||||
|
kind: "transcript" as const,
|
||||||
|
date: new Date(t.call_date + "T00:00:00"),
|
||||||
|
data: t,
|
||||||
|
})),
|
||||||
|
].sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="flex flex-col gap-8">
|
||||||
{/* Header + Actions */}
|
{/* Header + Actions */}
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex flex-col lg:flex-row justify-between lg:items-start gap-6 border-b border-border pb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">{lead.name}</h1>
|
<div className="flex items-center gap-3">
|
||||||
<p className="text-gray-600">{lead.company || "Azienda non specificata"}</p>
|
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||||
|
{lead.name}
|
||||||
|
</h1>
|
||||||
|
<StatusBadge status={lead.status} />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground mt-1">
|
||||||
|
{lead.company || "Azienda non specificata"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
<LogActivityModal leadId={lead.id} />
|
<LogActivityModal leadId={lead.id} />
|
||||||
<TranscriptModal leadId={lead.id} />
|
<TranscriptModal leadId={lead.id} />
|
||||||
<SendQuoteModal leadId={lead.id} />
|
|
||||||
<Link
|
|
||||||
href={`/admin/preventivi/genera?lead_id=${lead.id}`}
|
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-2 bg-[#DEF168] text-[#1A463C] rounded-md text-sm font-semibold hover:bg-[#d4e85e] transition-colors"
|
|
||||||
>
|
|
||||||
Genera preventivo
|
|
||||||
</Link>
|
|
||||||
{convertedClientId ? (
|
{convertedClientId ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/clients/${convertedClientId}`}
|
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"
|
className="inline-flex items-center gap-2 px-4 py-2.5 border border-border text-primary hover:bg-muted text-xs font-semibold rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
✓ Convertito → Apri cliente
|
✓ Convertito → Apri cliente
|
||||||
</Link>
|
</Link>
|
||||||
@@ -136,203 +156,288 @@ export function LeadDetail({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleConvert}
|
onClick={handleConvert}
|
||||||
disabled={converting}
|
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"
|
className="inline-flex items-center gap-2 px-4 py-2.5 border border-border text-muted-foreground hover:text-foreground hover:bg-muted text-xs font-semibold rounded-lg transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{converting ? "Conversione..." : "Converti in cliente"}
|
{converting ? "Conversione..." : "Converti in cliente"}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Primary action */}
|
||||||
|
<Link
|
||||||
|
href={`/admin/preventivi/genera?lead_id=${lead.id}`}
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 bg-primary text-primary-foreground hover:bg-primary/90 text-xs font-medium tracking-wide rounded-lg shadow-sm transition-colors"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
Genera preventivo
|
||||||
|
</Link>
|
||||||
|
|
||||||
<EditLeadModal lead={lead} />
|
<EditLeadModal lead={lead} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
{/* Body: asymmetric columns */}
|
||||||
{/* Lead Profile Card */}
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
|
||||||
<Card>
|
{/* Left (1/3): Profile card */}
|
||||||
<CardHeader>
|
<div className="lg:col-span-1 bg-card border border-border rounded-xl p-6 shadow-card flex flex-col gap-6">
|
||||||
<CardTitle>Profilo</CardTitle>
|
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||||
</CardHeader>
|
Profilo Lead
|
||||||
<CardContent className="space-y-4">
|
</h3>
|
||||||
<div>
|
|
||||||
<label className="text-sm text-gray-600">Stato</label>
|
<div className="space-y-4">
|
||||||
<Badge className={STAGE_COLOR[lead.status as keyof typeof STAGE_COLOR] || ""}>
|
<ProfileRow Icon={Mail} label="Email">
|
||||||
{lead.status.replace(/_/g, " ")}
|
{lead.email ? (
|
||||||
</Badge>
|
<a
|
||||||
</div>
|
href={`mailto:${lead.email}`}
|
||||||
<div>
|
className="font-semibold text-foreground hover:text-primary transition-colors"
|
||||||
<label className="text-sm text-gray-600">Tag</label>
|
>
|
||||||
<OptionMultiSelect
|
{lead.email}
|
||||||
values={tags}
|
</a>
|
||||||
options={tagOptions}
|
) : (
|
||||||
onAdd={(v) => run(() => addLeadTag(lead.id, v))}
|
<span className="font-semibold text-muted-foreground">—</span>
|
||||||
onRemove={(v) => run(() => removeLeadTag(lead.id, v))}
|
)}
|
||||||
onRename={(o, n) => run(() => renameLeadTag(o, n))}
|
</ProfileRow>
|
||||||
/>
|
|
||||||
{tagError && <p className="text-xs text-red-600 mt-1">{tagError}</p>}
|
<ProfileRow Icon={Phone} label="Telefono">
|
||||||
</div>
|
<span className="font-semibold text-foreground font-mono">
|
||||||
<div>
|
{lead.phone || "—"}
|
||||||
<label className="text-sm text-gray-600">Email</label>
|
</span>
|
||||||
<p>{lead.email || "—"}</p>
|
</ProfileRow>
|
||||||
</div>
|
|
||||||
<div>
|
<ProfileRow Icon={Clock} label="Ultimo contatto">
|
||||||
<label className="text-sm text-gray-600">Telefono</label>
|
<span className="font-semibold text-foreground">
|
||||||
<p>{lead.phone || "—"}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm text-gray-600">Ultimo contatto</label>
|
|
||||||
<p>
|
|
||||||
{lead.last_contact_date
|
{lead.last_contact_date
|
||||||
? formatDistanceToNow(new Date(lead.last_contact_date), {
|
? formatDistanceToNow(new Date(lead.last_contact_date), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: it,
|
locale: it,
|
||||||
})
|
})
|
||||||
: "—"}
|
: "—"}
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</ProfileRow>
|
||||||
<div>
|
|
||||||
<label className="text-sm text-gray-600">Prossima azione</label>
|
|
||||||
<p className="font-medium">{lead.next_action || "—"}</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Upcoming Reminders Card */}
|
<ProfileRow Icon={CalendarDays} label="Prossima azione programmata">
|
||||||
<Card>
|
<span
|
||||||
<CardHeader>
|
className={
|
||||||
<CardTitle>Prossimi Follow-up</CardTitle>
|
lead.next_action
|
||||||
</CardHeader>
|
? "font-semibold text-foreground"
|
||||||
<CardContent>
|
: "font-semibold text-muted-foreground"
|
||||||
{reminders.length > 0 ? (
|
}
|
||||||
<ul className="space-y-2">
|
>
|
||||||
{reminders.map((r) => (
|
{lead.next_action || "—"}
|
||||||
<li key={r.id} className="text-sm border-l-2 border-blue-300 pl-2">
|
</span>
|
||||||
<p className="font-medium">{r.title}</p>
|
</ProfileRow>
|
||||||
<p className="text-gray-600">
|
</div>
|
||||||
{format(new Date(r.due_date), "dd MMM yyyy", { locale: it })}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 text-sm">Nessun reminder</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Notes Card */}
|
{/* Tags */}
|
||||||
<Card>
|
<div className="border-t border-border pt-5">
|
||||||
<CardHeader>
|
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-wider mb-2.5">
|
||||||
<CardTitle>Note</CardTitle>
|
Tag Lead
|
||||||
</CardHeader>
|
</p>
|
||||||
<CardContent>
|
<OptionMultiSelect
|
||||||
<p className="text-sm">{lead.notes || "Nessuna nota"}</p>
|
values={tags}
|
||||||
</CardContent>
|
options={tagOptions}
|
||||||
</Card>
|
onAdd={(v) => run(() => addLeadTag(lead.id, v))}
|
||||||
</div>
|
onRemove={(v) => run(() => removeLeadTag(lead.id, v))}
|
||||||
|
onRename={(o, n) => run(() => renameLeadTag(o, n))}
|
||||||
|
/>
|
||||||
|
{tagError && <p className="text-xs text-destructive mt-1">{tagError}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Activity Log */}
|
{/* Right (2/3): Note, Follow-up, Timeline */}
|
||||||
<Card>
|
<div className="lg:col-span-2 flex flex-col gap-6">
|
||||||
<CardHeader>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<CardTitle>Storico Attività</CardTitle>
|
{/* Note */}
|
||||||
</CardHeader>
|
<div className="bg-card border border-border rounded-xl p-5 shadow-card">
|
||||||
<CardContent>
|
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||||
{activities.length > 0 ? (
|
Note
|
||||||
<div className="space-y-4">
|
</h3>
|
||||||
{activities.map((activity) => (
|
{lead.notes ? (
|
||||||
<div key={activity.id} className="border-l-4 border-blue-300 pl-4 pb-4">
|
<div className="text-xs text-foreground/80 leading-relaxed min-h-[80px] p-2.5 bg-muted/50 rounded-lg border border-border">
|
||||||
<div className="flex items-center gap-2">
|
{lead.notes}
|
||||||
<span className="text-xl">
|
|
||||||
{ACTIVITY_ICON[activity.type as keyof typeof ACTIVITY_ICON] || "📝"}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium capitalize">{activity.type}</span>
|
|
||||||
<span className="text-gray-600 text-sm">
|
|
||||||
{formatDistanceToNow(new Date(activity.activity_date), {
|
|
||||||
addSuffix: true,
|
|
||||||
locale: it,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm mt-2">{activity.notes}</p>
|
|
||||||
{activity.duration_minutes && (
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Durata: {activity.duration_minutes} minuti
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
<div className="flex items-center justify-center min-h-[80px] text-[11px] font-medium text-muted-foreground p-2.5 bg-muted/50 rounded-lg border border-border">
|
||||||
|
Nessuna nota
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 text-sm">Nessuna attività registrata</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Transcript Call */}
|
{/* Next follow-up */}
|
||||||
<Card>
|
<div className="bg-card border border-border rounded-xl p-5 shadow-card">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||||
<CardTitle>Transcript Call</CardTitle>
|
Prossimo Follow-up
|
||||||
<span className="text-sm text-gray-500">
|
</h3>
|
||||||
{transcripts.length} {transcripts.length === 1 ? "transcript" : "transcript"}
|
{reminders.length > 0 ? (
|
||||||
</span>
|
<ul className="space-y-2.5">
|
||||||
</CardHeader>
|
{reminders.map((r) => (
|
||||||
<CardContent>
|
<li
|
||||||
{transcripts.length > 0 ? (
|
key={r.id}
|
||||||
<div className="space-y-4">
|
className="border-l-2 border-primary/40 pl-2.5 text-xs"
|
||||||
{transcripts.map((t) => {
|
>
|
||||||
const isExpanded = expandedIds.has(t.id);
|
<p className="font-semibold text-foreground">{r.title}</p>
|
||||||
const formattedDate = format(
|
<p className="text-muted-foreground font-mono text-[11px] mt-0.5">
|
||||||
new Date(t.call_date + "T00:00:00"),
|
{format(new Date(r.due_date), "dd MMM yyyy", { locale: it })}
|
||||||
"d MMMM yyyy",
|
</p>
|
||||||
{ locale: it }
|
</li>
|
||||||
);
|
))}
|
||||||
const lines = t.content.split("\n");
|
</ul>
|
||||||
const preview = lines.slice(0, 3).join("\n").slice(0, 200);
|
) : (
|
||||||
const hasMore = t.content.length > preview.length || lines.length > 3;
|
<div className="flex flex-col items-center justify-center min-h-[80px] border border-dashed border-border rounded-lg text-muted-foreground p-3">
|
||||||
|
<Clock className="w-5 h-5 mb-1.5 text-muted-foreground/60" />
|
||||||
|
<span className="text-[11px] font-medium">
|
||||||
|
Nessun reminder impostato
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{/* Unified timeline */}
|
||||||
<div key={t.id} className="border-l-4 border-purple-300 pl-4 pb-4">
|
<div className="bg-card border border-border rounded-xl p-6 shadow-card">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-6">
|
||||||
<div className="flex-1 min-w-0">
|
Storico delle Interazioni
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
</h3>
|
||||||
<span className="font-medium text-sm">{formattedDate}</span>
|
|
||||||
{t.title && (
|
{timeline.length > 0 ? (
|
||||||
<span className="text-gray-600 text-sm">— {t.title}</span>
|
<div className="relative space-y-6">
|
||||||
)}
|
{/* vertical connector line */}
|
||||||
|
<div className="absolute top-2 bottom-2 left-[19px] w-px bg-border z-0" />
|
||||||
|
|
||||||
|
{timeline.map((item) => {
|
||||||
|
if (item.kind === "transcript") {
|
||||||
|
const t = item.data;
|
||||||
|
const isExpanded = expandedIds.has(t.id);
|
||||||
|
const isLong =
|
||||||
|
t.content.length > 140 || t.content.split("\n").length > 2;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`t-${t.id}`}
|
||||||
|
className="relative z-10 pl-10 flex gap-4 items-start group"
|
||||||
|
>
|
||||||
|
<div className="absolute left-0 w-10 h-10 rounded-full bg-primary text-primary-foreground flex items-center justify-center shadow">
|
||||||
|
<Mic className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-sm text-gray-700 whitespace-pre-wrap">
|
<div className="flex-1 bg-muted border border-border rounded-xl p-4 hover:border-muted-foreground/30 transition-colors">
|
||||||
{isExpanded ? t.content : preview}
|
<div className="flex flex-wrap justify-between items-start gap-2 mb-2">
|
||||||
{!isExpanded && hasMore && (
|
<div>
|
||||||
<span className="text-gray-400">...</span>
|
<span className="text-[10px] font-bold text-primary uppercase tracking-wide">
|
||||||
)}
|
Transcript Chiamata AI
|
||||||
|
</span>
|
||||||
|
{t.title && (
|
||||||
|
<h4 className="text-xs font-bold text-foreground">
|
||||||
|
{t.title}
|
||||||
|
</h4>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[10px] text-muted-foreground font-mono">
|
||||||
|
{format(new Date(t.call_date + "T00:00:00"), "d MMMM yyyy", {
|
||||||
|
locale: it,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDeleteTranscript(t.id)}
|
||||||
|
disabled={deletingId === t.id}
|
||||||
|
aria-label="Elimina transcript"
|
||||||
|
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-foreground/80 leading-relaxed pt-2 border-t border-border/60">
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
isExpanded ? "whitespace-pre-wrap" : "line-clamp-2"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t.content}
|
||||||
|
</p>
|
||||||
|
{isLong && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleExpand(t.id)}
|
||||||
|
className="text-[11px] font-bold text-primary hover:text-primary/80 mt-2 inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{isExpanded ? "Mostra meno" : "Mostra tutto"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasMore && (
|
</div>
|
||||||
<button
|
);
|
||||||
onClick={() => toggleExpand(t.id)}
|
}
|
||||||
className="text-xs text-blue-600 hover:underline mt-1"
|
|
||||||
>
|
const a = item.data;
|
||||||
{isExpanded ? "Mostra meno" : "Mostra tutto"}
|
const meta = ACTIVITY_META[a.type] ?? ACTIVITY_META.note;
|
||||||
</button>
|
const Icon = meta.Icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`a-${a.id}`}
|
||||||
|
className="relative z-10 pl-10 flex gap-4 items-start"
|
||||||
|
>
|
||||||
|
<div className="absolute left-0 w-10 h-10 rounded-full bg-muted text-muted-foreground border border-border flex items-center justify-center">
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border border-border rounded-xl p-4 hover:border-muted-foreground/30 transition-colors">
|
||||||
|
<div className="flex justify-between items-center gap-2 mb-1.5">
|
||||||
|
<span className="text-[10px] font-bold text-muted-foreground uppercase tracking-wide">
|
||||||
|
{meta.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground font-mono">
|
||||||
|
{formatDistanceToNow(new Date(a.activity_date), {
|
||||||
|
addSuffix: true,
|
||||||
|
locale: it,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{a.notes && (
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
|
{a.notes}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{a.duration_minutes && (
|
||||||
|
<p className="text-[11px] text-muted-foreground/70 font-mono mt-1.5">
|
||||||
|
Durata: {a.duration_minutes} min
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-red-600 hover:text-red-700 shrink-0"
|
|
||||||
disabled={deletingId === t.id}
|
|
||||||
onClick={() => handleDeleteTranscript(t.id)}
|
|
||||||
>
|
|
||||||
{deletingId === t.id ? "..." : "Elimina"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<p className="text-sm text-muted-foreground text-center py-6">
|
||||||
<p className="text-gray-500 text-sm">Nessun transcript registrato</p>
|
Nessuna interazione registrata
|
||||||
)}
|
</p>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileRow({
|
||||||
|
Icon,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
Icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3.5 text-xs">
|
||||||
|
<div className="p-2 bg-muted text-muted-foreground rounded-lg shrink-0">
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-[10px] text-muted-foreground font-medium">{label}</p>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Pencil } from "lucide-react";
|
||||||
|
|
||||||
export function CreateLeadModal() {
|
export function CreateLeadModal() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -228,9 +229,14 @@ export function EditLeadModal({ lead }: { lead: Lead }) {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<button
|
||||||
Modifica
|
type="button"
|
||||||
</Button>
|
aria-label="Modifica lead"
|
||||||
|
title="Modifica"
|
||||||
|
className="inline-flex items-center justify-center p-2.5 border border-border text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Activity as ActivityIcon } from "lucide-react";
|
||||||
|
|
||||||
type LogActivityInput = z.infer<typeof createActivitySchema>;
|
type LogActivityInput = z.infer<typeof createActivitySchema>;
|
||||||
|
|
||||||
@@ -71,9 +72,13 @@ export function LogActivityModal({ leadId }: { leadId: string }) {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2.5 border border-border text-muted-foreground hover:text-foreground hover:bg-muted text-xs font-semibold rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ActivityIcon className="w-4 h-4 text-muted-foreground/70" />
|
||||||
Registra Attività
|
Registra Attività
|
||||||
</Button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-sm">
|
<DialogContent className="max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Mic } from "lucide-react";
|
||||||
|
|
||||||
const transcriptSchema = z.object({
|
const transcriptSchema = z.object({
|
||||||
lead_id: z.string().min(1),
|
lead_id: z.string().min(1),
|
||||||
@@ -72,9 +73,13 @@ export function TranscriptModal({ leadId }: { leadId: string }) {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2.5 border border-border text-muted-foreground hover:text-foreground hover:bg-muted text-xs font-semibold rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Mic className="w-4 h-4 text-muted-foreground/70" />
|
||||||
Aggiungi Transcript
|
Aggiungi Transcript
|
||||||
</Button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user