Populate fabric planning from supply demand
This commit is contained in:
@@ -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`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user