Link supply blockers to actionable queues
This commit is contained in:
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user