Clarify synchronized and local inventory

This commit is contained in:
Cauê Faleiros
2026-08-04 12:57:22 -03:00
parent 8e8994099e
commit 58caee64eb

View File

@@ -831,7 +831,17 @@ const StatGrid = ({ summary }: { summary: SupplySummary }) => {
);
};
const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary; onNewReceipt: () => void }) => {
const InventoryDashboard = ({
summary,
stock,
onNewReceipt,
onOpenStock,
}: {
summary: SupplySummary;
stock: StockData[];
onNewReceipt: () => void;
onOpenStock: () => void;
}) => {
const categories = summary.lots.reduce<Record<string, { quantity: number; unit: string; lots: number }>>((acc, lot) => {
const current = acc[lot.category] || { quantity: 0, unit: lot.unit, lots: 0 };
acc[lot.category] = {
@@ -842,12 +852,46 @@ const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary;
return acc;
}, {});
const latestReceipts = summary.receipts.slice(0, 4);
const negativeStockCount = stock.filter(item => Number(item.saldo || 0) < 0).length;
const zeroStockCount = stock.filter(item => Number(item.saldo || 0) === 0).length;
const latestStockUpdate = stock
.map(item => item.updated_at ? new Date(item.updated_at).getTime() : 0)
.filter(Boolean)
.sort((a, b) => b - a)[0] || null;
return (
<div className="space-y-4">
<StatGrid summary={summary} />
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<button type="button" onClick={onOpenStock} className="rounded-2xl border border-dark-border bg-dark-card p-5 text-left shadow-sm transition-colors hover:border-brand-primary/35 cursor-pointer">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Estoque sincronizado</p>
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(stock.length, 0)}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">SKUs recebidos do Tiny</p>
</button>
<button type="button" onClick={onOpenStock} className={`rounded-2xl border p-5 text-left shadow-sm transition-colors cursor-pointer ${negativeStockCount ? 'border-red-400/35 bg-red-400/[0.055] hover:border-red-400/60' : 'border-dark-border bg-dark-card hover:border-brand-primary/35'}`}>
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldos a conferir</p>
<p className={`mt-2 text-3xl font-bold ${negativeStockCount ? 'text-red-300' : 'text-dark-text'}`}>{formatNumber(negativeStockCount, 0)}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">SKUs com saldo negativo</p>
</button>
<button type="button" onClick={onNewReceipt} className="rounded-2xl border border-dark-border bg-dark-card p-5 text-left shadow-sm transition-colors hover:border-brand-primary/35 cursor-pointer">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Recebimentos pendentes</p>
<p className="mt-2 text-3xl font-bold text-amber-300">{formatNumber(summary.stats.pendingReceipts, 0)}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">Entradas aguardando aprovação</p>
</button>
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Lotes locais</p>
<p className="mt-2 text-3xl font-bold text-brand-primary">{formatNumber(summary.stats.activeLots, 0)}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">Controle físico registrado aqui</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1.15fr)_minmax(340px,.85fr)]">
<div className={`${panelClassName} p-5`}>
<h2 className="text-base font-bold text-dark-text">Por categoria de material</h2>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 className="text-base font-bold text-dark-text">Estoque físico por categoria</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Lotes aprovados por recebimento ou inventário local.</p>
</div>
<span className="w-fit rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-muted">{formatNumber(summary.stats.rolls, 0)} rolos</span>
</div>
{Object.keys(categories).length ? (
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
{Object.entries(categories).map(([category, data]) => (
@@ -861,14 +905,25 @@ const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary;
) : (
<div className={emptyStateClassName}>
<Warehouse className="h-8 w-8 text-brand-primary" />
<h3 className="text-base font-bold text-dark-text">Nenhuma categoria com saldo</h3>
<p className="text-sm font-semibold text-dark-muted">Registre entradas para agrupar o estoque por tipo de material.</p>
<h3 className="text-base font-bold text-dark-text">Nenhum lote físico registrado</h3>
<p className="text-sm font-semibold text-dark-muted">O saldo do Tiny aparece na aba Estoque. Registre recebimentos para controlar lotes aqui.</p>
</div>
)}
</div>
<div className={`${panelClassName} p-5`}>
<h2 className="flex items-center gap-2 text-base font-bold text-dark-text"><AlertTriangle className="h-4 w-4 text-yellow-400" /> Alertas de estoque</h2>
<p className="mt-4 text-sm font-semibold text-dark-muted">Nenhum alerta no momento.</p>
<h2 className="flex items-center gap-2 text-base font-bold text-dark-text"><AlertTriangle className="h-4 w-4 text-yellow-400" /> Conferir antes de planejar</h2>
<div className="mt-4 space-y-3">
<button type="button" onClick={onOpenStock} className="flex w-full items-center justify-between gap-3 rounded-xl border border-red-400/25 bg-red-400/[0.045] px-3.5 py-3 text-left transition-colors hover:border-red-400/55 cursor-pointer">
<span><span className="block text-sm font-bold text-dark-text">Saldos negativos</span><span className="block text-xs font-semibold text-dark-muted">Confirmar ajuste ou entrada no Tiny</span></span>
<span className="text-lg font-bold text-red-300">{formatNumber(negativeStockCount, 0)}</span>
</button>
<button type="button" onClick={onOpenStock} className="flex w-full items-center justify-between gap-3 rounded-xl border border-dark-border bg-dark-input/45 px-3.5 py-3 text-left transition-colors hover:border-brand-primary/45 cursor-pointer">
<span><span className="block text-sm font-bold text-dark-text">Sem saldo no Tiny</span><span className="block text-xs font-semibold text-dark-muted">SKUs sincronizados com zero</span></span>
<span className="text-lg font-bold text-dark-text">{formatNumber(zeroStockCount, 0)}</span>
</button>
<p className="pt-1 text-xs font-semibold text-dark-muted">Última sincronização: {latestStockUpdate ? formatDateTime(new Date(latestStockUpdate).toISOString()) : 'sem atualização registrada'}.</p>
</div>
</div>
</div>
<div className={`${panelClassName} p-5`}>
<div className="flex items-center justify-between gap-3">
@@ -1732,7 +1787,7 @@ const InventoryScreen = () => {
<div className={`${panelClassName} p-5 text-sm font-bold text-dark-muted`}>Carregando estoque...</div>
) : (
<>
{activeTab === 'dashboard' && <InventoryDashboard summary={summary} onNewReceipt={() => setActiveTab('receipts')} />}
{activeTab === 'dashboard' && <InventoryDashboard summary={summary} stock={stockSnapshot} onNewReceipt={() => setActiveTab('receipts')} onOpenStock={() => setActiveTab('stock')} />}
{activeTab === 'stock' && <StockTab stock={stockSnapshot} onRefresh={loadStockSnapshot} />}
{activeTab === 'balance' && <BalanceTab summary={summary} onRefresh={loadSummary} />}
{activeTab === 'receipts' && (
@@ -2244,10 +2299,10 @@ const PurchaseNeedsScreen = () => {
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [isLoading, setIsLoading] = useState(true);
const [search, setSearch] = useState('');
const initialTab = searchParams.get('tab');
const initialFilter = searchParams.get('filter');
const [activeTab, setActiveTab] = useState<PurchaseNeedTab>(initialTab === 'services' || initialTab === 'mapping' ? initialTab : 'materials');
const [materialFilter, setMaterialFilter] = useState<MaterialPurchaseFilter>(initialFilter === 'negative_balance' || initialFilter === 'no_stock' || initialFilter === 'all' ? initialFilter : 'to_buy');
const requestedTab = searchParams.get('tab');
const requestedFilter = searchParams.get('filter');
const activeTab: PurchaseNeedTab = requestedTab === 'services' || requestedTab === 'mapping' ? requestedTab : 'materials';
const materialFilter: MaterialPurchaseFilter = requestedFilter === 'negative_balance' || requestedFilter === 'no_stock' || requestedFilter === 'all' ? requestedFilter : 'to_buy';
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
@@ -2276,18 +2331,7 @@ const PurchaseNeedsScreen = () => {
};
}, []);
useEffect(() => {
const tab = searchParams.get('tab');
const filter = searchParams.get('filter');
setActiveTab(tab === 'services' || tab === 'mapping' ? tab : 'materials');
setMaterialFilter(filter === 'negative_balance' || filter === 'no_stock' || filter === 'all' ? filter : 'to_buy');
setSearch('');
setCurrentPage(1);
}, [searchParams]);
const openQueue = (tab: PurchaseNeedTab, filter: MaterialPurchaseFilter = 'to_buy') => {
setActiveTab(tab);
setMaterialFilter(filter);
setSearch('');
setCurrentPage(1);
setSearchParams(tab === 'materials' ? { tab, filter } : { tab });