Populate fabric planning from supply demand

This commit is contained in:
Cauê Faleiros
2026-08-04 13:01:31 -03:00
parent 58caee64eb
commit 62cf279ad4

View File

@@ -1831,6 +1831,7 @@ const InventoryScreen = () => {
const FabricPlanningScreen = () => { const FabricPlanningScreen = () => {
const [plans, setPlans] = useState<SupplyFabricPlan[]>([]); const [plans, setPlans] = useState<SupplyFabricPlan[]>([]);
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [form, setForm] = useState({ const [form, setForm] = useState({
material: '', material: '',
color: '', color: '',
@@ -1842,12 +1843,20 @@ const FabricPlanningScreen = () => {
const [isBusy, setIsBusy] = useState(false); const [isBusy, setIsBusy] = useState(false);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const totalKg = plans.reduce((total, plan) => total + plan.quantityKg, 0); const fabricNeeds = summary.purchaseNeeds.filter(need => (
!need.missingReference &&
!isServicePurchaseNeed(need) &&
/\b(?:MALHA|TECIDO|RIBANA|MOLETOM)\b/i.test(need.material)
));
const fabricNeedsToBuy = fabricNeeds.filter(need => need.purchaseKg > 0);
const fabricNeedUnits = new Set(fabricNeeds.flatMap(need => (need.products || []).map(product => product.productId).filter(Boolean))).size;
const plannedFabricKg = fabricNeeds.reduce((total, need) => total + Math.max(0, need.plannedKg), 0);
const loadPlans = async () => { const loadPlans = async () => {
setErrorMessage(''); setErrorMessage('');
const summary = await fetchSupplySummary(); const summary = await fetchSupplySummary();
setPlans(summary.fabricPlans); setPlans(summary.fabricPlans);
setSummary(summary);
}; };
useEffect(() => { useEffect(() => {
@@ -1857,7 +1866,10 @@ const FabricPlanningScreen = () => {
setIsLoading(true); setIsLoading(true);
try { try {
const summary = await fetchSupplySummary(); const summary = await fetchSupplySummary();
if (isMounted) setPlans(summary.fabricPlans); if (isMounted) {
setPlans(summary.fabricPlans);
setSummary(summary);
}
} finally { } finally {
if (isMounted) setIsLoading(false); if (isMounted) setIsLoading(false);
} }
@@ -1910,7 +1922,7 @@ const FabricPlanningScreen = () => {
return ( return (
<div className={pageClassName}> <div className={pageClassName}>
<Header title="Planejamento de Malha" subtitle="Fila de matéria-prima para compra, recebimento e abastecimento do corte." backTo="/supplies" /> <Header title="Planejamento de Malha" subtitle="Fila de tecidos que abastece o corte; a compra continua sendo decidida em Necessidade de Compra." backTo="/supplies" />
{errorMessage && ( {errorMessage && (
<div className="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm font-bold text-red-300"> <div className="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm font-bold text-red-300">
{errorMessage} {errorMessage}
@@ -1918,24 +1930,80 @@ const FabricPlanningScreen = () => {
)} )}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"> <div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Planos ativos</p> <p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Tecidos com necessidade</p>
<p className="mt-2 text-3xl font-bold text-dark-text">{fabricNeedsToBuy.length}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(fabricNeedUnits, 0)} SKUs dependem deles</p>
</div>
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Demanda de tecido</p>
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(plannedFabricKg)} kg</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">necessidade planejada para o período</p>
</div>
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Planos locais ativos</p>
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.length}</p> <p className="mt-2 text-3xl font-bold text-dark-text">{plans.length}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">{plans.filter(plan => plan.priority === 'Crítico').length} críticos</p>
</div> </div>
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"> </div>
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Kg planejado</p>
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalKg)} kg</p> <div className={`${panelClassName} overflow-hidden`}>
</div> <div className="flex flex-col gap-4 border-b border-dark-border p-5 md:flex-row md:items-start md:justify-between">
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"> <div>
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Críticos</p> <h2 className="text-base font-bold text-dark-text">Fila de tecidos para abastecer o corte</h2>
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.filter(plan => plan.priority === 'Crítico').length}</p> <p className="mt-1 text-sm font-semibold text-dark-muted">Necessidade calculada pela composição, saldo sincronizado e recebimentos pendentes.</p>
</div>
<RouterLink to="/supplies/purchase-needs?tab=materials&filter=to_buy" className={buttonClassName}>Abrir compra de materiais <ArrowRight className="h-4 w-4" /></RouterLink>
</div> </div>
{isLoading ? (
<div className={emptyStateClassName}>
<Ruler className="h-8 w-8 text-brand-primary" />
<h3 className="text-base font-bold text-dark-text">Calculando fila de tecidos...</h3>
</div>
) : fabricNeeds.length ? (
<div className="divide-y divide-dark-border">
{fabricNeeds.slice().sort((a, b) => b.purchaseKg - a.purchaseKg).map(need => {
const impactedSkus = new Set((need.products || []).map(product => product.productId).filter(Boolean)).size;
const isCovered = need.purchaseKg <= 0;
return (
<div key={need.material} className="grid grid-cols-1 gap-4 px-5 py-4 lg:grid-cols-[minmax(240px,1.25fr)_minmax(360px,1fr)_150px] lg:items-center">
<div>
<p className="text-sm font-bold text-dark-text">{need.material}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">{impactedSkus} SKUs impactados · {need.suppliers.length ? need.suppliers.join(' · ') : 'Fornecedor não informado'}</p>
</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{[
['Necessário', need.plannedKg],
['Em estoque', need.stockKg],
['A receber', need.pendingKg],
['Comprar', need.purchaseKg],
].map(([label, value]) => (
<div key={label as string} className="rounded-lg border border-dark-border bg-dark-input/45 px-3 py-2">
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">{label}</p>
<p className={`mt-1 text-sm font-bold ${label === 'Comprar' && !isCovered ? 'text-amber-300' : label === 'Em estoque' && Number(value) < 0 ? 'text-red-300' : 'text-dark-text'}`}>{formatNumber(Number(value))} {need.unit || 'kg'}</p>
</div>
))}
</div>
<div className="lg:text-right">
<span className={`inline-flex rounded-full border px-2.5 py-1 text-xs font-bold ${isCovered ? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300' : 'border-amber-400/30 bg-amber-400/10 text-amber-300'}`}>{isCovered ? 'Coberto' : 'Comprar tecido'}</span>
</div>
</div>
);
})}
</div>
) : (
<div className={emptyStateClassName}>
<Ruler className="h-8 w-8 text-brand-primary" />
<h3 className="text-base font-bold text-dark-text">Nenhum tecido identificado na composição</h3>
<p className="text-sm font-semibold text-dark-muted">Mapeie consumo de malha, tecido ou ribana para gerar esta fila.</p>
</div>
)}
</div> </div>
<div className="grid grid-cols-1 items-stretch gap-6 xl:grid-cols-[1fr_420px]"> <div className="grid grid-cols-1 items-stretch gap-6 xl:grid-cols-[1fr_420px]">
<div className={`${panelClassName} flex flex-col overflow-hidden`}> <div className={`${panelClassName} flex flex-col overflow-hidden`}>
<div className="border-b border-dark-border p-5"> <div className="border-b border-dark-border p-5">
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2> <h2 className="text-base font-bold text-dark-text">Planos locais de recebimento e alocação</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens planejados para compra ou recebimento.</p> <p className="mt-1 text-sm font-semibold text-dark-muted">Registros operacionais internos; não substituem a necessidade calculada acima.</p>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className={`${emptyStateClassName} min-h-[220px] flex-1`}> <div className={`${emptyStateClassName} min-h-[220px] flex-1`}>