ae355c33a6
- Client detail: category + tier badges on active offers - Dashboard: remove top KPI cards + recent activity; fold current KPIs into bottom MetricCard style; add 12-month income forecast chart - Forecast query: branch by offer_type (retainer = monthly fee, una_tantum = spread over duration), filter archived projects - Payments: set the month a payment was collected (paid_at) from PaymentsTab - Redesign FollowUpWidget in clean Pipedrive style (brand green, no orange) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { getLeadsNeedingFollowUp } from "@/lib/lead-service";
|
|
import Link from "next/link";
|
|
|
|
const MAX_ROWS = 4;
|
|
|
|
function initials(name: string): string {
|
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
if (parts.length === 0) return "?";
|
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
|
|
function relativeDays(date: Date | string | null | undefined): string | null {
|
|
if (!date) return "Mai contattato";
|
|
const d = date instanceof Date ? date : new Date(date);
|
|
const diffMs = Date.now() - d.getTime();
|
|
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
if (days <= 0) return "Contattato oggi";
|
|
if (days === 1) return "Contattato ieri";
|
|
return `Contattato ${days} giorni fa`;
|
|
}
|
|
|
|
export async function FollowUpWidget() {
|
|
const leads = await getLeadsNeedingFollowUp(7);
|
|
|
|
if (leads.length === 0) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-[#e5e7eb] px-5 py-4">
|
|
<div className="flex items-center gap-2">
|
|
<h2 className="text-sm font-bold text-[#1a1a1a]">Follow-up</h2>
|
|
</div>
|
|
<p className="text-sm text-[#71717a] mt-2">Tutti i lead sono aggiornati ✓</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const visible = leads.slice(0, MAX_ROWS);
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-5 py-3.5 border-b border-[#f4f4f5]">
|
|
<div className="flex items-center gap-2.5">
|
|
<h2 className="text-sm font-bold text-[#1a1a1a]">Follow-up</h2>
|
|
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full bg-[#1A463C] text-white text-[11px] font-bold">
|
|
{leads.length}
|
|
</span>
|
|
</div>
|
|
<Link
|
|
href="/admin/leads"
|
|
className="text-xs font-medium text-[#71717a] hover:text-[#1A463C] transition-colors"
|
|
>
|
|
Vedi tutti →
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Lead rows */}
|
|
<ul className="divide-y divide-[#f4f4f5]">
|
|
{visible.map((lead) => (
|
|
<li key={lead.id}>
|
|
<Link
|
|
href={`/admin/leads/${lead.id}`}
|
|
className="flex items-center gap-3 px-5 py-3 hover:bg-[#f9f9f9] transition-colors group"
|
|
>
|
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#1A463C]/10 text-[#1A463C] text-xs font-bold">
|
|
{initials(lead.name)}
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-semibold text-[#1a1a1a] truncate">{lead.name}</p>
|
|
<p className="text-xs text-[#71717a] truncate">
|
|
{lead.next_action
|
|
? lead.next_action
|
|
: [lead.company, relativeDays(lead.last_contact_date)]
|
|
.filter(Boolean)
|
|
.join(" · ")}
|
|
</p>
|
|
</div>
|
|
<span className="text-xs font-medium text-[#71717a] group-hover:text-[#1A463C] transition-colors shrink-0">
|
|
Contatta →
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{leads.length > MAX_ROWS && (
|
|
<Link
|
|
href="/admin/leads"
|
|
className="block px-5 py-2.5 text-center text-xs font-medium text-[#71717a] hover:text-[#1A463C] border-t border-[#f4f4f5] transition-colors"
|
|
>
|
|
Vedi tutti i {leads.length} lead
|
|
</Link>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|