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.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 (
);
}
// DETAIL VIEW
if (selectedItem) {
return (
setSelectedId(null)}
onEdit={() => setEditingId(selectedItem.id)}
onUpdate={handleDetailUpdate}
/>
);
}
// LIST VIEW
return (
{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)}
/>
))}
))}
);
}
ReactDOM.createRoot(document.getElementById('root')).render();