function ScreenReports() { const { BRL, BRLk } = window.SCP; const [period, setPeriod] = useState('90d'); const [productId, setProductId] = useState(null); const [data, setData] = useState({ quotations: [], orders: [], products: [], receivings: [] }); const [loading, setLoading] = useState(true); const [loadErr, setLoadErr] = useState(null); const [priceHistory, setPriceHistory] = useState([]); useEffect(() => { let mounted = true; (async () => { try { const [d, sups] = await Promise.all([ window.scpDb.dashboard.load(), window.scpDb.suppliers.list(), ]); if (!mounted) return; setData({ ...d, suppliers: sups }); // Default: produto mais cotado const topPid = computeTopProduct(d.orders); if (topPid) setProductId(topPid); } catch (e) { if (mounted) setLoadErr(e.message || 'Falha ao carregar relatórios'); } finally { if (mounted) setLoading(false); } })(); return () => { mounted = false; }; }, []); // Carrega histórico de preço quando productId muda useEffect(() => { if (!productId || !data.orders?.length) { setPriceHistory([]); return; } const points = []; (data.orders || []) .filter(o => o.status !== 'cancelado' && o.emitted_at) .forEach(o => { (o.items || []).forEach(it => { if (it.product_id === productId && it.unit_price) { points.push({ d: new Date(o.emitted_at), v: Number(it.unit_price) }); } }); }); points.sort((a, b) => a.d - b.d); setPriceHistory(points.map((p, i) => ({ d: p.d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }).replace('.', ''), v: p.v, }))); }, [productId, data.orders]); const days = period === '30d' ? 30 : period === '90d' ? 90 : period === '12m' ? 365 : 365 * 5; const today = new Date(); const periodStart = new Date(today.getTime() - days * 86400000); const inPeriod = (d) => d && new Date(d) >= periodStart; // ====== KPIs ====== const kpis = useMemo(() => { const fechadasPeriod = (data.quotations || []).filter(q => q.status === 'fechada' && inPeriod(q.closed_at)); const allClosed = (data.quotations || []).filter(q => q.status === 'fechada'); const economiaTotal = allClosed.reduce((s, q) => s + Number(q.saving_value || 0), 0); const cotacoesFechadas = fechadasPeriod.length; const ordersPeriod = (data.orders || []).filter(o => inPeriod(o.emitted_at) && o.status !== 'cancelado'); const volume = ordersPeriod.reduce((s, o) => s + Number(o.total || 0), 0); const totalInvited = (data.quotations || []).reduce((s, q) => s + (q.invited?.length || 0), 0); const totalResponded = (data.quotations || []).reduce((s, q) => s + (q.invited || []).filter(i => i.status === 'respondida').length, 0); const taxa = totalInvited ? Math.round(100 * totalResponded / totalInvited) : 0; return { economiaTotal, cotacoesFechadas, volume, taxa }; }, [data, period]); // ====== Evolução de preço ====== const priceStats = useMemo(() => { if (!priceHistory.length) return { min: 0, max: 0, current: 0, variation: 0 }; const vals = priceHistory.map(p => p.v); const min = Math.min(...vals); const max = Math.max(...vals); const current = vals[vals.length - 1]; const first = vals[0]; const variation = first ? ((current - first) / first) * 100 : 0; return { min, max, current, variation }; }, [priceHistory]); // ====== Savings (últimos 6 meses) ====== const savingsData = useMemo(() => { const byMonth = new Map(); for (let i = 5; i >= 0; i--) { const d = new Date(today.getFullYear(), today.getMonth() - i, 1); const key = d.toISOString().slice(0, 7); const label = d.toLocaleDateString('pt-BR', { month: 'short', year: '2-digit' }).replace('.', '').replace(' de ', '/'); byMonth.set(key, { m: label, value: 0, quotes: 0 }); } (data.quotations || []) .filter(q => q.status === 'fechada' && q.closed_at && q.saving_value) .forEach(q => { const key = new Date(q.closed_at).toISOString().slice(0, 7); if (byMonth.has(key)) { const row = byMonth.get(key); row.value += Number(q.saving_value); row.quotes += 1; } }); return Array.from(byMonth.values()); }, [data]); // ====== Performance dos fornecedores ====== const supplierPerf = useMemo(() => { const map = {}; (data.orders || []).forEach(o => { if (o.status === 'cancelado') return; const sid = o.supplier?.id; const name = o.supplier?.name; if (!sid || !name) return; if (!map[sid]) map[sid] = { sid, name, wins: 0 }; map[sid].wins += 1; }); const arr = Object.values(map).sort((a, b) => b.wins - a.wins).slice(0, 10); const max = arr[0]?.wins || 1; return arr.map(s => ({ ...s, share: Math.round(100 * s.wins / max) })); }, [data]); // ====== Lista de produtos (para o select de evolução de preços) ====== const productOptions = useMemo(() => { const used = new Map(); (data.orders || []).forEach(o => { (o.items || []).forEach(it => { if (!it.product_id) return; used.set(it.product_id, (used.get(it.product_id) || 0) + 1); }); }); return (data.products || []) .filter(p => used.has(p.id)) .map(p => ({ id: p.id, name: p.name, count: used.get(p.id) || 0 })) .sort((a, b) => b.count - a.count) .slice(0, 20); }, [data]); const dateRangeLabel = `${periodStart.toLocaleDateString('pt-BR')} até ${today.toLocaleDateString('pt-BR')}`; if (loading) { return