Connect supplies to project demand

This commit is contained in:
Cauê Faleiros
2026-07-13 15:17:33 -03:00
parent 8f3f2f3e93
commit 060b4da907
5 changed files with 334 additions and 17 deletions

View File

@@ -1124,6 +1124,24 @@ const purchaseStatusLabels: Record<SupplyPurchaseNeed['status'], string> = {
ok: 'Coberto',
};
const getNeedUnit = (need: SupplyPurchaseNeed) => need.unit || 'kg';
const formatNeedQuantity = (need: SupplyPurchaseNeed, value: number) => (
`${formatNumber(value)} ${getNeedUnit(need)}`
);
const summarizePurchaseNeeds = (needs: SupplyPurchaseNeed[]) => {
const totalsByUnit = needs.reduce<Record<string, number>>((totals, need) => {
if (need.purchaseKg <= 0) return totals;
const unit = getNeedUnit(need);
totals[unit] = (totals[unit] || 0) + need.purchaseKg;
return totals;
}, {});
const summaries = Object.entries(totalsByUnit).map(([unit, total]) => `${formatNumber(total)} ${unit}`);
return summaries.length ? summaries.join(' + ') : '0 kg';
};
const PurchaseNeedsScreen = () => {
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [isLoading, setIsLoading] = useState(true);
@@ -1159,17 +1177,19 @@ const PurchaseNeedsScreen = () => {
!normalizedSearch || normalizeSearch(`${need.material} ${need.suppliers.join(' ')} ${need.colors.join(' ')}`).includes(normalizedSearch)
));
const purchaseItemCount = summary.purchaseNeeds.filter(need => need.purchaseKg > 0).length;
const suggestedPurchaseKg = summary.purchaseNeeds.reduce((total, need) => total + need.purchaseKg, 0);
const suggestedPurchaseSummary = summarizePurchaseNeeds(summary.purchaseNeeds);
const pendingSupplierCount = new Set(summary.purchaseNeeds.flatMap(need => need.suppliers)).size;
const missingReferenceCount = summary.purchaseNeeds.filter(need => need.missingReference).length;
return (
<div className={pageClassName}>
<Header title="Necessidade de Compra" subtitle="Materiais abaixo do mínimo e necessidade projetada para compra." backTo="/supplies" />
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{[
{ label: 'Itens a comprar', value: `${purchaseItemCount}` },
{ label: 'Compra sugerida', value: `${formatNumber(suggestedPurchaseKg)} kg` },
{ label: 'Fornecedores envolvidos', value: `${pendingSupplierCount}` },
{ label: 'Compra sugerida', value: suggestedPurchaseSummary },
{ label: 'Fornecedores', value: `${pendingSupplierCount}` },
{ label: 'Sem referência', value: `${missingReferenceCount}` },
].map(stat => (
<div key={stat.label} 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">{stat.label}</p>
@@ -1193,10 +1213,12 @@ const PurchaseNeedsScreen = () => {
estoque_kg: need.stockKg,
pendente_kg: need.pendingKg,
comprar_kg: need.purchaseKg,
unidade: getNeedUnit(need),
prioridade: need.priority,
cobertura: purchaseStatusLabels[need.status],
cobertura: need.missingReference ? 'Sem referência de consumo' : purchaseStatusLabels[need.status],
fornecedores: need.suppliers.join(' | '),
cores: need.colors.join(' | '),
produtos: (need.products || []).map(product => `${product.productId} ${product.name}`).join(' | '),
})))}
className={buttonClassName}
>
@@ -1229,21 +1251,31 @@ const PurchaseNeedsScreen = () => {
<div>
<p className="text-sm font-bold text-dark-text">{need.material}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">
{(need.colors.length ? need.colors.join(', ') : 'Todas as cores')} · {(need.suppliers.length ? need.suppliers.join(', ') : 'Sem fornecedor')}
{need.missingReference
? 'Cadastre produto/material em Cadastros > Referência de Consumo'
: `${(need.colors.length ? need.colors.join(', ') : 'Todas as cores')} · ${(need.suppliers.length ? need.suppliers.join(', ') : 'Sem fornecedor')}`}
</p>
{need.products?.length ? (
<p className="mt-1 text-xs font-semibold text-dark-muted">
{need.products.slice(0, 2).map(product => product.productId).join(', ')}
{need.products.length > 2 ? ` +${need.products.length - 2}` : ''}
</p>
) : null}
</div>
<p className="text-sm font-bold text-dark-text">{formatNumber(need.plannedKg)} kg</p>
<p className="text-sm font-bold text-dark-text">{formatNumber(need.stockKg)} kg</p>
<p className="text-sm font-bold text-dark-text">{formatNumber(need.pendingKg)} kg</p>
<p className={`text-sm font-bold ${need.purchaseKg > 0 ? 'text-red-300' : 'text-emerald-300'}`}>{formatNumber(need.purchaseKg)} kg</p>
<p className="text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.plannedKg)}</p>
<p className="text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.stockKg)}</p>
<p className="text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.pendingKg)}</p>
<p className={`text-sm font-bold ${need.purchaseKg > 0 ? 'text-red-300' : 'text-emerald-300'}`}>{formatNeedQuantity(need, need.purchaseKg)}</p>
<span className={`w-fit rounded-full border px-2.5 py-1 text-xs font-bold ${
need.status === 'critical'
need.missingReference
? 'border-red-400/30 bg-red-400/10 text-red-300'
: need.status === 'critical'
? 'border-red-400/30 bg-red-400/10 text-red-300'
: need.status === 'attention'
? 'border-amber-400/30 bg-amber-400/10 text-amber-300'
: 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
}`}>
{purchaseStatusLabels[need.status]}
{need.missingReference ? 'Sem referência' : purchaseStatusLabels[need.status]}
</span>
</div>
))}