import { useCollection, useIdentity } from '@deplixo/sdk'; import { Icon } from './components/icons.jsx'; import { TIPI_LAVORO, STATI, statusClass, formatDate, formatPrice, initials, MESI_SHORT } from './components/constants.js'; import WorkOrderDialog from './components/WorkOrderDialog.jsx'; import CustomerDialog from './components/CustomerDialog.jsx'; function App() { const { user } = useIdentity(); const { items: allLavori, add: addLavoro, update: updateLavoro, remove: removeLavoro } = useCollection('lavori'); const { items: allClienti, add: addCliente, update: updateCliente } = useCollection('clienti'); const lavori = (allLavori || []).filter((x) => x.author?.id === user?.id); const clienti = (allClienti || []).filter((x) => x.author?.id === user?.id); const [view, setView] = useState('dashboard'); // dashboard | lavori | clienti | detail const [selectedId, setSelectedId] = useState(null); const [editingWO, setEditingWO] = useState(null); const [editingClient, setEditingClient] = useState(null); const [toast, setToast] = useState(''); // filters for lavori list const [filterStato, setFilterStato] = useState('TUTTI'); const [filterTipo, setFilterTipo] = useState('TUTTI'); const [search, setSearch] = useState(''); const woDialogRef = useRef(null); const custDialogRef = useRef(null); const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(''), 2200); }; const openNewWO = () => { setEditingWO(null); setTimeout(() => woDialogRef.current?.showModal(), 0); }; const openEditWO = (l) => { setEditingWO({ id: l.id, ...(l.value || {}) }); setTimeout(() => woDialogRef.current?.showModal(), 0); }; const closeWODialog = () => { woDialogRef.current?.close(); setEditingWO(null); }; const openNewClient = () => { setEditingClient(null); setTimeout(() => custDialogRef.current?.showModal(), 0); }; const openEditClient = (c) => { setEditingClient({ id: c.id, ...(c.value || {}) }); setTimeout(() => custDialogRef.current?.showModal(), 0); }; const closeCustDialog = () => { custDialogRef.current?.close(); setEditingClient(null); }; // Save work order — also upserts a customer entry const saveWO = async (data) => { // upsert customer const key = `${(data.nome||'').toLowerCase().trim()}|${(data.cognome||'').toLowerCase().trim()}|${(data.azienda||'').toLowerCase().trim()}`; const existing = clienti.find((c) => { const v = c.value || {}; const k = `${(v.nome||'').toLowerCase().trim()}|${(v.cognome||'').toLowerCase().trim()}|${(v.azienda||'').toLowerCase().trim()}`; return k === key && key !== '||'; }); if (!existing && (data.nome || data.cognome || data.azienda)) { await addCliente({ nome: data.nome, cognome: data.cognome, azienda: data.azienda, telefono: data.telefono, email: data.email, indirizzo: '', note: '', }); } if (data.id) { const { id, ...payload } = data; await updateLavoro(id, payload); showToast('Lavoro aggiornato'); } else { await addLavoro(data); showToast('Lavoro creato'); } closeWODialog(); }; const saveClient = async (data) => { if (data.id) { const { id, ...payload } = data; await updateCliente(id, payload); showToast('Cliente aggiornato'); } else { await addCliente(data); showToast('Cliente creato'); } closeCustDialog(); }; const deleteWO = async (id) => { await removeLavoro(id); showToast('Lavoro eliminato'); setView('lavori'); setSelectedId(null); }; const changeStato = async (l, s) => { await updateLavoro(l.id, { ...(l.value || {}), stato: s }); showToast(`Stato: ${s}`); }; // stats const inCorso = lavori.filter((l) => l.value?.stato === 'IN CORSO').length; const daFare = lavori.filter((l) => l.value?.stato === 'DA FARE').length; const completati = lavori.filter((l) => l.value?.stato === 'COMPLETATO' || l.value?.stato === 'CONSEGNATO').length; const inAttesa = lavori.filter((l) => l.value?.stato === 'IN ATTESA').length; const upcoming = lavori .filter((l) => l.value?.dataPrevista && !['COMPLETATO', 'CONSEGNATO'].includes(l.value?.stato)) .sort((a, b) => new Date(a.value.dataPrevista) - new Date(b.value.dataPrevista)) .slice(0, 5); // filtered work list const filtered = lavori .filter((l) => filterStato === 'TUTTI' ? true : l.value?.stato === filterStato) .filter((l) => filterTipo === 'TUTTI' ? true : l.value?.tipoLavoro === filterTipo) .filter((l) => { if (!search.trim()) return true; const s = search.toLowerCase(); const v = l.value || {}; return [v.nome, v.cognome, v.azienda, v.targa, v.marca, v.modello, v.veicolo] .filter(Boolean).some((f) => f.toLowerCase().includes(s)); }) .sort((a, b) => { const da = a.value?.dataPrevista || a.createdAt; const db = b.value?.dataPrevista || b.createdAt; return new Date(db) - new Date(da); }); const selected = lavori.find((l) => l.id === selectedId); const goToDetail = (id) => { setSelectedId(id); setView('detail'); }; const customerLavori = (nome, cognome, azienda) => { const key = `${(nome||'').toLowerCase().trim()}|${(cognome||'').toLowerCase().trim()}|${(azienda||'').toLowerCase().trim()}`; return lavori.filter((l) => { const v = l.value || {}; const k = `${(v.nome||'').toLowerCase().trim()}|${(v.cognome||'').toLowerCase().trim()}|${(v.azienda||'').toLowerCase().trim()}`; return k === key; }); }; return (
LH

LavoriHub

{user?.name || 'Ospite'}
{view === 'dashboard' && ( )} {view === 'lavori' && ( )} {view === 'clienti' && ( )} {view === 'detail' && selected && ( setView('lavori')} onEdit={() => openEditWO(selected)} onDelete={() => deleteWO(selected.id)} onChangeStato={(s) => changeStato(selected, s)} /> )} {view === 'detail' && !selected && (

Lavoro non trovato

)} {toast &&
{toast}
}
); } /* ------------ Dashboard ------------ */ function DashboardView({ inCorso, daFare, completati, inAttesa, upcoming, onOpenLavoro, onNewLavoro }) { return (

Dashboard

In corso
{inCorso}
Da iniziare
{daFare}
Completati
{completati}
In attesa
{inAttesa}
Prossimi lavori
{upcoming.length === 0 ? (

Nessun lavoro programmato

Crea un nuovo lavoro per iniziare

) : (
{upcoming.map((l) => { const v = l.value || {}; const d = v.dataPrevista ? new Date(v.dataPrevista) : null; return ( ); })}
)}
); } /* ------------ Lavori list ------------ */ function LavoriView({ lavori, totalCount, filterStato, setFilterStato, filterTipo, setFilterTipo, search, setSearch, onOpen, onNew }) { return (

Lavori

setSearch(e.target.value)} data-testid="lavori-search-input" aria-label="Cerca lavori" />
{['TUTTI', ...STATI].map((s) => ( ))}
{['TUTTI', ...TIPI_LAVORO].map((t) => ( ))}
{lavori.length === 0 ? (

{totalCount === 0 ? 'Nessun lavoro' : 'Nessun risultato'}

{totalCount === 0 ? 'Tocca + per crearne uno' : 'Prova a modificare i filtri'}

) : (
{lavori.map((l) => { const v = l.value || {}; return ( ); })}
)}
); } /* ------------ Clienti ------------ */ function ClientiView({ clienti, onNew, onEdit, customerLavori, onOpenLavoro }) { const [expandedId, setExpandedId] = useState(null); const [q, setQ] = useState(''); const filtered = clienti.filter((c) => { if (!q.trim()) return true; const v = c.value || {}; const s = q.toLowerCase(); return [v.nome, v.cognome, v.azienda, v.telefono, v.email].filter(Boolean).some((f) => f.toLowerCase().includes(s)); }); return (

Clienti

setQ(e.target.value)} data-testid="clienti-search-input" aria-label="Cerca clienti" />
{filtered.length === 0 ? (

{clienti.length === 0 ? 'Nessun cliente' : 'Nessun risultato'}

{clienti.length === 0 ? 'Tocca + per aggiungere un cliente' : ''}

) : (
{filtered.map((c) => { const v = c.value || {}; const woList = customerLavori(v.nome, v.cognome, v.azienda); const isOpen = expandedId === c.id; return (
{isOpen && (
{v.telefono &&
Telefono{v.telefono}
} {v.email &&
Email{v.email}
} {v.indirizzo &&
Indirizzo{v.indirizzo}
} {v.azienda &&
Azienda{v.azienda}
} {v.note && ( <>
Note
{v.note}
)}
Lavori ({woList.length})
{woList.length === 0 ? (
Nessun lavoro registrato
) : (
{woList.map((l) => { const lv = l.value || {}; return ( ); })}
)}
)}
); })}
)}
); } /* ------------ Detail ------------ */ function DetailView({ lavoro, onBack, onEdit, onDelete, onChangeStato }) { const v = lavoro.value || {}; const [confirmDel, setConfirmDel] = useState(false); const margine = (v.prezzo && v.costoMateriale) ? (Number(v.prezzo) - Number(v.costoMateriale)) : null; return ( <>
{[v.nome, v.cognome].filter(Boolean).join(' ') || v.azienda || 'Lavoro'}
{v.tipoLavoro || 'SERVIZIO'} {v.stato || 'DA FARE'}

Cambia stato

{STATI.map((s) => ( ))}

Cliente

Nome{[v.nome, v.cognome].filter(Boolean).join(' ') || '—'}
{v.azienda &&
Azienda{v.azienda}
} {v.telefono && (
Telefono {v.telefono}
)} {v.email && (
Email {v.email}
)}

Veicolo

{v.veicolo &&
Tipo{v.veicolo}
}
Marca{v.marca || '—'}
Modello{v.modello || '—'}
Targa{v.targa || '—'}

Date

Data prevista{formatDate(v.dataPrevista)}
Data consegna{formatDate(v.dataConsegna)}

Prezzo & Costi

Prezzo{formatPrice(v.prezzo)}
Costo materiale{formatPrice(v.costoMateriale)}
{margine !== null && (
Margine = 0 ? 'var(--success)' : 'var(--danger)' }}>{formatPrice(margine)}
)}
{v.note && (

Note

{v.note}
)} {(v.foto || []).length > 0 && (

Foto ({v.foto.length})

{v.foto.map((p) => (
{p.name}
))}
)} {(v.allegati || []).length > 0 && (

Allegati ({v.allegati.length})

{v.allegati.map((a) => (
{a.name} Apri
))}
)} {!confirmDel ? ( ) : (
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();