Add current data supply planning
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m34s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m34s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { Link as RouterLink, Navigate, useParams } from 'react-router-dom';
|
||||
import { type FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { Link as RouterLink, Navigate, useOutletContext, useParams } from 'react-router-dom';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Download,
|
||||
Link as LinkIcon,
|
||||
Package,
|
||||
PackageSearch,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Repeat2,
|
||||
@@ -23,9 +24,14 @@ import {
|
||||
Warehouse,
|
||||
} from 'lucide-react';
|
||||
import { buildConsumptionReferencePath } from '../catalogLinks';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchSupplySummary } from '../dataService';
|
||||
import type { SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types';
|
||||
import ProductTypeBadge from '../components/ProductTypeBadge';
|
||||
import { classifyCutFamily } from '../analytics/cutting';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchCuttingSettings, fetchProductAnalytics, fetchSupplySummary } from '../dataService';
|
||||
import { parseProductName } from '../productParsing';
|
||||
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||
import type { CuttingSettings, DateRange, ProductAnalyticsItem, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types';
|
||||
|
||||
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||
type ReceiptView = 'new' | 'pending' | 'history';
|
||||
@@ -108,6 +114,38 @@ const receiptCategories: Array<{ name: string; icon: typeof Package }> = [
|
||||
{ name: 'Outro material', icon: ClipboardList },
|
||||
];
|
||||
|
||||
type DemandQueue = 'apparel' | 'inputs' | 'review' | 'dead';
|
||||
|
||||
type DemandRow = ProductAnalyticsItem & {
|
||||
productType: ProductTypeKey;
|
||||
color: string;
|
||||
size: string;
|
||||
dailySales: number;
|
||||
daysOfCover: number | null;
|
||||
targetDemand: number;
|
||||
suggestedUnits: number;
|
||||
missingData: string[];
|
||||
queue: DemandQueue;
|
||||
};
|
||||
|
||||
const demandQueueLabels: Record<DemandQueue, string> = {
|
||||
apparel: 'Cortar / Repor',
|
||||
inputs: 'Comprar insumos',
|
||||
review: 'Revisar dados',
|
||||
dead: 'Estoque parado',
|
||||
};
|
||||
|
||||
const demandQueueDescriptions: Record<DemandQueue, string> = {
|
||||
apparel: 'Produtos acabados com venda e cobertura baixa. Ainda é sugestão por unidade, não kg de malha.',
|
||||
inputs: 'Embalagens, matérias-primas e insumos DTF com cobertura baixa pelo próprio estoque.',
|
||||
review: 'SKUs vendidos que precisam de correção de tipo, cor, tamanho ou família antes de planejar melhor.',
|
||||
dead: 'Itens com estoque e pouca ou nenhuma venda no período.',
|
||||
};
|
||||
|
||||
const directPurchaseTypes = new Set<ProductTypeKey>(['packaging', 'dtf_input', 'raw_material', 'machine_part', 'finished_accessory']);
|
||||
|
||||
const planningReviewTypes = new Set<ProductTypeKey>(['unknown', 'kit_bundle']);
|
||||
|
||||
const Header = ({ title, subtitle, backTo }: { title: string; subtitle: string; backTo?: string }) => (
|
||||
<div className="flex flex-col gap-3">
|
||||
{backTo && (
|
||||
@@ -154,6 +192,7 @@ const SuppliesHub = () => (
|
||||
<div className={pageClassName}>
|
||||
<Header title="Suprimentos" subtitle="Corte, estoque, malha e compras em um fluxo operacional." />
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<ModuleCard title="Planejamento por Dados Atuais" description="Filas de reposição, compra, revisão e estoque parado usando vendas, estoque e cobertura do Tiny." icon={PackageSearch} to="/supplies/current-planning" />
|
||||
<ModuleCard title="Planejamento de Corte" description="Ruptura por cor e tamanho, montagem do corte e prioridade por cobertura." icon={Scissors} to="/cutting" />
|
||||
<ModuleCard title="Controle de Estoque" description="Saldo, lotes, recebimentos, inventário e movimentações." icon={Package} to="/supplies/inventory" />
|
||||
<ModuleCard title="Ordens de Produção" description="Ordens geradas pelo corte e acompanhamento de produção." icon={ClipboardList} to="/production-orders" />
|
||||
@@ -163,6 +202,369 @@ const SuppliesHub = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const getRangeDays = (range: DateRange) => {
|
||||
const start = new Date(range.start);
|
||||
const end = new Date(range.end);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
return Math.max(1, Math.round((end.getTime() - start.getTime()) / 86_400_000) + 1);
|
||||
};
|
||||
|
||||
const formatDays = (value: number | null) => {
|
||||
if (value === null) return '-';
|
||||
if (value > 999) return '999+ dias';
|
||||
return `${formatNumber(value, value < 10 ? 1 : 0)} dias`;
|
||||
};
|
||||
|
||||
const buildDemandRow = (
|
||||
product: ProductAnalyticsItem,
|
||||
settings: CuttingSettings,
|
||||
rangeDays: number,
|
||||
targetCoverageDays: number
|
||||
): DemandRow => {
|
||||
const override = settings.productOverrides[product.id];
|
||||
const metadata = parseProductName(product.name);
|
||||
const productType = resolveProductType(product.name, override);
|
||||
const color = override?.color || metadata.color;
|
||||
const size = (override?.size || metadata.size).toUpperCase();
|
||||
const dailySales = product.quantitySold / rangeDays;
|
||||
const daysOfCover = dailySales > 0 ? product.stock / dailySales : null;
|
||||
const targetDemand = dailySales * targetCoverageDays;
|
||||
const suggestedUnits = Math.max(0, Math.ceil(targetDemand - product.stock));
|
||||
const missingData: string[] = [];
|
||||
|
||||
if (planningReviewTypes.has(productType)) missingData.push('tipo');
|
||||
if (productType === 'finished_apparel') {
|
||||
if (!color) missingData.push('cor');
|
||||
if (!size) missingData.push('tamanho');
|
||||
if ((override?.familyKey || classifyCutFamily(metadata.baseName).key) === 'OUTROS') missingData.push('família');
|
||||
}
|
||||
|
||||
let queue: DemandQueue = 'review';
|
||||
if (product.stock > 0 && (dailySales === 0 || (daysOfCover !== null && daysOfCover > 120))) {
|
||||
queue = 'dead';
|
||||
} else if (missingData.length) {
|
||||
queue = 'review';
|
||||
} else if (productType === 'finished_apparel') {
|
||||
queue = 'apparel';
|
||||
} else if (directPurchaseTypes.has(productType)) {
|
||||
queue = 'inputs';
|
||||
}
|
||||
|
||||
return {
|
||||
...product,
|
||||
productType,
|
||||
color,
|
||||
size,
|
||||
dailySales,
|
||||
daysOfCover,
|
||||
targetDemand,
|
||||
suggestedUnits,
|
||||
missingData,
|
||||
queue,
|
||||
};
|
||||
};
|
||||
|
||||
const DemandPlanningScreen = () => {
|
||||
const { dateRange, setDateRange } = useOutletContext<{
|
||||
dateRange: DateRange,
|
||||
setDateRange: (range: DateRange) => void
|
||||
}>();
|
||||
const [products, setProducts] = useState<ProductAnalyticsItem[]>([]);
|
||||
const [settings, setSettings] = useState<CuttingSettings>({ familyYields: {}, productOverrides: {} });
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [queue, setQueue] = useState<DemandQueue>('apparel');
|
||||
const [search, setSearch] = useState('');
|
||||
const [targetCoverageDays, setTargetCoverageDays] = useState(30);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const load = async () => {
|
||||
setIsLoading(true);
|
||||
const [productData, planningSettings] = await Promise.all([
|
||||
fetchProductAnalytics(dateRange),
|
||||
fetchCuttingSettings(),
|
||||
]);
|
||||
|
||||
if (isMounted) {
|
||||
setProducts(productData);
|
||||
setSettings(planningSettings);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [dateRange]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const rangeDays = getRangeDays(dateRange);
|
||||
return products.map(product => buildDemandRow(product, settings, rangeDays, targetCoverageDays));
|
||||
}, [dateRange, products, settings, targetCoverageDays]);
|
||||
|
||||
const queueRows = useMemo(() => {
|
||||
const normalizedSearch = normalizeSearch(search);
|
||||
return rows
|
||||
.filter(row => row.queue === queue)
|
||||
.filter(row => {
|
||||
if (queue === 'dead') return row.stock > 0;
|
||||
if (queue === 'review') return row.quantitySold > 0 || row.stock > 0;
|
||||
return row.dailySales > 0 && (row.suggestedUnits > 0 || (row.daysOfCover !== null && row.daysOfCover <= targetCoverageDays));
|
||||
})
|
||||
.filter(row => (
|
||||
!normalizedSearch || normalizeSearch(`${row.id} ${row.name} ${row.color} ${row.size} ${row.missingData.join(' ')}`).includes(normalizedSearch)
|
||||
))
|
||||
.sort((a, b) => {
|
||||
if (queue === 'dead') {
|
||||
return (b.stock * b.lastPrice) - (a.stock * a.lastPrice);
|
||||
}
|
||||
if (queue === 'review') {
|
||||
if (b.quantitySold !== a.quantitySold) return b.quantitySold - a.quantitySold;
|
||||
return b.revenue - a.revenue;
|
||||
}
|
||||
if (b.suggestedUnits !== a.suggestedUnits) return b.suggestedUnits - a.suggestedUnits;
|
||||
return b.dailySales - a.dailySales;
|
||||
});
|
||||
}, [queue, rows, search, targetCoverageDays]);
|
||||
|
||||
const queueStats = useMemo(() => {
|
||||
return (Object.keys(demandQueueLabels) as DemandQueue[]).reduce<Record<DemandQueue, number>>((acc, item) => {
|
||||
acc[item] = rows.filter(row => {
|
||||
if (row.queue !== item) return false;
|
||||
if (item === 'dead') return row.stock > 0;
|
||||
if (item === 'review') return row.quantitySold > 0 || row.stock > 0;
|
||||
return row.dailySales > 0 && (row.suggestedUnits > 0 || (row.daysOfCover !== null && row.daysOfCover <= targetCoverageDays));
|
||||
}).length;
|
||||
return acc;
|
||||
}, { apparel: 0, inputs: 0, review: 0, dead: 0 });
|
||||
}, [rows, targetCoverageDays]);
|
||||
|
||||
const totalPages = Math.ceil(queueRows.length / itemsPerPage);
|
||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||
const paginatedRows = queueRows.slice(startIndex, startIndex + itemsPerPage);
|
||||
const suggestedTotal = queueRows.reduce((total, row) => total + (queue === 'dead' ? row.stock * row.lastPrice : row.suggestedUnits), 0);
|
||||
|
||||
return (
|
||||
<div className={pageClassName}>
|
||||
<div className="grid grid-cols-1 gap-4 2xl:grid-cols-[minmax(520px,1fr)_auto] 2xl:items-start">
|
||||
<Header title="Planejamento por Dados Atuais" subtitle="Filas operacionais usando vendas, estoque e cobertura já disponíveis no Tiny/prod graphs." backTo="/supplies" />
|
||||
<DateRangePicker
|
||||
dateRange={dateRange}
|
||||
onChange={(range) => {
|
||||
setDateRange(range);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
{(Object.keys(demandQueueLabels) as DemandQueue[]).map(item => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setQueue(item);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`rounded-2xl border p-4 text-left transition-colors cursor-pointer ${
|
||||
queue === item ? 'border-brand-primary/45 bg-brand-primary/10' : 'border-dark-border bg-dark-card hover:border-brand-primary/35'
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">{demandQueueLabels[item]}</p>
|
||||
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(queueStats[item], 0)}</p>
|
||||
<p className="mt-1 line-clamp-2 text-xs font-semibold text-dark-muted">{demandQueueDescriptions[item]}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-dark-text">{demandQueueLabels[queue]}</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">{demandQueueDescriptions[queue]}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<label className="relative min-w-72">
|
||||
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`${inputClassName} pl-9`}
|
||||
placeholder="Buscar SKU, produto, cor..."
|
||||
/>
|
||||
</label>
|
||||
<select
|
||||
value={targetCoverageDays}
|
||||
onChange={(event) => {
|
||||
setTargetCoverageDays(Number(event.target.value));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={inputClassName}
|
||||
title="Cobertura alvo"
|
||||
>
|
||||
{[15, 30, 45, 60].map(days => <option key={days} value={days}>{days} dias</option>)}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => exportCsv('planejamento-dados-atuais.csv', queueRows.map(row => ({
|
||||
fila: demandQueueLabels[queue],
|
||||
sku: row.id,
|
||||
produto: row.name,
|
||||
tipo: row.productType,
|
||||
cor: row.color,
|
||||
tamanho: row.size,
|
||||
vendido: row.quantitySold,
|
||||
estoque: row.stock,
|
||||
media_diaria: row.dailySales.toFixed(2).replace('.', ','),
|
||||
cobertura_dias: row.daysOfCover === null ? '' : row.daysOfCover.toFixed(1).replace('.', ','),
|
||||
sugestao_unidades: row.suggestedUnits,
|
||||
pendencias: row.missingData.join(' | '),
|
||||
})))}
|
||||
className={buttonClassName}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/35 p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Itens na fila</p>
|
||||
<p className="mt-2 text-2xl font-bold text-dark-text">{formatNumber(queueRows.length, 0)}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/35 p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">{queue === 'dead' ? 'Valor em estoque' : 'Unidades sugeridas'}</p>
|
||||
<p className="mt-2 text-2xl font-bold text-dark-text">
|
||||
{queue === 'dead'
|
||||
? new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(suggestedTotal)
|
||||
: `${formatNumber(suggestedTotal, 0)} un.`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/35 p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Cobertura alvo</p>
|
||||
<p className="mt-2 text-2xl font-bold text-dark-text">{targetCoverageDays} dias</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className={emptyStateClassName}>
|
||||
<PackageSearch className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Carregando dados de produtos...</h3>
|
||||
</div>
|
||||
) : queueRows.length ? (
|
||||
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[1080px] table-fixed border-collapse">
|
||||
<colgroup>
|
||||
<col className="w-[120px]" />
|
||||
<col />
|
||||
<col className="w-[140px]" />
|
||||
<col className="w-[120px]" />
|
||||
<col className="w-[120px]" />
|
||||
<col className="w-[120px]" />
|
||||
<col className="w-[130px]" />
|
||||
<col className="w-[100px]" />
|
||||
</colgroup>
|
||||
<thead className="border-b border-dark-border bg-dark-input/40 text-xs font-bold uppercase tracking-widest text-dark-muted">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3 text-left">SKU</th>
|
||||
<th scope="col" className="px-4 py-3 text-left">Produto</th>
|
||||
<th scope="col" className="px-4 py-3 text-left">Tipo</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">Média/dia</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">Estoque</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">Cobertura</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">{queue === 'dead' ? 'Valor estoque' : 'Sugestão'}</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dark-border bg-dark-card">
|
||||
{paginatedRows.map(row => (
|
||||
<tr key={row.id}>
|
||||
<td className="px-4 py-3 font-mono text-[11px] text-dark-muted">#{row.id}</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p>
|
||||
<p className="mt-1 truncate text-xs font-semibold text-dark-muted">
|
||||
Cor: {row.color || '-'} · Tam.: {row.size || '-'}
|
||||
{row.missingData.length ? ` · Falta: ${row.missingData.join(', ')}` : ''}
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-4 py-3"><ProductTypeBadge type={row.productType} /></td>
|
||||
<td className="px-4 py-3 text-right text-sm font-bold text-dark-text">{formatNumber(row.dailySales, 2)}</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-bold text-dark-text">{formatNumber(row.stock, 0)} un.</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-bold text-dark-text">{formatDays(row.daysOfCover)}</td>
|
||||
<td className={`px-4 py-3 text-right text-sm font-bold ${queue === 'dead' ? 'text-amber-300' : row.suggestedUnits > 0 ? 'text-amber-300' : 'text-emerald-300'}`}>
|
||||
{queue === 'dead'
|
||||
? new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(row.stock * row.lastPrice)
|
||||
: `${formatNumber(row.suggestedUnits, 0)} un.`}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{queue === 'review' && (
|
||||
<RouterLink
|
||||
to="/planning-issues"
|
||||
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"
|
||||
title="Abrir dados pendentes"
|
||||
aria-label="Abrir dados pendentes"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</RouterLink>
|
||||
)}
|
||||
<RouterLink
|
||||
to={`/products/${row.id}`}
|
||||
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"
|
||||
title={`Ver SKU ${row.id}`}
|
||||
aria-label={`Ver SKU ${row.id}`}
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<PaginationControls
|
||||
totalItems={queueRows.length}
|
||||
currentPage={safeCurrentPage}
|
||||
totalPages={totalPages}
|
||||
pageSize={itemsPerPage}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
itemLabel="SKUs"
|
||||
pageSizeLabel="itens por página"
|
||||
startIndex={startIndex}
|
||||
endIndex={Math.min(startIndex + itemsPerPage, queueRows.length)}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={(pageSize) => {
|
||||
setItemsPerPage(pageSize);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="border-t border-dark-border px-4 py-3"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={emptyStateClassName}>
|
||||
<PackageSearch className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Nada nesta fila</h3>
|
||||
<p className="text-sm font-semibold text-dark-muted">Ajuste a busca, período ou cobertura alvo.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatGrid = ({ summary }: { summary: SupplySummary }) => {
|
||||
const stats = [
|
||||
{ label: 'Total em estoque', value: `${formatNumber(summary.stats.totalQuantityKg)} kg` },
|
||||
@@ -1364,6 +1766,7 @@ const Supplies = () => {
|
||||
const { section } = useParams<{ section?: string }>();
|
||||
|
||||
if (!section) return <SuppliesHub />;
|
||||
if (section === 'current-planning') return <DemandPlanningScreen />;
|
||||
if (section === 'inventory') return <InventoryScreen />;
|
||||
if (section === 'fabric-planning') return <FabricPlanningScreen />;
|
||||
if (section === 'purchase-needs') return <PurchaseNeedsScreen />;
|
||||
|
||||
Reference in New Issue
Block a user