import { type FormEvent, useEffect, useState } from 'react'; import { Link as RouterLink, Navigate, useParams } from 'react-router-dom'; import { AlertTriangle, ArrowLeft, ArrowRight, BarChart3, Boxes, ClipboardCheck, ClipboardList, Download, Link as LinkIcon, Package, RefreshCw, Repeat2, Ruler, Save, Search, Scissors, Truck, Trash2, Warehouse, } from 'lucide-react'; import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchSupplySummary } from '../dataService'; import type { SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types'; type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements'; type ReceiptView = 'new' | 'pending' | 'history'; const pageClassName = 'mx-auto flex w-full max-w-7xl flex-col gap-6'; const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm'; const buttonClassName = 'inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer'; const inputClassName = 'h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary'; const emptyStateClassName = 'flex min-h-[190px] flex-col items-center justify-center gap-2 px-4 py-10 text-center'; const emptySupplySummary: SupplySummary = { receipts: [], lots: [], movements: [], fabricPlans: [], purchaseNeeds: [], stats: { totalQuantityKg: 0, activeLots: 0, rolls: 0, alerts: 0, pendingReceipts: 0, approvedReceipts: 0, }, }; const formatNumber = (value: number, maximumFractionDigits = 2) => ( new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value) ); const parseDecimal = (value: string) => { const number = Number(value.replace(',', '.')); return Number.isFinite(number) ? number : 0; }; const formatDateTime = (value: string | null) => { if (!value) return '-'; return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', }).format(new Date(value)); }; const normalizeSearch = (value: string) => value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); const exportCsv = (filename: string, rows: Array>) => { if (!rows.length) return; const headers = Object.keys(rows[0]); const escapeCell = (value: string | number | null) => `"${String(value ?? '').replace(/"/g, '""')}"`; const csv = [ headers.join(','), ...rows.map(row => headers.map(header => escapeCell(row[header])).join(',')), ].join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.click(); URL.revokeObjectURL(url); }; const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarChart3 }> = [ { id: 'dashboard', name: 'Dashboard', icon: Warehouse }, { id: 'balance', name: 'Saldo', icon: BarChart3 }, { id: 'receipts', name: 'Recebimentos', icon: Truck }, { id: 'inventory', name: 'Inventário', icon: ClipboardCheck }, { id: 'movements', name: 'Movimentações', icon: Repeat2 }, ]; const receiptCategories: Array<{ name: string; icon: typeof Package }> = [ { name: 'Malha / Tecido', icon: Ruler }, { name: 'Embalagem', icon: Package }, { name: 'Material de Limpeza', icon: Boxes }, { name: 'Acessórios / Botões', icon: Warehouse }, { name: 'Etiquetas', icon: LinkIcon }, { name: 'Outro material', icon: ClipboardList }, ]; const Header = ({ title, subtitle, backTo }: { title: string; subtitle: string; backTo?: string }) => (
{backTo && ( Suprimentos )}

{title}

{subtitle}

); const ModuleCard = ({ title, description, icon: Icon, to, }: { title: string; description: string; icon: typeof Package; to: string; }) => { return (

{title}

{description}

); }; const SuppliesHub = () => (
); const StatGrid = ({ summary }: { summary: SupplySummary }) => { const stats = [ { label: 'Total em estoque', value: `${formatNumber(summary.stats.totalQuantityKg)} kg` }, { label: 'Lotes ativos', value: `${summary.stats.activeLots}` }, { label: 'Rolos', value: `${formatNumber(summary.stats.rolls, 0)}` }, { label: 'Alertas', value: `${summary.stats.alerts}` }, ]; return (
{stats.map(stat => (

{stat.value}

{stat.label}

))}
); }; const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary; onNewReceipt: () => void }) => { const categories = summary.lots.reduce>((acc, lot) => { const current = acc[lot.category] || { quantity: 0, unit: lot.unit, lots: 0 }; acc[lot.category] = { quantity: current.quantity + lot.quantity, unit: current.unit === lot.unit ? lot.unit : 'mix', lots: current.lots + 1, }; return acc; }, {}); const latestReceipts = summary.receipts.slice(0, 4); return (

Por categoria de material

{Object.keys(categories).length ? (
{Object.entries(categories).map(([category, data]) => (

{category}

{formatNumber(data.quantity)} {data.unit}

{data.lots} lote(s) ativo(s)

))}
) : (

Nenhuma categoria com saldo

Registre entradas para agrupar o estoque por tipo de material.

)}

Alertas de estoque

Nenhum alerta no momento.

Últimas entradas

{latestReceipts.length ? (
{latestReceipts.map(receipt => (

{receipt.product}

{receipt.category} · {formatDateTime(receipt.createdAt)}

{formatNumber(receipt.quantity)} {receipt.unit}

{receipt.status === 'approved' ? 'Aprovado' : 'Pendente'}
))}
) : (

Nenhuma entrada registrada.

)}
); }; const BalanceTab = ({ summary, onRefresh }: { summary: SupplySummary; onRefresh: () => void }) => { const [search, setSearch] = useState(''); const [category, setCategory] = useState('all'); const [supplier, setSupplier] = useState('all'); const categories = Array.from(new Set(summary.lots.map(lot => lot.category))).sort(); const suppliers = Array.from(new Set(summary.lots.map(lot => lot.supplier || 'Sem fornecedor'))).sort(); const normalizedSearch = normalizeSearch(search); const visibleLots = summary.lots.filter(lot => { const lotSupplier = lot.supplier || 'Sem fornecedor'; const matchesSearch = !normalizedSearch || normalizeSearch(`${lot.product} ${lot.category} ${lotSupplier} ${lot.invoice} ${lot.id}`).includes(normalizedSearch); const matchesCategory = category === 'all' || lot.category === category; const matchesSupplier = supplier === 'all' || lotSupplier === supplier; return matchesSearch && matchesCategory && matchesSupplier; }); return (

Saldo por tipo

Clique para expandir lotes
{visibleLots.length ? (
Material Categoria Saldo Fornecedor Lote
{visibleLots.map(lot => (

{lot.product}

{lot.category}

{formatNumber(lot.quantity)} {lot.unit}

{lot.supplier || 'Sem fornecedor'}

#{lot.id} · {lot.invoice || 'sem NF'}

))}
) : (

{summary.lots.length ? 'Nenhum lote encontrado' : 'Nenhum item em estoque'}

{summary.lots.length ? 'Ajuste a busca ou os filtros.' : 'Registre entradas para ver o saldo aqui.'}

)}
); }; const ReceiptList = ({ receipts, emptyTitle, onApprove, onRemove, }: { receipts: SupplyReceipt[]; emptyTitle: string; onApprove: (receiptId: number) => void; onRemove: (receiptId: number) => void; }) => (
{receipts.length ? (
{receipts.map(receipt => (

{receipt.product}

{receipt.category} · {receipt.invoice ? `NF ${receipt.invoice}` : 'sem NF'} · {formatDateTime(receipt.createdAt)}

{formatNumber(receipt.quantity)} {receipt.unit}

{receipt.supplier}

{receipt.status === 'approved' ? 'Aprovado' : 'Pendente'}
{receipt.status === 'pending' && ( )}
))}
) : (

{emptyTitle}

Registre um recebimento para preencher esta lista.

)}
); const ReceiptsTab = ({ receipts, onCreate, onApprove, onRemove, isBusy, }: { receipts: SupplyReceipt[]; onCreate: (payload: { category: string; product: string; quantity: number; unit: string; supplier: string; invoice: string; notes: string }) => Promise; onApprove: (receiptId: number) => Promise; onRemove: (receiptId: number) => Promise; isBusy: boolean; }) => { const [activeView, setActiveView] = useState('new'); const [selectedCategory, setSelectedCategory] = useState(receiptCategories[0].name); const [form, setForm] = useState({ product: '', quantity: '', unit: 'kg', supplier: '', invoice: '', notes: '', }); const pendingReceipts = receipts.filter(receipt => receipt.status === 'pending'); const visibleReceipts = activeView === 'pending' ? pendingReceipts : receipts; const handleReceiptSubmit = async (event: FormEvent) => { event.preventDefault(); const product = form.product.trim(); const quantity = parseDecimal(form.quantity); if (!product || quantity <= 0) return; await onCreate({ category: selectedCategory, product, quantity, unit: form.unit, supplier: form.supplier.trim() || 'Sem fornecedor', invoice: form.invoice.trim(), notes: form.notes.trim(), }); setForm({ product: '', quantity: '', unit: 'kg', supplier: '', invoice: '', notes: '' }); setActiveView('pending'); }; return (
{[ { id: 'new', label: 'Novo recebimento' }, { id: 'pending', label: `Pendentes (${pendingReceipts.length})` }, { id: 'history', label: `Histórico (${receipts.length})` }, ].map(item => ( ))}
{activeView === 'new' && (

Registrar recebimento

Preencha o que chegou. O financeiro vincula OC/NF e aprova o lançamento.

1. Qual categoria de produto chegou?

{receiptCategories.map(category => ( ))}

2. Detalhes do recebimento