From 59fe9104d6d51f0c413d547395b6f11896259332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Mon, 3 Aug 2026 13:16:46 -0300 Subject: [PATCH] Redesign purchase needs workflow --- src/pages/Supplies.tsx | 268 +++++++++++++++++++++++++++++++---------- 1 file changed, 202 insertions(+), 66 deletions(-) diff --git a/src/pages/Supplies.tsx b/src/pages/Supplies.tsx index a375060..287967b 100644 --- a/src/pages/Supplies.tsx +++ b/src/pages/Supplies.tsx @@ -1840,13 +1840,8 @@ const getNeedPendingLabel = (need: SupplyPurchaseNeed) => { const hasNegativeMaterialBalance = (need: SupplyPurchaseNeed) => need.stockKg < 0; -const getNeedStatusLabel = (need: SupplyPurchaseNeed) => { - if (hasNegativeMaterialBalance(need)) return 'Saldo negativo'; - if (need.missingReference) return 'Sem referência'; - return purchaseStatusLabels[need.status]; -}; - type PurchaseNeedTab = 'materials' | 'services' | 'mapping'; +type MaterialPurchaseFilter = 'to_buy' | 'negative_balance' | 'no_stock' | 'all'; const sumNeedProducts = ( need: SupplyPurchaseNeed, @@ -1859,11 +1854,107 @@ const countImpactedSkus = (needs: SupplyPurchaseNeed[]) => ( new Set(needs.flatMap(need => (need.products || []).map(product => product.productId).filter(Boolean))).size ); +const PurchaseMaterialQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => ( + <> +
+
+ Material e impacto + Como a compra foi calculada + Decisão +
+
+
+ {needs.map(need => { + const isNegative = hasNegativeMaterialBalance(need); + const isNoStock = need.stockKg === 0; + const isCovered = need.purchaseKg <= 0; + const statusTone = isNegative + ? 'border-red-400/35 bg-red-400/10 text-red-300' + : isNoStock + ? 'border-red-400/30 bg-red-400/10 text-red-300' + : isCovered + ? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300' + : 'border-amber-400/30 bg-amber-400/10 text-amber-300'; + const statusLabel = isNegative + ? 'Saldo negativo' + : isNoStock + ? 'Sem saldo' + : isCovered + ? 'Coberto' + : 'Comprar'; + const accent = isNegative || isNoStock ? 'border-l-red-400' : isCovered ? 'border-l-emerald-400' : 'border-l-amber-400'; + const products = need.products || []; + + return ( +
+
+
+
+

{getNeedDisplayName(need)}

+ {statusLabel} +
+

+ {need.colors.length ? need.colors.join(', ') : 'Todas as cores'} + + {need.suppliers.length ? need.suppliers.join(', ') : 'Fornecedor não informado'} +

+
+ +
+
+

Necessário

+

{formatNeedQuantity(need, need.plannedKg)}

+
+
+

Estoque

+

{formatNeedQuantity(need, need.stockKg)}

+
+
+

A receber

+

{formatNeedQuantity(need, need.pendingKg)}

+
+
+

Comprar

+

{formatNeedQuantity(need, need.purchaseKg)}

+
+
+ +
+

Impacto

+

{products.length} {products.length === 1 ? 'SKU' : 'SKUs'} dependem

+

{formatNumber(sumNeedProducts(need, 'suggestedQuantity'))} un. planejadas

+
+
+ + {products.length > 0 && ( +
+ Produtos afetados + {products.slice(0, 3).map(product => ( + + #{product.productId} · {product.name} + + ))} + {products.length > 3 && +{products.length - 3} SKUs} +
+ )} +
+ ); + })} +
+ +); + const PurchaseNeedsScreen = () => { const [summary, setSummary] = useState(emptySupplySummary); const [isLoading, setIsLoading] = useState(true); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState('materials'); + const [materialFilter, setMaterialFilter] = useState('to_buy'); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); @@ -1895,8 +1986,17 @@ const PurchaseNeedsScreen = () => { const materialNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need)); const serviceNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && isServicePurchaseNeed(need)); const mappingNeeds = summary.purchaseNeeds.filter(need => need.missingReference); + const purchaseNeeds = materialNeeds.filter(need => need.purchaseKg > 0); + const negativeBalanceNeeds = materialNeeds.filter(hasNegativeMaterialBalance); + const noStockNeeds = materialNeeds.filter(need => need.stockKg === 0 && need.purchaseKg > 0); + const filteredMaterialNeeds = materialNeeds.filter(need => { + if (materialFilter === 'to_buy') return need.purchaseKg > 0; + if (materialFilter === 'negative_balance') return hasNegativeMaterialBalance(need); + if (materialFilter === 'no_stock') return need.stockKg === 0 && need.purchaseKg > 0; + return true; + }); const activeNeeds = activeTab === 'materials' - ? [...materialNeeds].sort((a, b) => { + ? [...filteredMaterialNeeds].sort((a, b) => { if (hasNegativeMaterialBalance(a) !== hasNegativeMaterialBalance(b)) return hasNegativeMaterialBalance(a) ? -1 : 1; if (a.status !== b.status) return a.status === 'critical' ? -1 : b.status === 'critical' ? 1 : 0; return b.purchaseKg - a.purchaseKg; @@ -1910,34 +2010,61 @@ const PurchaseNeedsScreen = () => { const safeCurrentPage = Math.min(currentPage, totalPages || 1); const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedNeeds = visibleNeeds.slice(startIndex, startIndex + itemsPerPage); - const purchaseItemCount = materialNeeds.filter(need => need.purchaseKg > 0).length; - const suggestedPurchaseSummary = summarizePurchaseNeeds(materialNeeds); + const purchaseItemCount = purchaseNeeds.length; + const suggestedPurchaseSummary = summarizePurchaseNeeds(purchaseNeeds); const impactedSkuCount = countImpactedSkus(mappingNeeds); const pageTitle = 'Necessidade de Compra'; const pageSubtitle = 'Materiais, serviços terceirizados e pendências de cadastro em filas separadas.'; - const panelTitle = activeTab === 'materials' ? 'Compra de materiais' : activeTab === 'services' ? 'Serviços terceirizados' : 'Pendências de composição'; + const materialFilters: Array<{ id: MaterialPurchaseFilter; label: string; description: string; count: number }> = [ + { id: 'to_buy', label: 'Comprar agora', description: 'Materiais com saldo insuficiente para a demanda.', count: purchaseItemCount }, + { id: 'negative_balance', label: 'Saldo negativo', description: 'Divergências de estoque que precisam de conferência.', count: negativeBalanceNeeds.length }, + { id: 'no_stock', label: 'Sem saldo', description: 'Materiais sem saldo disponível no Tiny.', count: noStockNeeds.length }, + { id: 'all', label: 'Todos os materiais', description: 'Inclui itens cobertos para auditoria da conta.', count: materialNeeds.length }, + ]; + const activeMaterialFilter = materialFilters.find(filter => filter.id === materialFilter); + const panelTitle = activeTab === 'materials' ? activeMaterialFilter?.label || 'Compra de materiais' : activeTab === 'services' ? 'Serviços terceirizados' : 'Pendências de composição'; const panelSubtitle = activeTab === 'materials' - ? 'Demanda de produto → material → saldo disponível → quantidade para comprar.' + ? activeMaterialFilter?.description || 'Demanda de produto → material → saldo disponível → quantidade para comprar.' : activeTab === 'services' ? 'Necessidades de beneficiamento e serviços. Não entram no total de compra de materiais.' : 'SKUs sem consumo confiável. Corrija o cadastro antes de usar estes valores para comprar.'; - const stats = [ - { label: 'Itens para comprar', value: `${purchaseItemCount} materiais` }, - { label: 'Compra sugerida', value: suggestedPurchaseSummary }, - { label: 'Serviços a revisar', value: `${serviceNeeds.filter(need => need.purchaseKg > 0).length}` }, - { label: 'SKUs sem mapeamento', value: `${impactedSkuCount || mappingNeeds.length}` }, - ]; return (
- {stats.map(stat => ( -
-

{stat.label}

-

{stat.value}

-
- ))} + +
+

Compra sugerida

+

{suggestedPurchaseSummary}

+

necessidade após saldo e recebimentos

+
+ +
@@ -1969,29 +2096,46 @@ const PurchaseNeedsScreen = () => {
- -
- {[ - { id: 'materials' as const, label: `Materiais (${materialNeeds.length} linhas)` }, - { id: 'services' as const, label: `Serviços (${serviceNeeds.length})` }, - { id: 'mapping' as const, label: `Mapear dados (${impactedSkuCount || mappingNeeds.length} SKUs)` }, - ].map(tab => ( - - ))} +
+
+ {[ + { id: 'materials' as const, label: `Compra (${purchaseItemCount})` }, + { id: 'services' as const, label: `Serviços (${serviceNeeds.length})` }, + { id: 'mapping' as const, label: `Mapear dados (${mappingNeeds.length})` }, + ].map(tab => ( + + ))} +
+
+ {activeTab === 'materials' && ( +
+ {materialFilters.map(filter => ( + + ))} +
+ )} {isLoading ? (
@@ -2000,7 +2144,9 @@ const PurchaseNeedsScreen = () => { ) : visibleNeeds.length ? (
- {activeTab === 'mapping' ? ( + {activeTab === 'materials' ? ( + + ) : activeTab === 'mapping' ? ( @@ -2079,11 +2225,11 @@ const PurchaseNeedsScreen = () => { - - - - - + + + + + @@ -2127,24 +2273,14 @@ const PurchaseNeedsScreen = () => { - - + + @@ -2160,7 +2296,7 @@ const PurchaseNeedsScreen = () => { totalPages={totalPages} pageSize={itemsPerPage} pageSizeOptions={[10, 20, 50, 100]} - itemLabel="materiais" + itemLabel={activeTab === 'materials' ? 'materiais' : activeTab === 'services' ? 'serviços' : 'pendências'} pageSizeLabel="itens por página" startIndex={startIndex} endIndex={Math.min(startIndex + itemsPerPage, visibleNeeds.length)}
Demanda{activeTab === 'services' ? 'Serviço' : 'Material'}{activeTab === 'services' ? 'Volume previsto' : 'Necessário'}{activeTab === 'services' ? 'Agendado' : 'Estoque atual'}{activeTab === 'services' ? 'Em aberto' : 'Pendente'}{activeTab === 'services' ? 'Contratar' : 'Comprar'}ServiçoVolume previstoAgendadoEm abertoContratar Situação
{formatNeedQuantity(need, need.plannedKg)}{activeTab === 'services' ? '—' : formatNeedQuantity(need, need.stockKg)}{activeTab === 'services' ? '—' : formatNeedQuantity(need, need.pendingKg)} 0 ? 'text-amber-300' : 'text-emerald-300'}`}>{formatNeedQuantity(need, need.purchaseKg)} - {activeTab === 'services' ? 'Programar serviço' : getNeedStatusLabel(need)} + Programar serviço