Link supply blockers to actionable queues

This commit is contained in:
Cauê Faleiros
2026-08-04 12:51:17 -03:00
parent 22dd4ca376
commit 43632c9aa5

View File

@@ -1,5 +1,5 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'; import { type FormEvent, useEffect, useMemo, useState } from 'react';
import { Link as RouterLink, Navigate, useOutletContext, useParams } from 'react-router-dom'; import { Link as RouterLink, Navigate, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
import { import {
AlertTriangle, AlertTriangle,
ArrowLeft, ArrowLeft,
@@ -313,7 +313,7 @@ const SuppliesHub = () => {
description: 'Estoque negativo distorce a compra sugerida e pode liberar corte sem material físico.', description: 'Estoque negativo distorce a compra sugerida e pode liberar corte sem material físico.',
meta: 'Abrir compra de materiais', meta: 'Abrir compra de materiais',
icon: AlertTriangle, icon: AlertTriangle,
to: '/supplies/purchase-needs', to: '/supplies/purchase-needs?tab=materials&filter=negative_balance',
tone: 'critical' as const, tone: 'critical' as const,
} }
: mappingSkuCount > 0 : mappingSkuCount > 0
@@ -323,7 +323,7 @@ const SuppliesHub = () => {
description: 'Sem composição confiável, a necessidade de compra e o corte não representam a demanda real.', description: 'Sem composição confiável, a necessidade de compra e o corte não representam a demanda real.',
meta: 'Abrir pendências de composição', meta: 'Abrir pendências de composição',
icon: LinkIcon, icon: LinkIcon,
to: '/supplies/purchase-needs', to: '/supplies/purchase-needs?tab=mapping',
tone: 'info' as const, tone: 'info' as const,
} }
: materialPurchaseNeeds.length > 0 : materialPurchaseNeeds.length > 0
@@ -333,7 +333,7 @@ const SuppliesHub = () => {
description: 'A necessidade já desconta saldo e recebimentos para apoiar a decisão de compra.', description: 'A necessidade já desconta saldo e recebimentos para apoiar a decisão de compra.',
meta: 'Ver lista de compra', meta: 'Ver lista de compra',
icon: BarChart3, icon: BarChart3,
to: '/supplies/purchase-needs', to: '/supplies/purchase-needs?tab=materials&filter=to_buy',
tone: 'attention' as const, tone: 'attention' as const,
} }
: activeProductionCount > 0 : activeProductionCount > 0
@@ -413,9 +413,9 @@ const SuppliesHub = () => {
<p className="mt-1 text-sm font-semibold text-dark-muted">Resolva estes pontos antes de confiar no plano.</p> <p className="mt-1 text-sm font-semibold text-dark-muted">Resolva estes pontos antes de confiar no plano.</p>
</div> </div>
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<FlowBlocker title="Saldos a conferir" description="Materiais com estoque negativo" count={negativeMaterialNeeds.length} to="/supplies/purchase-needs" tone="critical" /> <FlowBlocker title="Saldos a conferir" description="Materiais com estoque negativo" count={negativeMaterialNeeds.length} to="/supplies/purchase-needs?tab=materials&filter=negative_balance" tone="critical" />
<FlowBlocker title="Dados para mapear" description="SKUs sem composição confiável" count={mappingSkuCount} to="/supplies/purchase-needs" tone="info" /> <FlowBlocker title="Dados para mapear" description="SKUs sem composição confiável" count={mappingSkuCount} to="/supplies/purchase-needs?tab=mapping" tone="info" />
<FlowBlocker title="Materiais sem saldo" description="Itens necessários sem disponibilidade" count={noStockMaterialNeeds.length} to="/supplies/purchase-needs" tone="attention" /> <FlowBlocker title="Materiais sem saldo" description="Itens necessários sem disponibilidade" count={noStockMaterialNeeds.length} to="/supplies/purchase-needs?tab=materials&filter=no_stock" tone="attention" />
</div> </div>
</div> </div>
</section> </section>
@@ -425,8 +425,8 @@ const SuppliesHub = () => {
<p className="mt-1 text-sm font-semibold text-dark-muted">Da necessidade de material até a produção acompanhada.</p> <p className="mt-1 text-sm font-semibold text-dark-muted">Da necessidade de material até a produção acompanhada.</p>
</div> </div>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3"> <div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
<FlowStepCard number="01" title="Garantir materiais" description="Confira saldos, pendências de composição e a compra sugerida." icon={BarChart3} to="/supplies/purchase-needs" tone={negativeMaterialNeeds.length > 0 ? 'critical' : materialPurchaseNeeds.length > 0 ? 'attention' : 'ready'} meta={negativeMaterialNeeds.length > 0 ? `${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos` : materialPurchaseNeeds.length > 0 ? `${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar` : 'Materiais cobertos'} /> <FlowStepCard number="01" title="Garantir materiais" description="Confira saldos, pendências de composição e a compra sugerida." icon={BarChart3} to={negativeMaterialNeeds.length > 0 ? '/supplies/purchase-needs?tab=materials&filter=negative_balance' : materialPurchaseNeeds.length > 0 ? '/supplies/purchase-needs?tab=materials&filter=to_buy' : mappingSkuCount > 0 ? '/supplies/purchase-needs?tab=mapping' : '/supplies/purchase-needs?tab=materials&filter=all'} tone={negativeMaterialNeeds.length > 0 ? 'critical' : materialPurchaseNeeds.length > 0 ? 'attention' : mappingSkuCount > 0 ? 'attention' : 'ready'} meta={negativeMaterialNeeds.length > 0 ? `${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos` : materialPurchaseNeeds.length > 0 ? `${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar` : mappingSkuCount > 0 ? 'Sem falta calculada · dados pendentes' : 'Materiais cobertos'} />
<FlowStepCard number="02" title="Definir corte" description="Priorize SKUs, cores e tamanhos com necessidade e material disponível." icon={Scissors} to="/cutting" tone={mappingSkuCount > 0 ? 'attention' : 'default'} meta={mappingSkuCount > 0 ? `${formatNumber(mappingSkuCount, 0)} SKUs precisam de mapeamento` : 'Abrir plano de corte'} /> <FlowStepCard number="02" title="Definir corte" description="Priorize SKUs, cores e tamanhos com necessidade e material disponível." icon={Scissors} to={mappingSkuCount > 0 ? '/supplies/purchase-needs?tab=mapping' : '/cutting'} tone={mappingSkuCount > 0 ? 'attention' : 'default'} meta={mappingSkuCount > 0 ? `${formatNumber(mappingSkuCount, 0)} SKUs precisam de mapeamento` : 'Abrir plano de corte'} />
<FlowStepCard number="03" title="Acompanhar produção" description="Gerencie as OPs criadas a partir do corte até sua finalização." icon={ClipboardList} to="/production-orders" tone={activeProductionCount > 0 ? 'ready' : 'default'} meta={activeProductionCount > 0 ? `${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento` : 'Nenhuma OP em execução'} /> <FlowStepCard number="03" title="Acompanhar produção" description="Gerencie as OPs criadas a partir do corte até sua finalização." icon={ClipboardList} to="/production-orders" tone={activeProductionCount > 0 ? 'ready' : 'default'} meta={activeProductionCount > 0 ? `${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento` : 'Nenhuma OP em execução'} />
</div> </div>
</section> </section>
@@ -2240,11 +2240,14 @@ const PurchaseMappingQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
); );
const PurchaseNeedsScreen = () => { const PurchaseNeedsScreen = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary); const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [activeTab, setActiveTab] = useState<PurchaseNeedTab>('materials'); const initialTab = searchParams.get('tab');
const [materialFilter, setMaterialFilter] = useState<MaterialPurchaseFilter>('to_buy'); 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 [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10); const [itemsPerPage, setItemsPerPage] = useState(10);
@@ -2273,6 +2276,23 @@ 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 });
};
const materialNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need)); const materialNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need));
const serviceNeeds = 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 mappingNeeds = summary.purchaseNeeds.filter(need => need.missingReference);
@@ -2325,7 +2345,7 @@ const PurchaseNeedsScreen = () => {
<div className="grid grid-cols-1 gap-4 md:grid-cols-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<button <button
type="button" type="button"
onClick={() => { setActiveTab('materials'); setMaterialFilter('to_buy'); setSearch(''); setCurrentPage(1); }} onClick={() => openQueue('materials', 'to_buy')}
className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'materials' && materialFilter === 'to_buy' ? 'border-brand-primary/45 bg-brand-primary/[0.07]' : 'border-dark-border bg-dark-card hover:border-brand-primary/35'}`} className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'materials' && materialFilter === 'to_buy' ? 'border-brand-primary/45 bg-brand-primary/[0.07]' : 'border-dark-border bg-dark-card hover:border-brand-primary/35'}`}
> >
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Materiais para comprar</p> <p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Materiais para comprar</p>
@@ -2339,7 +2359,7 @@ const PurchaseNeedsScreen = () => {
</div> </div>
<button <button
type="button" type="button"
onClick={() => { setActiveTab('materials'); setMaterialFilter('negative_balance'); setSearch(''); setCurrentPage(1); }} onClick={() => openQueue('materials', 'negative_balance')}
className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'materials' && materialFilter === 'negative_balance' ? 'border-red-400/45 bg-red-400/[0.07]' : 'border-dark-border bg-dark-card hover:border-red-400/35'}`} className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'materials' && materialFilter === 'negative_balance' ? 'border-red-400/45 bg-red-400/[0.07]' : 'border-dark-border bg-dark-card hover:border-red-400/35'}`}
> >
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldos a conferir</p> <p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldos a conferir</p>
@@ -2348,7 +2368,7 @@ const PurchaseNeedsScreen = () => {
</button> </button>
<button <button
type="button" type="button"
onClick={() => { setActiveTab('mapping'); setSearch(''); setCurrentPage(1); }} onClick={() => openQueue('mapping')}
className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'mapping' ? 'border-sky-400/45 bg-sky-400/[0.07]' : 'border-dark-border bg-dark-card hover:border-sky-400/35'}`} className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'mapping' ? 'border-sky-400/45 bg-sky-400/[0.07]' : 'border-dark-border bg-dark-card hover:border-sky-400/35'}`}
> >
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Dados bloqueando o plano</p> <p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Dados bloqueando o plano</p>
@@ -2393,7 +2413,7 @@ const PurchaseNeedsScreen = () => {
{ id: 'services' as const, label: `Serviços (${serviceNeeds.length})` }, { id: 'services' as const, label: `Serviços (${serviceNeeds.length})` },
{ id: 'mapping' as const, label: `Mapear dados (${mappingNeeds.length})` }, { id: 'mapping' as const, label: `Mapear dados (${mappingNeeds.length})` },
].map(tab => ( ].map(tab => (
<button key={tab.id} type="button" onClick={() => { setActiveTab(tab.id); setSearch(''); setCurrentPage(1); }} className={`rounded-lg px-3 py-2 text-xs font-bold transition-colors cursor-pointer ${activeTab === tab.id ? 'bg-brand-primary text-brand-contrast' : 'text-dark-muted hover:text-dark-text'}`}> <button key={tab.id} type="button" onClick={() => openQueue(tab.id)} className={`rounded-lg px-3 py-2 text-xs font-bold transition-colors cursor-pointer ${activeTab === tab.id ? 'bg-brand-primary text-brand-contrast' : 'text-dark-muted hover:text-dark-text'}`}>
{tab.label} {tab.label}
</button> </button>
))} ))}
@@ -2417,7 +2437,7 @@ const PurchaseNeedsScreen = () => {
<button <button
key={filter.id} key={filter.id}
type="button" type="button"
onClick={() => { setMaterialFilter(filter.id); setCurrentPage(1); }} onClick={() => openQueue('materials', filter.id)}
className={`rounded-lg border px-3 py-2 text-left text-xs font-bold transition-colors ${materialFilter === filter.id ? 'border-brand-primary/40 bg-brand-primary/10 text-brand-primary' : 'border-dark-border bg-dark-input text-dark-muted hover:border-brand-primary/35 hover:text-dark-text'}`} className={`rounded-lg border px-3 py-2 text-left text-xs font-bold transition-colors ${materialFilter === filter.id ? 'border-brand-primary/40 bg-brand-primary/10 text-brand-primary' : 'border-dark-border bg-dark-input text-dark-muted hover:border-brand-primary/35 hover:text-dark-text'}`}
title={filter.description} title={filter.description}
> >