Unify supply service and mapping queues
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s

This commit is contained in:
Cauê Faleiros
2026-08-03 14:42:14 -03:00
parent 2d738073bf
commit f18e7c4845

View File

@@ -1949,6 +1949,141 @@ const PurchaseMaterialQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
</>
);
const PurchaseServiceQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
<>
<div className="border-b border-dark-border bg-dark-input/40 px-4 py-3">
<div className="grid gap-3 text-[10px] font-bold uppercase tracking-widest text-dark-muted xl:grid-cols-[minmax(320px,1.4fr)_minmax(260px,1fr)_180px]">
<span>Serviço e impacto</span>
<span className="hidden xl:block">Volume a programar</span>
<span className="hidden text-right xl:block">Próximo passo</span>
</div>
</div>
<div className="divide-y divide-dark-border">
{needs.map(need => {
const products = need.products || [];
const isCovered = need.purchaseKg <= 0;
return (
<article key={need.material} className={`border-l-2 ${isCovered ? 'border-l-emerald-400' : 'border-l-sky-400'} px-4 py-4 transition-colors hover:bg-dark-input/35`}>
<div className="grid gap-4 xl:grid-cols-[minmax(320px,1.4fr)_minmax(260px,1fr)_180px] xl:items-center">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-bold text-dark-text">{getNeedDisplayName(need)}</h3>
<span className={`inline-flex rounded-full border px-2 py-0.5 text-[10px] font-bold ${isCovered ? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300' : 'border-sky-400/30 bg-sky-400/10 text-sky-300'}`}>
{isCovered ? 'Sem programação' : 'Programar serviço'}
</span>
</div>
<p className="mt-1 text-xs font-semibold text-dark-muted">
{need.colors.length ? need.colors.join(', ') : 'Todas as cores'}
<span className="px-1.5 text-dark-border"></span>
{need.suppliers.length ? need.suppliers.join(', ') : 'Fornecedor não informado'}
</p>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-dark-input/75 px-3 py-2">
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Volume previsto</p>
<p className="mt-1 text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.plannedKg)}</p>
</div>
<div className="rounded-lg border border-sky-400/20 bg-sky-400/[0.06] px-3 py-2">
<p className="text-[10px] font-bold uppercase tracking-wider text-sky-300">A programar</p>
<p className={`mt-1 text-sm font-bold ${isCovered ? 'text-emerald-300' : 'text-sky-300'}`}>{formatNeedQuantity(need, need.purchaseKg)}</p>
</div>
</div>
<div className="xl:text-right">
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Impacto</p>
<p className="mt-1 text-sm font-bold text-dark-text">{products.length} {products.length === 1 ? 'SKU' : 'SKUs'} dependem</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(sumNeedProducts(need, 'suggestedQuantity'))} un. planejadas</p>
</div>
</div>
{products.length > 0 && (
<div className="mt-3 flex flex-wrap items-center gap-2 border-t border-dark-border/70 pt-3">
<span className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Produtos afetados</span>
{products.slice(0, 3).map(product => (
<RouterLink key={product.productId} to={`/products/${product.productId}`} className="max-w-full truncate rounded-md border border-dark-border bg-dark-input px-2 py-1 text-[11px] font-bold text-dark-muted transition-colors hover:border-brand-primary hover:text-brand-primary" title={product.name}>
#{product.productId} · {product.name}
</RouterLink>
))}
{products.length > 3 && <span className="text-xs font-bold text-dark-muted">+{products.length - 3} SKUs</span>}
</div>
)}
</article>
);
})}
</div>
</>
);
const PurchaseMappingQueue = ({ needs }: { needs: SupplyPurchaseNeed[] }) => (
<>
<div className="border-b border-dark-border bg-dark-input/40 px-4 py-3">
<div className="grid gap-3 text-[10px] font-bold uppercase tracking-widest text-dark-muted xl:grid-cols-[minmax(330px,1.35fr)_minmax(380px,1fr)_180px]">
<span>SKU sem composição confiável</span>
<span className="hidden xl:block">Impacto antes de comprar</span>
<span className="hidden text-right xl:block">Correção</span>
</div>
</div>
<div className="divide-y divide-dark-border">
{needs.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 (
<article key={need.material} className="border-l-2 border-l-sky-400 px-4 py-4 transition-colors hover:bg-dark-input/35">
<div className="grid gap-4 xl:grid-cols-[minmax(330px,1.35fr)_minmax(380px,1fr)_180px] xl:items-center">
<div className="min-w-0">
{productId ? (
<RouterLink to={`/products/${productId}`} className="block truncate text-sm font-bold text-dark-text transition-colors hover:text-brand-primary" title={productName}>{productName}</RouterLink>
) : (
<h3 className="truncate text-sm font-bold text-dark-text">{productName}</h3>
)}
<div className="mt-1 flex flex-wrap items-center gap-2">
<span className="font-mono text-xs font-semibold text-dark-muted">{productId ? `SKU ${productId}` : 'SKU não informado'}</span>
<span className="inline-flex rounded-full border border-sky-400/30 bg-sky-400/10 px-2 py-0.5 text-[10px] font-bold text-sky-300">{getNeedPendingLabel(need)}</span>
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="rounded-lg bg-dark-input/75 px-3 py-2">
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Vendido</p>
<p className="mt-1 text-sm font-bold text-dark-text">{formatNumber(soldQuantity, 0)} un.</p>
</div>
<div className="rounded-lg bg-dark-input/75 px-3 py-2">
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Estoque produto</p>
<p className={`mt-1 text-sm font-bold ${stockQuantity < 0 ? 'text-red-300' : 'text-dark-text'}`}>{formatNumber(stockQuantity, 0)} un.</p>
</div>
<div className="rounded-lg border border-amber-400/20 bg-amber-400/[0.06] px-3 py-2">
<p className="text-[10px] font-bold uppercase tracking-wider text-amber-300">Necessidade estimada</p>
<p className="mt-1 text-sm font-bold text-amber-300">{formatNeedQuantity(need, need.purchaseKg)}</p>
</div>
</div>
<div className="xl:text-right">
{referenceProduct ? (
<RouterLink
to={buildConsumptionReferencePath({ sku: referenceProduct.productId, name: referenceProduct.name })}
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-3 text-xs font-bold text-sky-300 transition-colors hover:bg-sky-400/20"
>
<Pencil className="h-3.5 w-3.5" />
Mapear consumo
</RouterLink>
) : (
<span className="text-xs font-bold text-dark-muted">SKU necessário para corrigir</span>
)}
</div>
</div>
</article>
);
})}
</div>
</>
);
const PurchaseNeedsScreen = () => {
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [isLoading, setIsLoading] = useState(true);
@@ -2147,147 +2282,9 @@ const PurchaseNeedsScreen = () => {
{activeTab === 'materials' ? (
<PurchaseMaterialQueue needs={paginatedNeeds} />
) : activeTab === 'mapping' ? (
<table className="w-full min-w-[880px] table-fixed border-collapse">
<colgroup>
<col className="w-[34%]" />
<col className="w-[12%]" />
<col className="w-[14%]" />
<col className="w-[15%]" />
<col className="w-[15%]" />
<col className="w-[10%]" />
</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-right">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 text-right align-middle">
<span className="inline-flex w-fit rounded-full border border-sky-400/30 bg-sky-400/10 px-2.5 py-1 text-xs font-bold text-sky-300">
{getNeedPendingLabel(need)}
</span>
</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 w-9 items-center justify-center rounded-lg bg-dark-input text-dark-text transition-colors hover:bg-dark-border"
title={`Cadastrar referência do SKU ${referenceProduct.productId}`}
aria-label={`Cadastrar referência do SKU ${referenceProduct.productId}`}
>
<Pencil className="h-4 w-4" />
</RouterLink>
<PurchaseMappingQueue needs={paginatedNeeds} />
) : (
<span className="text-xs font-semibold text-dark-muted">Sem SKU</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
) : (
<table className="w-full min-w-[980px] table-fixed border-collapse">
<colgroup>
<col className="w-[210px]" />
<col />
<col className="w-[120px]" />
<col className="w-[120px]" />
<col className="w-[120px]" />
<col className="w-[120px]" />
<col className="w-[150px]" />
</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">Demanda</th>
<th scope="col" className="px-4 py-3 text-left">Serviço</th>
<th scope="col" className="px-4 py-3 text-right">Volume previsto</th>
<th scope="col" className="px-4 py-3 text-right">Agendado</th>
<th scope="col" className="px-4 py-3 text-right">Em aberto</th>
<th scope="col" className="px-4 py-3 text-right">Contratar</th>
<th scope="col" className="px-4 py-3 text-left">Situação</th>
</tr>
</thead>
<tbody className="divide-y divide-dark-border bg-dark-card">
{paginatedNeeds.map(need => {
const referenceProduct = need.products?.[0];
return (
<tr key={need.material}>
<td className="px-4 py-4 align-middle">
{need.products?.length ? (
<div>
<p className="text-sm font-bold text-dark-text">{need.products.length} {need.products.length === 1 ? 'SKU impactado' : 'SKUs impactados'}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">Demanda: {formatNumber(sumNeedProducts(need, 'suggestedQuantity'))} un.</p>
<p className="mt-1 flex flex-wrap gap-x-1.5 gap-y-1 font-mono text-[10px] text-dark-muted">
{need.products.slice(0, 3).map(product => <RouterLink key={product.productId} to={`/products/${product.productId}`} className="hover:text-brand-primary hover:underline">#{product.productId}</RouterLink>)}
{need.products.length > 3 && <span>+{need.products.length - 3}</span>}
</p>
</div>
) : <span className="text-xs font-semibold text-dark-muted">Plano manual</span>}
</td>
<td className="px-4 py-4 align-middle">
<div className="min-w-0">
<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
? '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.missingReference && referenceProduct ? (
<RouterLink
to={buildConsumptionReferencePath({ sku: referenceProduct.productId, name: referenceProduct.name })}
className="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-dark-input text-dark-text transition-colors hover:bg-dark-border"
title={`Cadastrar referência do SKU ${referenceProduct.productId}`}
aria-label={`Cadastrar referência do SKU ${referenceProduct.productId}`}
>
<Pencil className="h-3.5 w-3.5" />
</RouterLink>
) : null}
</div>
</div>
</td>
<td className="px-4 py-4 text-right align-middle text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.plannedKg)}</td>
<td className="px-4 py-4 text-right align-middle text-sm font-bold text-dark-text"></td>
<td className="px-4 py-4 text-right align-middle text-sm font-bold text-dark-text"></td>
<td className={`px-4 py-4 text-right align-middle text-sm font-bold ${need.purchaseKg > 0 ? 'text-amber-300' : 'text-emerald-300'}`}>{formatNeedQuantity(need, need.purchaseKg)}</td>
<td className="px-4 py-4 align-middle">
<span className={`inline-flex whitespace-nowrap rounded-full border px-2.5 py-1 text-xs font-bold ${
'border-sky-400/30 bg-sky-400/10 text-sky-300'
}`}>
Programar serviço
</span>
</td>
</tr>
);
})}
</tbody>
</table>
<PurchaseServiceQueue needs={paginatedNeeds} />
)}
</div>
<PaginationControls