From bc74a1089bac2aa6cc1475b107fb8acf4c597be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 21 Jul 2026 10:46:24 -0300 Subject: [PATCH] Show Tiny stock in inventory control --- src/pages/Supplies.tsx | 170 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 6 deletions(-) diff --git a/src/pages/Supplies.tsx b/src/pages/Supplies.tsx index 867e04a..8e13575 100644 --- a/src/pages/Supplies.tsx +++ b/src/pages/Supplies.tsx @@ -28,13 +28,13 @@ import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import ProductTypeBadge from '../components/ProductTypeBadge'; import { classifyCutFamily } from '../analytics/cutting'; -import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchCuttingSettings, fetchProductAnalytics, fetchSupplySummary } from '../dataService'; +import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchCuttingSettings, fetchProductAnalytics, fetchStock, fetchSupplySummary } from '../dataService'; import { parseProductName } from '../productParsing'; import { resolveProductType, type ProductTypeKey } from '../productClassification'; import { getPlanningStock } from '../planningStock'; -import type { CuttingSettings, DateRange, ProductAnalyticsItem, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types'; +import type { CuttingSettings, DateRange, ProductAnalyticsItem, StockData, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types'; -type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements'; +type InventoryTab = 'dashboard' | 'tiny_stock' | 'balance' | 'receipts' | 'inventory' | 'movements'; type ReceiptView = 'new' | 'pending' | 'history'; const pageClassName = 'flex w-full flex-col gap-6'; @@ -100,6 +100,7 @@ const exportCsv = (filename: string, rows: Array = [ { id: 'dashboard', name: 'Dashboard', icon: Warehouse }, + { id: 'tiny_stock', name: 'Estoque Tiny', icon: PackageSearch }, { id: 'balance', name: 'Saldo', icon: BarChart3 }, { id: 'receipts', name: 'Recebimentos', icon: Truck }, { id: 'inventory', name: 'Inventário', icon: ClipboardCheck }, @@ -958,6 +959,140 @@ const ReceiptsTab = ({ ); }; +const TinyStockTab = ({ + stock, + onRefresh, +}: { + stock: StockData[]; + onRefresh: () => void; +}) => { + const [search, setSearch] = useState(''); + const [stockFilter, setStockFilter] = useState('all'); + const normalizedSearch = normalizeSearch(search); + const totalBalance = stock.reduce((total, item) => total + Number(item.saldo || 0), 0); + const updatedItems = stock.filter(item => Number(item.delta_estoque || 0) !== 0).length; + const latestUpdate = stock + .map(item => item.updated_at ? new Date(item.updated_at).getTime() : 0) + .filter(Boolean) + .sort((a, b) => b - a)[0] || null; + + const visibleStock = stock.filter(item => { + const balance = Number(item.saldo || 0); + const delta = Number(item.delta_estoque || 0); + const matchesSearch = !normalizedSearch || normalizeSearch(`${item.produto_id} ${item.nome}`).includes(normalizedSearch); + const matchesFilter = + stockFilter === 'all' || + (stockFilter === 'positive' && balance > 0) || + (stockFilter === 'empty' && balance <= 0) || + (stockFilter === 'changed' && delta !== 0); + return matchesSearch && matchesFilter; + }).sort((a, b) => Number(b.saldo || 0) - Number(a.saldo || 0)); + + return ( +
+
+
+

SKUs Tiny

+

{formatNumber(stock.length, 0)}

+
+
+

Saldo total

+

{formatNumber(totalBalance, 0)} un.

+
+
+

Com delta

+

{formatNumber(updatedItems, 0)}

+
+
+

Última atualização

+

{latestUpdate ? formatDateTime(new Date(latestUpdate).toISOString()) : '-'}

+
+
+ +
+
+
+

Estoque vindo do Tiny

+

Saldo de produtos sincronizado pelo fluxo Tiny para Graphs.

+
+
+ + +
+
+ +
+ + +
+ + {visibleStock.length ? ( +
+
+ SKU Tiny + Produto + Saldo + Delta + Atualizado +
+
+ {visibleStock.map(item => { + const delta = Number(item.delta_estoque || 0); + return ( +
+

#{item.produto_id}

+

{item.nome}

+

{formatNumber(Number(item.saldo || 0), 0)} un.

+

0 ? 'text-emerald-300' : delta < 0 ? 'text-red-300' : 'text-dark-muted'}`}> + {delta > 0 ? '+' : ''}{formatNumber(delta, 0)} +

+

{formatDateTime(item.updated_at || null)}

+
+ ); + })} +
+
+ ) : ( +
+ +

{stock.length ? 'Nenhum SKU encontrado' : 'Nenhum estoque Tiny sincronizado'}

+

+ {stock.length ? 'Ajuste a busca ou o filtro.' : 'Quando o fluxo Tiny postar em /api/stock, os saldos aparecem aqui.'} +

+
+ )} +
+
+ ); +}; + const InventoryCountTab = ({ lots, onAdjust, @@ -1233,24 +1368,46 @@ const MovementsTab = ({ const InventoryScreen = () => { const [activeTab, setActiveTab] = useState('dashboard'); const [summary, setSummary] = useState(emptySupplySummary); + const [tinyStock, setTinyStock] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isBusy, setIsBusy] = useState(false); const [errorMessage, setErrorMessage] = useState(''); + const loadInventoryData = async () => { + setErrorMessage(''); + const [nextSummary, nextTinyStock] = await Promise.all([ + fetchSupplySummary(), + fetchStock(), + ]); + setSummary(nextSummary); + setTinyStock(nextTinyStock); + }; + const loadSummary = async () => { setErrorMessage(''); const nextSummary = await fetchSupplySummary(); setSummary(nextSummary); }; + const loadTinyStock = async () => { + setErrorMessage(''); + setTinyStock(await fetchStock()); + }; + useEffect(() => { let isMounted = true; const load = async () => { setIsLoading(true); try { - const nextSummary = await fetchSupplySummary(); - if (isMounted) setSummary(nextSummary); + const [nextSummary, nextTinyStock] = await Promise.all([ + fetchSupplySummary(), + fetchStock(), + ]); + if (isMounted) { + setSummary(nextSummary); + setTinyStock(nextTinyStock); + } } finally { if (isMounted) setIsLoading(false); } @@ -1268,7 +1425,7 @@ const InventoryScreen = () => { setErrorMessage(''); try { await action(); - await loadSummary(); + await loadInventoryData(); } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Não foi possível atualizar suprimentos.'); } finally { @@ -1304,6 +1461,7 @@ const InventoryScreen = () => { ) : ( <> {activeTab === 'dashboard' && setActiveTab('receipts')} />} + {activeTab === 'tiny_stock' && } {activeTab === 'balance' && } {activeTab === 'receipts' && (