import { useCollection, useIdentity } from '@deplixo/sdk'; import ItemForm from './components/ItemForm.jsx'; import ItemDetail from './components/ItemDetail.jsx'; import { buildSeedItems } from './components/seedData.js'; import { TIPOS, formatEuro, formatDatePT, annualCost, daysUntil, statusForDays, mostUrgentDeadline, nextDeadline, allDeadlines, monthKey, monthLabel, toUpper } from './components/helpers.js'; function Summary({ items, totalAnnual }) { const buckets = { green: 0, yellow: 0, orange: 0, red: 0, gray: 0 }; for (const it of items) { const urg = mostUrgentDeadline(it); if (!urg) continue; const d = daysUntil(urg.date); const s = statusForDays(d); if (s === 'green') buckets.green++; else if (s === 'yellow') buckets.yellow++; else if (s === 'orange') buckets.orange++; else if (s === 'red' || s === 'darkred') buckets.red++; else if (s === 'gray') buckets.gray++; } const monthly = totalAnnual / 12; return (
TOTAL DE ITENS
{items.length}
{buckets.green}
🟢 EM ORDEM
{buckets.yellow}
🟡 ATENÇÃO
{buckets.orange}
🟠 PRÓXIMO
{buckets.red}
🔴 URGENTE
{buckets.gray}
⚪ INCUMPRIMENTO
CUSTO FIXO ANUAL
{formatEuro(totalAnnual)}
CUSTO FIXO MENSAL
{formatEuro(monthly)}
); } function ItemCard({ item, onClick }) { const urg = mostUrgentDeadline(item); const next = nextDeadline(item); const days = urg ? daysUntil(urg.date) : null; const status = statusForDays(days); const subParts = []; if (item.matricula) subParts.push(item.matricula); if (item.marca) subParts.push(item.marca); if (item.modelo) subParts.push(item.modelo); if (item.companhia) subParts.push(item.companhia); if (item.entidade && !subParts.length) subParts.push(item.entidade); if (item.apolice) subParts.push(`APÓLICE ${item.apolice}`); const sub = subParts.join(' · '); let daysLabel = ''; if (days === null) daysLabel = 'SEM PRAZO'; else if (days < 0) daysLabel = `VENCEU HÁ ${Math.abs(days)}D`; else if (days === 0) daysLabel = 'HOJE'; else daysLabel = `${days}D`; const deadlines = allDeadlines(item); const extraDeadlines = deadlines.filter(d => d.date !== (next && next.date)); return ( ); } function App() { const { user } = useIdentity(); const { items: allItems, loading, add, update, remove } = useCollection('frota-items'); const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState('TODOS'); const [selectedId, setSelectedId] = useState(null); const [editingId, setEditingId] = useState(null); const [creating, setCreating] = useState(false); const dialogRef = useRef(null); // Only this user's items (private app) const items = useMemo(() => { if (!user) return []; return (allItems || []).filter(raw => raw.author?.id === user.id).map(raw => ({ id: raw.id, ...(raw.value || {}), })); }, [allItems, user]); // Global search filter — matches across NOME, DESCRIÇÃO, MATRÍCULA, MARCA, // MODELO, NÚMERO, APÓLICE, ENTIDADE and TIPO (plus companhia/notas as before). const searched = useMemo(() => { const q = search.trim().toLocaleLowerCase('pt-PT'); let list = items; if (q) { list = list.filter(it => { const hay = [ it.nome, // NOME / DESCRIÇÃO it.descricao, // DESCRIÇÃO (campo alternativo, se existir) it.matricula, // MATRÍCULA it.marca, // MARCA it.modelo, // MODELO it.numero, // NÚMERO it.apolice, // APÓLICE it.entidade, // ENTIDADE it.tipo, // TIPO it.companhia, it.notas, ].filter(Boolean).join(' ').toLocaleLowerCase('pt-PT'); return hay.includes(q); }); } if (typeFilter !== 'TODOS') { list = list.filter(it => it.tipo === typeFilter); } return list; }, [items, search, typeFilter]); // Total annual cost (across all items) const totalAnnual = useMemo(() => { return items.reduce((sum, it) => sum + annualCost(it.valor, it.periodicidade), 0); }, [items]); const hasSeed = useMemo(() => items.some(it => it.seed), [items]); // Open/close dialog imperatively useEffect(() => { const dlg = dialogRef.current; if (!dlg) return; if (creating || editingId) { if (!dlg.open) dlg.showModal(); } else { if (dlg.open) dlg.close(); } }, [creating, editingId]); function closeDialog() { setCreating(false); setEditingId(null); } async function handleSave(payload) { if (editingId) { await update(editingId, payload); } else { await add(payload); } closeDialog(); } async function handleDelete() { if (editingId) { await remove(editingId); setEditingId(null); setSelectedId(null); } } async function handleAddSeed() { const seeds = buildSeedItems(); for (const s of seeds) await add(s); } async function handleRemoveSeed() { const seedItems = items.filter(i => i.seed); for (const s of seedItems) await remove(s.id); } // Direct update from the detail view (notification actions) async function handleDetailUpdate(payload) { if (!selectedId) return; await update(selectedId, payload); } // Build grouped list (incumprimento + months) const { incumprimentoItems, monthGroups } = useMemo(() => { const withDate = searched.map(it => { const urg = mostUrgentDeadline(it); const next = nextDeadline(it); // Sort by next deadline date; if none, treat as far future const sortDate = next ? next.date : '9999-12-31'; const days = urg ? daysUntil(urg.date) : null; return { it, next, urg, sortDate, days }; }); const incump = withDate .filter(x => x.urg && daysUntil(x.urg.date) < 0) .sort((a, b) => a.urg.date.localeCompare(b.urg.date)); const future = withDate .filter(x => !(x.urg && daysUntil(x.urg.date) < 0)) .sort((a, b) => a.sortDate.localeCompare(b.sortDate)); const groups = {}; for (const entry of future) { const key = entry.next ? monthKey(entry.next.date) : 'sem-prazo'; if (!groups[key]) groups[key] = []; groups[key].push(entry); } const orderedKeys = Object.keys(groups).sort((a, b) => { if (a === 'sem-prazo') return 1; if (b === 'sem-prazo') return -1; return a.localeCompare(b); }); return { incumprimentoItems: incump, monthGroups: orderedKeys.map(k => ({ key: k, entries: groups[k] })), }; }, [searched]); const selectedItem = selectedId ? items.find(i => i.id === selectedId) : null; const editingItem = editingId ? items.find(i => i.id === editingId) : null; if (loading) { return (
A carregar…
); } // DETAIL VIEW if (selectedItem) { return (
setSelectedId(null)} onEdit={() => setEditingId(selectedItem.id)} onUpdate={handleDetailUpdate} />

{editingItem ? 'EDITAR ITEM' : 'NOVO ITEM'}

{(creating || editingItem) && ( )}
); } // LIST VIEW return (

MINHA FROTA

PRAZOS · CUSTOS · DOCUMENTOS
{items.length === 0 && (
SEM ITENS — EXPERIMENTE COM DADOS DE DEMONSTRAÇÃO
)} {hasSeed && (
DADOS DE DEMONSTRAÇÃO ATIVOS
)}
setSearch(e.target.value)} data-testid="search-input" aria-label="Pesquisar itens por nome, descrição, matrícula, marca, modelo, número, apólice, entidade ou tipo" />
{TIPOS.map((t) => ( ))}
{searched.length === 0 && items.length > 0 && (

SEM RESULTADOS

Nenhum item corresponde aos filtros.

)} {incumprimentoItems.length > 0 && ( <>
⚠️ INCUMPRIMENTO
{incumprimentoItems.map(({ it }) => ( setSelectedId(it.id)} /> ))} )} {monthGroups.map((group) => (
{group.key === 'sem-prazo' ? 'SEM PRAZO DEFINIDO' : monthLabel(group.key)}
{group.entries.map(({ it }) => ( setSelectedId(it.id)} /> ))}
))}

{editingItem ? 'EDITAR ITEM' : 'NOVO ITEM'}

{(creating || editingItem) && ( )}
); } ReactDOM.createRoot(document.getElementById('root')).render();