Refine purchase needs setup queue

This commit is contained in:
Cauê Faleiros
2026-07-21 13:56:10 -03:00
parent 51b73ed82b
commit b4e51eb8eb

View File

@@ -1781,6 +1781,26 @@ const summarizePurchaseNeeds = (needs: SupplyPurchaseNeed[]) => {
return summaries.length ? summaries.join(' + ') : '0 kg';
};
const getNeedDisplayName = (need: SupplyPurchaseNeed) => (
need.material.replace(/^Cadastrar (consumo|rendimento):\s*/i, '')
);
const getNeedPendingLabel = (need: SupplyPurchaseNeed) => {
if (!need.missingReference) return purchaseStatusLabels[need.status];
return /^Cadastrar rendimento:/i.test(need.material) ? 'Rendimento' : 'Referência';
};
const sumNeedProducts = (
need: SupplyPurchaseNeed,
field: 'suggestedQuantity' | 'quantitySold' | 'stockQuantity'
) => (
(need.products || []).reduce((total, product) => total + Number(product[field] || 0), 0)
);
const countImpactedSkus = (needs: SupplyPurchaseNeed[]) => (
new Set(needs.flatMap(need => (need.products || []).map(product => product.productId).filter(Boolean))).size
);
const PurchaseNeedsScreen = () => {
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [isLoading, setIsLoading] = useState(true);
@@ -1815,7 +1835,7 @@ const PurchaseNeedsScreen = () => {
const normalizedSearch = normalizeSearch(search);
const visibleNeeds = summary.purchaseNeeds.filter(need => (
!normalizedSearch || normalizeSearch(`${need.material} ${need.suppliers.join(' ')} ${need.colors.join(' ')}`).includes(normalizedSearch)
!normalizedSearch || normalizeSearch(`${need.material} ${getNeedDisplayName(need)} ${need.suppliers.join(' ')} ${need.colors.join(' ')} ${(need.products || []).map(product => `${product.productId} ${product.name}`).join(' ')}`).includes(normalizedSearch)
));
const totalPages = Math.ceil(visibleNeeds.length / itemsPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
@@ -1825,17 +1845,36 @@ const PurchaseNeedsScreen = () => {
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-4">
{[
const mappedNeedCount = summary.purchaseNeeds.length - missingReferenceCount;
const impactedSkuCount = countImpactedSkus(summary.purchaseNeeds);
const setupMode = missingReferenceCount > 0 && missingReferenceCount >= mappedNeedCount;
const pageTitle = setupMode ? 'Pendências para Compra' : 'Necessidade de Compra';
const pageSubtitle = setupMode
? 'Complete referências de consumo para liberar o cálculo real de compra por material.'
: 'Materiais abaixo do mínimo e necessidade projetada para compra.';
const panelTitle = setupMode ? 'Fila de cadastro para compra' : 'Necessidade por material';
const panelSubtitle = setupMode
? 'Priorize os SKUs com maior impacto e cadastre material, rendimento e unidade de consumo.'
: 'Planejado - estoque aprovado - recebimentos pendentes.';
const stats = setupMode
? [
{ label: 'Pendências', value: `${missingReferenceCount}` },
{ label: 'SKUs impactados', value: `${impactedSkuCount || missingReferenceCount}` },
{ label: 'Impacto estimado', value: suggestedPurchaseSummary },
{ label: 'Referências prontas', value: `${mappedNeedCount}` },
]
: [
{ label: 'Itens a comprar', value: `${purchaseItemCount}` },
{ label: 'Compra sugerida', value: suggestedPurchaseSummary },
{ label: 'Fornecedores', value: `${pendingSupplierCount}` },
{ label: 'Sem referência', value: `${missingReferenceCount}` },
].map(stat => (
];
return (
<div className={pageClassName}>
<Header title={pageTitle} subtitle={pageSubtitle} backTo="/supplies" />
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{stats.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>
<p className="mt-2 text-3xl font-bold text-dark-text">{stat.value}</p>
@@ -1845,19 +1884,20 @@ const PurchaseNeedsScreen = () => {
<div className={`${panelClassName} p-5`}>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-base font-bold text-dark-text">Necessidade por material</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Planejado - estoque aprovado - recebimentos pendentes.</p>
<h2 className="text-base font-bold text-dark-text">{panelTitle}</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">{panelSubtitle}</p>
</div>
<div className="flex flex-wrap gap-2">
<button type="button" onClick={loadSummary} className={buttonClassName}><RefreshCw className="h-4 w-4" /> Atualizar</button>
<button
type="button"
onClick={() => exportCsv('necessidade-compra.csv', visibleNeeds.map(need => ({
material: need.material,
planejado_kg: need.plannedKg,
estoque_kg: need.stockKg,
pendente_kg: need.pendingKg,
comprar_kg: need.purchaseKg,
onClick={() => exportCsv(setupMode ? 'pendencias-compra.csv' : 'necessidade-compra.csv', visibleNeeds.map(need => ({
material: getNeedDisplayName(need),
pendencia: need.missingReference ? getNeedPendingLabel(need) : '',
planejado: need.plannedKg,
estoque: need.stockKg,
pendente: need.pendingKg,
comprar: need.purchaseKg,
unidade: getNeedUnit(need),
prioridade: need.priority,
cobertura: need.missingReference ? 'Sem referência de consumo' : purchaseStatusLabels[need.status],
@@ -1871,6 +1911,11 @@ const PurchaseNeedsScreen = () => {
</button>
</div>
</div>
{setupMode && !isLoading && summary.purchaseNeeds.length > 0 && (
<div className="mt-4 rounded-xl border border-amber-400/20 bg-amber-400/10 px-4 py-3 text-sm font-semibold text-amber-100">
Esta fila ainda não é uma lista final de compra. Os valores mostram impacto estimado por SKU até que as referências de consumo sejam cadastradas.
</div>
)}
<label className="relative mt-4 block">
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
<input
@@ -1880,7 +1925,7 @@ const PurchaseNeedsScreen = () => {
setCurrentPage(1);
}}
className={`${inputClassName} pl-9`}
placeholder="Buscar por material, fornecedor ou cor..."
placeholder={setupMode ? 'Buscar por SKU, produto ou pendência...' : 'Buscar por material, fornecedor ou cor...'}
/>
</label>
{isLoading ? (
@@ -1891,6 +1936,74 @@ const PurchaseNeedsScreen = () => {
) : visibleNeeds.length ? (
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
<div className="overflow-x-auto">
{setupMode ? (
<table className="w-full min-w-[980px] table-fixed border-collapse">
<colgroup>
<col />
<col className="w-[120px]" />
<col className="w-[130px]" />
<col className="w-[150px]" />
<col className="w-[150px]" />
<col className="w-[140px]" />
</colgroup>
<thead className="border-b border-dark-border bg-dark-input/40 text-xs font-bold uppercase tracking-widest text-dark-muted">
<tr>
<th scope="col" className="px-4 py-3 text-left">Produto</th>
<th scope="col" className="px-4 py-3 text-right">Vendido</th>
<th scope="col" className="px-4 py-3 text-right">Estoque produto</th>
<th scope="col" className="px-4 py-3 text-right">Impacto estimado</th>
<th scope="col" className="px-4 py-3 text-left">Pendência</th>
<th scope="col" className="px-4 py-3 text-right">Ação</th>
</tr>
</thead>
<tbody className="divide-y divide-dark-border bg-dark-card">
{paginatedNeeds.map(need => {
const referenceProduct = need.products?.[0];
const productName = referenceProduct?.name || getNeedDisplayName(need);
const productId = referenceProduct?.productId || '';
const soldQuantity = referenceProduct?.quantitySold ?? sumNeedProducts(need, 'quantitySold');
const stockQuantity = referenceProduct?.stockQuantity ?? sumNeedProducts(need, 'stockQuantity');
return (
<tr key={need.material}>
<td className="px-4 py-4 align-middle">
<div className="min-w-0">
<p className="truncate text-sm font-bold text-dark-text">{productName}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">
{productId ? `SKU ${productId}` : 'SKU não informado'}
</p>
</div>
</td>
<td className="px-4 py-4 text-right align-middle text-sm font-bold text-dark-text">{formatNumber(soldQuantity, 0)} un.</td>
<td className="px-4 py-4 text-right align-middle text-sm font-bold text-dark-text">{formatNumber(stockQuantity, 0)} un.</td>
<td className="px-4 py-4 text-right align-middle text-sm font-bold text-amber-300">{formatNeedQuantity(need, need.purchaseKg)}</td>
<td className="px-4 py-4 align-middle">
<div className="flex flex-col gap-1">
<span className="w-fit rounded-full border border-amber-400/30 bg-amber-400/10 px-2.5 py-1 text-xs font-bold text-amber-300">
{getNeedPendingLabel(need)}
</span>
<span className="text-xs font-semibold text-dark-muted">Referência de consumo incompleta</span>
</div>
</td>
<td className="px-4 py-4 text-right align-middle">
{referenceProduct ? (
<RouterLink
to={buildConsumptionReferencePath({ sku: referenceProduct.productId, name: referenceProduct.name })}
className="inline-flex h-9 items-center justify-center gap-2 rounded-lg bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:bg-dark-border"
>
<Pencil className="h-4 w-4" />
Cadastrar
</RouterLink>
) : (
<span className="text-xs font-semibold text-dark-muted">Sem SKU</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
) : (
<table className="w-full min-w-[920px] table-fixed border-collapse">
<colgroup>
<col />
@@ -1917,7 +2030,7 @@ const PurchaseNeedsScreen = () => {
<tr key={need.material}>
<td className="px-4 py-4 align-middle">
<div className="min-w-0">
<p className="text-sm font-bold text-dark-text">{need.material}</p>
<p className="text-sm font-bold text-dark-text">{getNeedDisplayName(need)}</p>
<div className="mt-1 flex flex-wrap items-center gap-2">
<p className="text-xs font-semibold text-dark-muted">
{need.missingReference
@@ -1965,6 +2078,7 @@ const PurchaseNeedsScreen = () => {
})}
</tbody>
</table>
)}
</div>
<PaginationControls
totalItems={visibleNeeds.length}