Make supplies hub action-driven
This commit is contained in:
@@ -201,6 +201,81 @@ const ModuleCard = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FlowStepCard = ({
|
||||||
|
number,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
meta,
|
||||||
|
icon: Icon,
|
||||||
|
to,
|
||||||
|
tone = 'default',
|
||||||
|
}: {
|
||||||
|
number: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
meta: string;
|
||||||
|
icon: typeof Package;
|
||||||
|
to: string;
|
||||||
|
tone?: 'default' | 'critical' | 'attention' | 'ready';
|
||||||
|
}) => {
|
||||||
|
const tones = {
|
||||||
|
default: 'border-dark-border bg-dark-card text-brand-primary',
|
||||||
|
critical: 'border-red-400/30 bg-red-500/[0.055] text-red-300',
|
||||||
|
attention: 'border-amber-400/30 bg-amber-500/[0.055] text-amber-300',
|
||||||
|
ready: 'border-emerald-400/30 bg-emerald-500/[0.055] text-emerald-300',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterLink to={to} className={`${panelClassName} group flex min-h-44 flex-col p-5 transition-colors hover:border-brand-primary/70`}>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<span className="text-xs font-bold tracking-[0.18em] text-dark-muted">{number}</span>
|
||||||
|
<div className={`flex h-10 w-10 items-center justify-center rounded-xl border ${tones[tone]}`}>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<h3 className="text-base font-bold text-dark-text">{title}</h3>
|
||||||
|
<p className="mt-1.5 text-sm font-semibold leading-5 text-dark-muted">{description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex items-center justify-between gap-3 pt-5">
|
||||||
|
<span className={`text-xs font-bold ${tone === 'critical' ? 'text-red-300' : tone === 'attention' ? 'text-amber-300' : tone === 'ready' ? 'text-emerald-300' : 'text-dark-text'}`}>{meta}</span>
|
||||||
|
<ArrowRight className="h-4 w-4 shrink-0 text-dark-muted transition-transform group-hover:translate-x-0.5 group-hover:text-brand-primary" />
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlowBlocker = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
count,
|
||||||
|
to,
|
||||||
|
tone,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
count: number;
|
||||||
|
to: string;
|
||||||
|
tone: 'critical' | 'attention' | 'info';
|
||||||
|
}) => {
|
||||||
|
const tones = {
|
||||||
|
critical: 'border-red-400/25 bg-red-500/[0.045] text-red-300',
|
||||||
|
attention: 'border-amber-400/25 bg-amber-500/[0.045] text-amber-300',
|
||||||
|
info: 'border-sky-400/25 bg-sky-500/[0.045] text-sky-300',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterLink to={to} className={`flex items-center gap-3 rounded-xl border px-3.5 py-3 transition-colors hover:border-brand-primary/60 ${tones[tone]}`}>
|
||||||
|
<span className="flex h-7 min-w-7 items-center justify-center rounded-lg bg-dark-card text-xs font-bold text-dark-text">{formatNumber(count, 0)}</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block text-sm font-bold text-dark-text">{title}</span>
|
||||||
|
<span className="block truncate text-xs font-semibold text-dark-muted">{description}</span>
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="h-4 w-4 shrink-0" />
|
||||||
|
</RouterLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SuppliesHub = () => {
|
const SuppliesHub = () => {
|
||||||
const [isExportingDiagnostic, setIsExportingDiagnostic] = useState(false);
|
const [isExportingDiagnostic, setIsExportingDiagnostic] = useState(false);
|
||||||
const [diagnosticError, setDiagnosticError] = useState('');
|
const [diagnosticError, setDiagnosticError] = useState('');
|
||||||
@@ -224,10 +299,63 @@ const SuppliesHub = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const materialPurchaseNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need) && need.purchaseKg > 0);
|
const materialPurchaseNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need) && need.purchaseKg > 0);
|
||||||
|
const negativeMaterialNeeds = materialPurchaseNeeds.filter(need => need.stockKg < 0);
|
||||||
|
const noStockMaterialNeeds = materialPurchaseNeeds.filter(need => need.stockKg === 0);
|
||||||
const mappingSkuCount = new Set(summary.purchaseNeeds
|
const mappingSkuCount = new Set(summary.purchaseNeeds
|
||||||
.filter(need => need.missingReference)
|
.filter(need => need.missingReference)
|
||||||
.flatMap(need => (need.products || []).map(product => product.productId))
|
.flatMap(need => (need.products || []).map(product => product.productId))
|
||||||
.filter(Boolean)).size;
|
.filter(Boolean)).size;
|
||||||
|
const activeProductionCount = productionCounts.open + productionCounts.inProgress;
|
||||||
|
const nextAction = negativeMaterialNeeds.length > 0
|
||||||
|
? {
|
||||||
|
label: 'Prioridade imediata',
|
||||||
|
title: `Conferir ${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos`,
|
||||||
|
description: 'Estoque negativo distorce a compra sugerida e pode liberar corte sem material físico.',
|
||||||
|
meta: 'Abrir compra de materiais',
|
||||||
|
icon: AlertTriangle,
|
||||||
|
to: '/supplies/purchase-needs',
|
||||||
|
tone: 'critical' as const,
|
||||||
|
}
|
||||||
|
: mappingSkuCount > 0
|
||||||
|
? {
|
||||||
|
label: 'Próxima ação',
|
||||||
|
title: `Mapear o consumo de ${formatNumber(mappingSkuCount, 0)} SKUs`,
|
||||||
|
description: 'Sem composição confiável, a necessidade de compra e o corte não representam a demanda real.',
|
||||||
|
meta: 'Abrir pendências de composição',
|
||||||
|
icon: LinkIcon,
|
||||||
|
to: '/supplies/purchase-needs',
|
||||||
|
tone: 'info' as const,
|
||||||
|
}
|
||||||
|
: materialPurchaseNeeds.length > 0
|
||||||
|
? {
|
||||||
|
label: 'Próxima ação',
|
||||||
|
title: `Comprar ${formatNumber(materialPurchaseNeeds.length, 0)} materiais`,
|
||||||
|
description: 'A necessidade já desconta saldo e recebimentos para apoiar a decisão de compra.',
|
||||||
|
meta: 'Ver lista de compra',
|
||||||
|
icon: BarChart3,
|
||||||
|
to: '/supplies/purchase-needs',
|
||||||
|
tone: 'attention' as const,
|
||||||
|
}
|
||||||
|
: activeProductionCount > 0
|
||||||
|
? {
|
||||||
|
label: 'Próxima ação',
|
||||||
|
title: `Acompanhar ${formatNumber(activeProductionCount, 0)} ordens em execução`,
|
||||||
|
description: 'Há produção aberta ou em andamento para confirmar antes de planejar uma nova rodada.',
|
||||||
|
meta: 'Abrir ordens de produção',
|
||||||
|
icon: ClipboardList,
|
||||||
|
to: '/production-orders',
|
||||||
|
tone: 'ready' as const,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: 'Próxima ação',
|
||||||
|
title: 'Definir o próximo corte',
|
||||||
|
description: 'Use a demanda e a cobertura para escolher os SKUs que devem entrar em produção.',
|
||||||
|
meta: 'Abrir plano de corte',
|
||||||
|
icon: Scissors,
|
||||||
|
to: '/cutting',
|
||||||
|
tone: 'default' as const,
|
||||||
|
};
|
||||||
|
const NextActionIcon = nextAction.icon;
|
||||||
|
|
||||||
const handleExportDiagnostic = async () => {
|
const handleExportDiagnostic = async () => {
|
||||||
setIsExportingDiagnostic(true);
|
setIsExportingDiagnostic(true);
|
||||||
@@ -262,17 +390,44 @@ const SuppliesHub = () => {
|
|||||||
{diagnosticError}
|
{diagnosticError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section>
|
<section className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,.65fr)]">
|
||||||
<div className="mb-3 flex items-end justify-between gap-4">
|
<RouterLink to={nextAction.to} className={`group rounded-2xl border p-6 shadow-sm transition-colors hover:border-brand-primary/70 ${nextAction.tone === 'critical' ? 'border-red-400/30 bg-red-500/[0.055]' : nextAction.tone === 'attention' ? 'border-amber-400/30 bg-amber-500/[0.055]' : nextAction.tone === 'ready' ? 'border-emerald-400/30 bg-emerald-500/[0.055]' : nextAction.tone === 'info' ? 'border-sky-400/30 bg-sky-500/[0.055]' : 'border-brand-primary/30 bg-brand-primary/[0.045]'}`}>
|
||||||
|
<div className="flex items-start justify-between gap-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-[0.14em] text-dark-muted">{nextAction.label}</p>
|
||||||
|
<h2 className="mt-2 text-xl font-bold text-dark-text">{nextAction.title}</h2>
|
||||||
|
<p className="mt-2 max-w-2xl text-sm font-semibold leading-6 text-dark-muted">{nextAction.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border ${nextAction.tone === 'critical' ? 'border-red-400/30 bg-red-500/10 text-red-300' : nextAction.tone === 'attention' ? 'border-amber-400/30 bg-amber-500/10 text-amber-300' : nextAction.tone === 'ready' ? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-300' : nextAction.tone === 'info' ? 'border-sky-400/30 bg-sky-500/10 text-sky-300' : 'border-brand-primary/30 bg-brand-primary/10 text-brand-primary'}`}>
|
||||||
|
<NextActionIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex items-center gap-2 text-sm font-bold text-dark-text">
|
||||||
|
{nextAction.meta}
|
||||||
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5 group-hover:text-brand-primary" />
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
<div className={`${panelClassName} p-5`}>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-bold text-dark-text">Ações de hoje</h2>
|
<h2 className="text-base font-bold text-dark-text">Pontos que bloqueiam o fluxo</h2>
|
||||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Comprar materiais, definir o corte e acompanhar a produção.</p>
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Resolva estes pontos antes de confiar no plano.</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<FlowBlocker title="Saldos a conferir" description="Materiais com estoque negativo" count={negativeMaterialNeeds.length} to="/supplies/purchase-needs" tone="critical" />
|
||||||
|
<FlowBlocker title="Dados para mapear" description="SKUs sem composição confiável" count={mappingSkuCount} to="/supplies/purchase-needs" tone="info" />
|
||||||
|
<FlowBlocker title="Materiais sem saldo" description="Itens necessários sem disponibilidade" count={noStockMaterialNeeds.length} to="/supplies/purchase-needs" tone="attention" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div className="mb-3">
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Fluxo operacional</h2>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Da necessidade de material até a produção acompanhada.</p>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||||
<ModuleCard title="Comprar materiais" description="Necessidades de matéria-prima calculadas a partir da demanda e da composição." icon={BarChart3} to="/supplies/purchase-needs" priority meta={`${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar${mappingSkuCount ? ` · ${formatNumber(mappingSkuCount, 0)} SKUs para mapear` : ''}`} />
|
<FlowStepCard number="01" title="Garantir materiais" description="Confira saldos, pendências de composição e a compra sugerida." icon={BarChart3} to="/supplies/purchase-needs" tone={negativeMaterialNeeds.length > 0 ? 'critical' : materialPurchaseNeeds.length > 0 ? 'attention' : 'ready'} meta={negativeMaterialNeeds.length > 0 ? `${formatNumber(negativeMaterialNeeds.length, 0)} saldos negativos` : materialPurchaseNeeds.length > 0 ? `${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar` : 'Materiais cobertos'} />
|
||||||
<ModuleCard title="Planejar corte" description="Defina quais SKUs, cores e tamanhos priorizar conforme demanda e disponibilidade de material." icon={Scissors} to="/cutting" priority meta="Revise bloqueios de material antes de gerar OPs" />
|
<FlowStepCard number="02" title="Definir corte" description="Priorize SKUs, cores e tamanhos com necessidade e material disponível." icon={Scissors} to="/cutting" tone={mappingSkuCount > 0 ? 'attention' : 'default'} meta={mappingSkuCount > 0 ? `${formatNumber(mappingSkuCount, 0)} SKUs precisam de mapeamento` : 'Abrir plano de corte'} />
|
||||||
<ModuleCard title="Acompanhar produção" description="Consulte ordens geradas pelo corte e acompanhe o andamento da fabricação." icon={ClipboardList} to="/production-orders" priority meta={`${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento`} />
|
<FlowStepCard number="03" title="Acompanhar produção" description="Gerencie as OPs criadas a partir do corte até sua finalização." icon={ClipboardList} to="/production-orders" tone={activeProductionCount > 0 ? 'ready' : 'default'} meta={activeProductionCount > 0 ? `${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento` : 'Nenhuma OP em execução'} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="border-t border-dark-border pt-6">
|
<section className="border-t border-dark-border pt-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user