Populate fabric planning from supply demand
This commit is contained in:
@@ -1831,6 +1831,7 @@ const InventoryScreen = () => {
|
||||
|
||||
const FabricPlanningScreen = () => {
|
||||
const [plans, setPlans] = useState<SupplyFabricPlan[]>([]);
|
||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||
const [form, setForm] = useState({
|
||||
material: '',
|
||||
color: '',
|
||||
@@ -1842,12 +1843,20 @@ const FabricPlanningScreen = () => {
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
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 () => {
|
||||
setErrorMessage('');
|
||||
const summary = await fetchSupplySummary();
|
||||
setPlans(summary.fabricPlans);
|
||||
setSummary(summary);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1857,7 +1866,10 @@ const FabricPlanningScreen = () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const summary = await fetchSupplySummary();
|
||||
if (isMounted) setPlans(summary.fabricPlans);
|
||||
if (isMounted) {
|
||||
setPlans(summary.fabricPlans);
|
||||
setSummary(summary);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) setIsLoading(false);
|
||||
}
|
||||
@@ -1910,7 +1922,7 @@ const FabricPlanningScreen = () => {
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<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}
|
||||
@@ -1918,24 +1930,80 @@ const FabricPlanningScreen = () => {
|
||||
)}
|
||||
<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">
|
||||
<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-1 text-xs font-semibold text-dark-muted">{plans.filter(plan => plan.priority === 'Crítico').length} críticos</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">Kg planejado</p>
|
||||
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalKg)} kg</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">Críticos</p>
|
||||
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.filter(plan => plan.priority === 'Crítico').length}</p>
|
||||
|
||||
<div className={`${panelClassName} overflow-hidden`}>
|
||||
<div className="flex flex-col gap-4 border-b border-dark-border p-5 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-dark-text">Fila de tecidos para abastecer o corte</h2>
|
||||
<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>
|
||||
{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 className="grid grid-cols-1 items-stretch gap-6 xl:grid-cols-[1fr_420px]">
|
||||
<div className={`${panelClassName} flex flex-col overflow-hidden`}>
|
||||
<div className="border-b border-dark-border p-5">
|
||||
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens planejados para compra ou recebimento.</p>
|
||||
<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">Registros operacionais internos; não substituem a necessidade calculada acima.</p>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className={`${emptyStateClassName} min-h-[220px] flex-1`}>
|
||||
|
||||
Reference in New Issue
Block a user