Centralize planning data exceptions
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m43s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m43s
This commit is contained in:
@@ -7,13 +7,23 @@ import ProductTypeBadge from '../components/ProductTypeBadge';
|
||||
import RefreshStatus from '../components/RefreshStatus';
|
||||
import SkuPlanningModal from '../components/SkuPlanningModal';
|
||||
import { classifyCutFamily, CUT_FAMILY_RULES, type CutFamilyKey } from '../analytics/cutting';
|
||||
import { fetchCuttingSettings, fetchProductAnalytics, saveCuttingSettings } from '../dataService';
|
||||
import { fetchCuttingSettings, fetchProductAnalytics, fetchStock, fetchSupplySummary, saveCuttingSettings } from '../dataService';
|
||||
import { buildConsumptionReferencePath } from '../catalogLinks';
|
||||
import { getProductTypeConfig, resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||
import { parseProductName } from '../productParsing';
|
||||
import type { CutProductOverride, CuttingSettings, DateRange, ProductAnalyticsItem } from '../types';
|
||||
import type { CutProductOverride, CuttingSettings, DateRange, ProductAnalyticsItem, StockData, SupplySummary } from '../types';
|
||||
|
||||
type IssueKey = 'review_type' | 'missing_cut_family' | 'missing_color' | 'missing_size' | 'missing_yield';
|
||||
type IssueFilter = 'all' | IssueKey;
|
||||
type ExceptionQueue = 'negative_stock' | 'missing_composition' | 'cutting_data';
|
||||
|
||||
type MissingCompositionRow = {
|
||||
productId: string;
|
||||
name: string;
|
||||
quantitySold: number;
|
||||
stockQuantity: number;
|
||||
suggestedQuantity: number;
|
||||
};
|
||||
|
||||
type PlanningIssueRow = ProductAnalyticsItem & {
|
||||
productType: ProductTypeKey;
|
||||
@@ -64,6 +74,22 @@ const formatCurrency = (value: number) => (
|
||||
new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value)
|
||||
);
|
||||
|
||||
const emptySupplySummary: SupplySummary = {
|
||||
receipts: [],
|
||||
lots: [],
|
||||
movements: [],
|
||||
fabricPlans: [],
|
||||
purchaseNeeds: [],
|
||||
stats: {
|
||||
totalQuantityKg: 0,
|
||||
activeLots: 0,
|
||||
rolls: 0,
|
||||
alerts: 0,
|
||||
pendingReceipts: 0,
|
||||
approvedReceipts: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const buildIssues = (
|
||||
product: ProductAnalyticsItem,
|
||||
settings: CuttingSettings
|
||||
@@ -122,7 +148,10 @@ const PlanningIssues = () => {
|
||||
}>();
|
||||
const [products, setProducts] = useState<ProductAnalyticsItem[]>([]);
|
||||
const [settings, setSettings] = useState<CuttingSettings>({ familyYields: {}, productOverrides: {} });
|
||||
const [supplySummary, setSupplySummary] = useState<SupplySummary>(emptySupplySummary);
|
||||
const [stockSnapshot, setStockSnapshot] = useState<StockData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeQueue, setActiveQueue] = useState<ExceptionQueue>('negative_stock');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [issueFilter, setIssueFilter] = useState<IssueFilter>('all');
|
||||
const [editingProduct, setEditingProduct] = useState<PlanningIssueRow | null>(null);
|
||||
@@ -135,14 +164,18 @@ const PlanningIssues = () => {
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
const [productData, planningSettings] = await Promise.all([
|
||||
const [productData, planningSettings, nextSupplySummary, nextStockSnapshot] = await Promise.all([
|
||||
fetchProductAnalytics(dateRange),
|
||||
fetchCuttingSettings()
|
||||
fetchCuttingSettings(),
|
||||
fetchSupplySummary(),
|
||||
fetchStock(),
|
||||
]);
|
||||
|
||||
if (isMounted) {
|
||||
setProducts(productData);
|
||||
setSettings(planningSettings);
|
||||
setSupplySummary(nextSupplySummary);
|
||||
setStockSnapshot(nextStockSnapshot);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -187,6 +220,52 @@ const PlanningIssues = () => {
|
||||
return counts;
|
||||
}, [products, settings]);
|
||||
|
||||
const negativeStockRows = useMemo(() => (
|
||||
stockSnapshot
|
||||
.filter(item => Number(item.saldo || 0) < 0)
|
||||
.sort((a, b) => Number(a.saldo) - Number(b.saldo))
|
||||
), [stockSnapshot]);
|
||||
|
||||
const missingCompositionRows = useMemo(() => {
|
||||
const byProductId = new Map<string, MissingCompositionRow>();
|
||||
|
||||
supplySummary.purchaseNeeds
|
||||
.filter(need => need.missingReference)
|
||||
.flatMap(need => need.products || [])
|
||||
.forEach(product => {
|
||||
if (!product.productId) return;
|
||||
const existing = byProductId.get(product.productId);
|
||||
if (existing) {
|
||||
existing.quantitySold = Math.max(existing.quantitySold, Number(product.quantitySold || 0));
|
||||
existing.stockQuantity = Math.max(existing.stockQuantity, Number(product.stockQuantity || 0));
|
||||
existing.suggestedQuantity = Math.max(existing.suggestedQuantity, Number(product.suggestedQuantity || 0));
|
||||
return;
|
||||
}
|
||||
byProductId.set(product.productId, {
|
||||
productId: product.productId,
|
||||
name: product.name,
|
||||
quantitySold: Number(product.quantitySold || 0),
|
||||
stockQuantity: Number(product.stockQuantity || 0),
|
||||
suggestedQuantity: Number(product.suggestedQuantity || 0),
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(byProductId.values())
|
||||
.sort((a, b) => b.suggestedQuantity - a.suggestedQuantity || b.quantitySold - a.quantitySold);
|
||||
}, [supplySummary.purchaseNeeds]);
|
||||
|
||||
const normalizedSearch = searchTerm.trim().toLocaleLowerCase('pt-BR');
|
||||
const visibleNegativeStockRows = useMemo(() => (
|
||||
negativeStockRows.filter(row => (
|
||||
!normalizedSearch || `${row.produto_id} ${row.nome}`.toLocaleLowerCase('pt-BR').includes(normalizedSearch)
|
||||
))
|
||||
), [negativeStockRows, normalizedSearch]);
|
||||
const visibleMissingCompositionRows = useMemo(() => (
|
||||
missingCompositionRows.filter(row => (
|
||||
!normalizedSearch || `${row.productId} ${row.name}`.toLocaleLowerCase('pt-BR').includes(normalizedSearch)
|
||||
))
|
||||
), [missingCompositionRows, normalizedSearch]);
|
||||
|
||||
const saveProductOverride = async (productId: string, override: CutProductOverride | null) => {
|
||||
const productOverrides = { ...settings.productOverrides };
|
||||
if (override) {
|
||||
@@ -206,10 +285,17 @@ const PlanningIssues = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(rows.length / itemsPerPage);
|
||||
const activeRowCount = activeQueue === 'negative_stock'
|
||||
? visibleNegativeStockRows.length
|
||||
: activeQueue === 'missing_composition'
|
||||
? visibleMissingCompositionRows.length
|
||||
: rows.length;
|
||||
const totalPages = Math.ceil(activeRowCount / itemsPerPage);
|
||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||
const paginatedRows = rows.slice(startIndex, startIndex + itemsPerPage);
|
||||
const paginatedNegativeStockRows = visibleNegativeStockRows.slice(startIndex, startIndex + itemsPerPage);
|
||||
const paginatedMissingCompositionRows = visibleMissingCompositionRows.slice(startIndex, startIndex + itemsPerPage);
|
||||
const isRefreshing = isLoading && products.length > 0;
|
||||
|
||||
return (
|
||||
@@ -218,7 +304,7 @@ const PlanningIssues = () => {
|
||||
<div>
|
||||
<h1 className="mb-2 text-2xl font-bold text-zinc-900 dark:text-dark-text">Dados Pendentes</h1>
|
||||
<p className="font-medium text-zinc-500 dark:text-dark-muted">
|
||||
Fila principal para revisar tipo, cor, tamanho, família e rendimento dos SKUs.
|
||||
Exceções de estoque, composição e cadastro. Elas ficam visíveis aqui, sem parar compra, corte ou produção.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center 2xl:justify-end">
|
||||
@@ -241,6 +327,65 @@ const PlanningIssues = () => {
|
||||
|
||||
<RefreshStatus isRefreshing={isRefreshing} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
id: 'negative_stock' as const,
|
||||
label: 'Saldos negativos',
|
||||
description: 'Estoque informado abaixo de zero',
|
||||
count: negativeStockRows.length,
|
||||
tone: 'critical',
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
{
|
||||
id: 'missing_composition' as const,
|
||||
label: 'SKUs sem composição',
|
||||
description: 'Fora do cálculo de materiais',
|
||||
count: missingCompositionRows.length,
|
||||
tone: 'info',
|
||||
icon: PackageSearch,
|
||||
},
|
||||
{
|
||||
id: 'cutting_data' as const,
|
||||
label: 'Cadastro para corte',
|
||||
description: 'Tipo, cor, tamanho, família e rendimento',
|
||||
count: Array.from(issueCounts.values()).reduce((total, count) => total + count, 0),
|
||||
tone: 'attention',
|
||||
icon: Pencil,
|
||||
},
|
||||
].map(queue => {
|
||||
const Icon = queue.icon;
|
||||
const active = activeQueue === queue.id;
|
||||
const tone = queue.tone === 'critical'
|
||||
? active ? 'border-red-400/45 bg-red-500/[0.07]' : 'border-dark-border bg-dark-card hover:border-red-400/35'
|
||||
: queue.tone === 'info'
|
||||
? active ? 'border-sky-400/45 bg-sky-500/[0.07]' : 'border-dark-border bg-dark-card hover:border-sky-400/35'
|
||||
: active ? 'border-amber-400/45 bg-amber-500/[0.07]' : 'border-dark-border bg-dark-card hover:border-amber-400/35';
|
||||
const iconTone = queue.tone === 'critical' ? 'text-red-300' : queue.tone === 'info' ? 'text-sky-300' : 'text-amber-300';
|
||||
return (
|
||||
<button
|
||||
key={queue.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveQueue(queue.id);
|
||||
setCurrentPage(1);
|
||||
setSearchTerm('');
|
||||
}}
|
||||
className={`rounded-2xl border p-5 text-left transition-colors cursor-pointer ${tone}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-dark-muted">{queue.label}</span>
|
||||
<Icon className={`h-4 w-4 ${iconTone}`} />
|
||||
</div>
|
||||
<div className="mt-3 text-3xl font-bold text-dark-text">{formatNumber(queue.count)}</div>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{queue.description}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{activeQueue === 'cutting_data' ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 xl:grid-cols-5">
|
||||
{issueFilters.filter(option => option.value !== 'all').map(option => {
|
||||
const count = issueCounts.get(option.value as IssueKey) || 0;
|
||||
@@ -422,6 +567,144 @@ const PlanningIssues = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3 rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm xl:grid-cols-[minmax(360px,1fr)_auto] xl:items-center">
|
||||
<div className="relative w-full md:w-96">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-dark-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(event) => {
|
||||
setSearchTerm(event.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
placeholder={activeQueue === 'negative_stock' ? 'Buscar material ou SKU...' : 'Buscar SKU ou produto...'}
|
||||
className="h-10 w-full rounded-xl border border-dark-border bg-dark-input pl-10 pr-3 text-sm font-semibold text-dark-text outline-none transition-colors placeholder:text-dark-muted focus:border-brand-primary"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-dark-muted">
|
||||
{formatNumber(activeRowCount)} {activeQueue === 'negative_stock' ? 'materiais para conferir' : 'SKUs fora do cálculo'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading && !products.length ? (
|
||||
<PlanningIssuesSkeleton />
|
||||
) : activeQueue === 'negative_stock' ? (
|
||||
visibleNegativeStockRows.length ? (
|
||||
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
||||
<div className="border-b border-dark-border px-5 py-4">
|
||||
<h2 className="text-base font-bold text-dark-text">Saldos negativos</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">São exibidos para conferência, mas contam como zero disponível no planejamento.</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[860px] text-left text-sm">
|
||||
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
||||
<tr>
|
||||
<th className="px-5 py-4 text-[10px] font-bold uppercase tracking-wider">SKU</th>
|
||||
<th className="px-5 py-4 text-[10px] font-bold uppercase tracking-wider">Material</th>
|
||||
<th className="px-5 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Saldo informado</th>
|
||||
<th className="px-5 py-4 text-[10px] font-bold uppercase tracking-wider">Tratamento no plano</th>
|
||||
<th className="px-5 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Ação</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dark-border">
|
||||
{paginatedNegativeStockRows.map(row => (
|
||||
<tr key={row.produto_id} className="transition-colors hover:bg-dark-input/50">
|
||||
<td className="px-5 py-3 font-mono text-xs font-semibold text-dark-muted">#{row.produto_id}</td>
|
||||
<td className="max-w-[420px] px-5 py-3"><p className="truncate font-bold text-dark-text" title={row.nome}>{row.nome}</p></td>
|
||||
<td className="px-5 py-3 text-right font-bold text-red-300">{formatNumber(Number(row.saldo), 2)} un.</td>
|
||||
<td className="px-5 py-3"><span className="inline-flex rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-muted">0 disponível</span></td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<Link to="/supplies/inventory?tab=stock" className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-dark-input px-2.5 text-xs font-bold text-dark-text transition-colors hover:bg-dark-border cursor-pointer">
|
||||
Conferir <Eye className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<PaginationControls
|
||||
totalItems={visibleNegativeStockRows.length}
|
||||
currentPage={safeCurrentPage}
|
||||
totalPages={totalPages}
|
||||
pageSize={itemsPerPage}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
itemLabel="materiais"
|
||||
pageSizeLabel="por página"
|
||||
startIndex={startIndex}
|
||||
endIndex={Math.min(startIndex + itemsPerPage, visibleNegativeStockRows.length)}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={(size) => { setItemsPerPage(size); setCurrentPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center rounded-2xl border border-dark-border bg-dark-card px-6 py-16 text-center shadow-sm">
|
||||
<CheckCircle2 className="h-10 w-10 text-emerald-300" />
|
||||
<p className="mt-4 text-sm font-bold text-dark-text">Nenhum saldo negativo neste filtro.</p>
|
||||
</div>
|
||||
)
|
||||
) : visibleMissingCompositionRows.length ? (
|
||||
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
||||
<div className="border-b border-dark-border px-5 py-4">
|
||||
<h2 className="text-base font-bold text-dark-text">SKUs sem composição</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Estes produtos continuam operando, mas não geram demanda de material até receberem composição.</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[940px] text-left text-sm">
|
||||
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
||||
<tr>
|
||||
<th className="px-5 py-4 text-[10px] font-bold uppercase tracking-wider">SKU</th>
|
||||
<th className="px-5 py-4 text-[10px] font-bold uppercase tracking-wider">Produto</th>
|
||||
<th className="px-5 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Vendido</th>
|
||||
<th className="px-5 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Estoque</th>
|
||||
<th className="px-5 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Sugestão de corte</th>
|
||||
<th className="px-5 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dark-border">
|
||||
{paginatedMissingCompositionRows.map(row => (
|
||||
<tr key={row.productId} className="transition-colors hover:bg-dark-input/50">
|
||||
<td className="px-5 py-3 font-mono text-xs font-semibold text-dark-muted">#{row.productId}</td>
|
||||
<td className="max-w-[420px] px-5 py-3"><p className="truncate font-bold text-dark-text" title={row.name}>{row.name}</p></td>
|
||||
<td className="px-5 py-3 text-right font-bold text-dark-text">{formatNumber(row.quantitySold)} un.</td>
|
||||
<td className="px-5 py-3 text-right font-bold text-dark-text">{formatNumber(row.stockQuantity)} un.</td>
|
||||
<td className="px-5 py-3 text-right font-bold text-amber-300">{formatNumber(row.suggestedQuantity)} un.</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link to={buildConsumptionReferencePath({ sku: row.productId, name: row.name })} className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-dark-input text-dark-text transition-colors hover:bg-dark-border cursor-pointer" title="Cadastrar composição" aria-label={`Cadastrar composição de ${row.name}`}><Pencil className="h-3.5 w-3.5" /></Link>
|
||||
<Link to={`/products/${row.productId}`} className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-brand-primary/10 text-brand-primary transition-opacity hover:opacity-80 cursor-pointer" title="Ver produto" aria-label={`Ver produto ${row.name}`}><Eye className="h-3.5 w-3.5" /></Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<PaginationControls
|
||||
totalItems={visibleMissingCompositionRows.length}
|
||||
currentPage={safeCurrentPage}
|
||||
totalPages={totalPages}
|
||||
pageSize={itemsPerPage}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
itemLabel="SKUs"
|
||||
pageSizeLabel="por página"
|
||||
startIndex={startIndex}
|
||||
endIndex={Math.min(startIndex + itemsPerPage, visibleMissingCompositionRows.length)}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={(size) => { setItemsPerPage(size); setCurrentPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center rounded-2xl border border-dark-border bg-dark-card px-6 py-16 text-center shadow-sm">
|
||||
<CheckCircle2 className="h-10 w-10 text-emerald-300" />
|
||||
<p className="mt-4 text-sm font-bold text-dark-text">Nenhum SKU sem composição neste filtro.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editingProduct && (
|
||||
<SkuPlanningModal
|
||||
|
||||
@@ -405,10 +405,11 @@ const SuppliesHub = () => {
|
||||
<h2 className="text-base font-bold text-dark-text">Gestão de materiais</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Cadastros e controles que sustentam o planejamento operacional.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-4">
|
||||
<ModuleCard title="Estoque e recebimentos" description="Saldo, lotes, recebimentos, inventário e movimentações de materiais." icon={Package} to="/supplies/inventory" meta={`${formatNumber(summary.stats.pendingReceipts, 0)} recebimentos pendentes · ${formatNumber(summary.stats.activeLots, 0)} lotes ativos`} />
|
||||
<ModuleCard title="Planejamento de malha" description="Organize a fila de matéria-prima para abastecer compras e corte." icon={Ruler} to="/supplies/fabric-planning" />
|
||||
<ModuleCard title="Prioridades de estoque" description="Analise reposição, cobertura e itens que precisam de revisão de cadastro." icon={PackageSearch} to="/supplies/current-planning" />
|
||||
<ModuleCard title="Dados para revisar" description="Consulte exceções de estoque, composição e cadastro fora do cálculo atual." icon={ClipboardList} to="/planning-issues" meta="Abrir pendências" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1677,7 +1678,10 @@ const MovementsTab = ({
|
||||
};
|
||||
|
||||
const InventoryScreen = () => {
|
||||
const [activeTab, setActiveTab] = useState<InventoryTab>('dashboard');
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const requestedTab = searchParams.get('tab');
|
||||
const activeTab = inventoryTabs.some(tab => tab.id === requestedTab) ? requestedTab as InventoryTab : 'dashboard';
|
||||
const setActiveTab = (tab: InventoryTab) => setSearchParams(tab === 'dashboard' ? {} : { tab });
|
||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||
const [stockSnapshot, setStockSnapshot] = useState<StockData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
Reference in New Issue
Block a user