import { useState, useEffect } from “react”;
import * as XLSX from “xlsx”;
const STORAGE_KEY_CLIENTS = “crm_clients”;
const STORAGE_KEY_TICKETS = “crm_tickets”;
function initials(name) {
return name.split(” “).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? “”).join(“”);
}
function today() { return new Date().toISOString().split(“T”)[0]; }
function formatDate(d) { if (!d) return “”; const [y, m, day] = d.split(“-“); return `${day}/${m}/${y}`; }
function formatTime(ts) { if (!ts) return “”; return new Date(ts).toLocaleTimeString(“it-IT”, { hour: “2-digit”, minute: “2-digit” }); }
const STATUS_COLORS = {
Aperto: { bg: “#E6F1FB”, text: “#0C447C”, border: “#185FA5” },
“In lavorazione”: { bg: “#FAEEDA”, text: “#633806”, border: “#BA7517” },
Chiuso: { bg: “#EAF3DE”, text: “#27500A”, border: “#3B6D11” },
};
const PRIORITY_COLORS = {
Bassa: { bg: “#F1EFE8”, text: “#444441”, border: “#888780” },
Media: { bg: “#FAEEDA”, text: “#633806”, border: “#BA7517” },
Alta: { bg: “#FCEBEB”, text: “#791F1F”, border: “#A32D2D” },
};
const AVATAR_COLORS = [
{ bg: “#EEEDFE”, text: “#3C3489” }, { bg: “#E1F5EE”, text: “#085041” },
{ bg: “#E6F1FB”, text: “#0C447C” }, { bg: “#FAEEDA”, text: “#633806” },
{ bg: “#EAF3DE”, text: “#27500A” },
];
function avatarColor(name) {
let h = 0; for (const c of name) h = (h + c.charCodeAt(0)) % AVATAR_COLORS.length; return AVATAR_COLORS[h];
}
const DEMO_CLIENTS = [
{ id: “c1”, name: “Mario Rossi”, email: “mario.rossi@email.it”, phone: “081 123 4567”, company: “Rossi Srl”, address: “Via Roma 1, Napoli”, note: “” },
{ id: “c2”, name: “Luisa Esposito”, email: “l.esposito@azienda.it”, phone: “081 987 6543”, company: “Esposito & Co”, address: “Via Toledo 22, Napoli”, note: “” },
];
export default function App() {
const [view, setView] = useState(“dashboard”);
const [clients, setClients] = useState(() => { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_CLIENTS)) ?? DEMO_CLIENTS; } catch { return DEMO_CLIENTS; } });
const [tickets, setTickets] = useState(() => { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_TICKETS)) ?? []; } catch { return []; } });
const [clientForm, setClientForm] = useState(null);
const [ticketForm, setTicketForm] = useState(null);
const [ticketFilter, setTicketFilter] = useState({ date: today(), status: “”, clientId: “” });
const [search, setSearch] = useState(“”);
useEffect(() => { localStorage.setItem(STORAGE_KEY_CLIENTS, JSON.stringify(clients)); }, [clients]);
useEffect(() => { localStorage.setItem(STORAGE_KEY_TICKETS, JSON.stringify(tickets)); }, [tickets]);
function saveClient(data) {
if (data.id) setClients((p) => p.map((c) => (c.id === data.id ? data : c)));
else setClients((p) => […p, { …data, id: `c${Date.now()}` }]);
setClientForm(null);
}
function deleteClient(id) {
if (!confirm(“Eliminare questo cliente?”)) return;
setClients((p) => p.filter((c) => c.id !== id));
setTickets((p) => p.filter((t) => t.clientId !== id));
}
function saveTicket(data) {
if (data.id) setTickets((p) => p.map((t) => (t.id === data.id ? data : t)));
else setTickets((p) => […p, { …data, id: `t${Date.now()}`, createdAt: Date.now(), date: today() }]);
setTicketForm(null);
}
function deleteTicket(id) { if (!confirm(“Eliminare questo ticket?”)) return; setTickets((p) => p.filter((t) => t.id !== id)); }
function exportExcel() {
const filtered = tickets.filter(
(t) =>
(!ticketFilter.date || t.date === ticketFilter.date) &&
(!ticketFilter.status || t.status === ticketFilter.status) &&
(!ticketFilter.clientId || t.clientId === ticketFilter.clientId)
).sort((a, b) => a.createdAt – b.createdAt);
const rows = filtered.map((t, i) => {
const cl = clients.find((c) => c.id === t.clientId);
return {
“N.”: i + 1,
“Data”: formatDate(t.date),
“Orario”: formatTime(t.createdAt),
“Cliente”: cl?.name ?? “”,
“Azienda”: cl?.company ?? “”,
“Tel. cliente”: cl?.phone ?? “”,
“Oggetto”: t.subject,
“Descrizione”: t.description ?? “”,
“Priorita”: t.priority,
“Stato”: t.status,
“Tecnico”: t.tech ?? “”,
};
});
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(rows);
ws[“!cols”] = [
{ wch: 4 }, { wch: 12 }, { wch: 8 }, { wch: 22 }, { wch: 22 },
{ wch: 16 }, { wch: 32 }, { wch: 42 }, { wch: 10 }, { wch: 14 }, { wch: 16 },
];
XLSX.utils.book_append_sheet(wb, ws, “Assistenze”);
const clientRows = clients.map((c) => ({
“Nome”: c.name, “Azienda”: c.company, “Email”: c.email,
“Telefono”: c.phone, “Indirizzo”: c.address, “Note”: c.note,
“Ticket totali”: tickets.filter((t) => t.clientId === c.id).length,
}));
const wsc = XLSX.utils.json_to_sheet(clientRows);
wsc[“!cols”] = [{ wch: 22 }, { wch: 22 }, { wch: 30 }, { wch: 16 }, { wch: 32 }, { wch: 32 }, { wch: 14 }];
XLSX.utils.book_append_sheet(wb, wsc, “Clienti”);
const label = ticketFilter.date ? `_${ticketFilter.date}` : “”;
XLSX.writeFile(wb, `assistenze${label}.xlsx`);
}
function printDaily() {
const filtered = tickets.filter(
(t) =>
(!ticketFilter.date || t.date === ticketFilter.date) &&
(!ticketFilter.status || t.status === ticketFilter.status) &&
(!ticketFilter.clientId || t.clientId === ticketFilter.clientId)
).sort((a, b) => a.createdAt – b.createdAt);
const w = window.open(“”, “_blank”);
w.document.write(`
Registro assistenze
Data: ${formatDate(ticketFilter.date) || “tutte”} | Totale: ${filtered.length} ticket
${filtered.map((t, i) => {const cl = clients.find((c) => c.id === t.clientId);const sc = { Aperto: “open”, “In lavorazione”: “progress”, Chiuso: “closed” }[t.status] || “open”;const pc = { Alta: “alta”, Media: “media”, Bassa: “bassa” }[t.priority] || “bassa”;return “;
}).join(“”)}
| # | Orario | Cliente | Oggetto | Priorita | Stato | Tecnico |
|---|---|---|---|---|---|---|
| ${i + 1} | ${formatTime(t.createdAt)} | ${cl?.name ?? “”}
${cl?.company ?? “”}
|
${t.subject}
${t.description ?? “”}
|
${t.priority} | ${t.status} | ${t.tech ?? “”} |
`);
w.document.close(); w.print();
}
const todayTickets = tickets.filter((t) => t.date === today());
const openTickets = tickets.filter((t) => t.status !== “Chiuso”);
return (
CRM aziendale gestione clienti e ticket di assistenza
CRM Assistenza
{new Date().toLocaleDateString(“it-IT”, { weekday: “long”, day: “numeric”, month: “long”, year: “numeric” })}
{ label: “Clienti totali”, value: clients.length, icon: “ti-users”, color: “#185FA5”, bg: “#E6F1FB” },
{ label: “Ticket oggi”, value: todayTickets.length, icon: “ti-ticket”, color: “#633806”, bg: “#FAEEDA” },
{ label: “Aperti / In lav.”, value: openTickets.length, icon: “ti-clock”, color: “#A32D2D”, bg: “#FCEBEB” },
{ label: “Chiusi oggi”, value: todayTickets.filter((t) => t.status === “Chiuso”).length, icon: “ti-circle-check”, color: “#27500A”, bg: “#EAF3DE” },
].map((m) => (
))}
{todayTickets.length === 0 ? (
) : (
const cl = clients.find((c) => c.id === t.clientId);
const sc = STATUS_COLORS[t.status] || STATUS_COLORS[“Aperto”];
const pc = PRIORITY_COLORS[t.priority] || PRIORITY_COLORS[“Bassa”];
return (
{t.status}
);
})}
)}
)}
{view === “clients” && (
setSearch(e.target.value)} style={{ width: “100%”, paddingLeft: 32 }} />
{(() => {
const filtered = clients.filter((c) => !search || `${c.name} ${c.company} ${c.email}`.toLowerCase().includes(search.toLowerCase()));
if (filtered.length === 0) return (
);
return (
const av = avatarColor(cl.name);
const cTickets = tickets.filter((t) => t.clientId === cl.id);
return (
{cl.phone && {cl.phone}}
);
})}
);
})()}
)}
{view === “tickets” && (
{(() => {
const filtered = tickets.filter(
(t) =>
(!ticketFilter.date || t.date === ticketFilter.date) &&
(!ticketFilter.status || t.status === ticketFilter.status) &&
(!ticketFilter.clientId || t.clientId === ticketFilter.clientId)
).sort((a, b) => b.createdAt – a.createdAt);
if (filtered.length === 0) return (
Nessun ticket per i filtri selezionati
);
return (
const cl = clients.find((c) => c.id === t.clientId);
const sc = STATUS_COLORS[t.status] || STATUS_COLORS[“Aperto”];
const pc = PRIORITY_COLORS[t.priority] || PRIORITY_COLORS[“Bassa”];
return (
{t.description &&
}
{formatDate(t.date)} {formatTime(t.createdAt)}
{t.tech && {t.tech}}
{t.status}
);
})}
);
})()}
)}
{clientForm && (
{clientForm.id ? “Modifica cliente” : “Nuovo cliente”}
setClientForm(null)} />
)}
{ticketForm && (
{ticketForm.id ? “Modifica ticket” : “Nuovo ticket”}
setTicketForm(null)} clients={clients} />
)}
);
}
function Field({ label, children }) {
return (
{children}
);
}
function ClientForm({ data, onChange, onSave, onCancel }) {
const set = (k) => (e) => onChange((d) => ({ …d, [k]: e.target.value }));
return (

