Unify supply service and mapping queues
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
This commit is contained in:
@@ -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 PurchaseNeedsScreen = () => {
|
||||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -2147,147 +2282,9 @@ const PurchaseNeedsScreen = () => {
|
|||||||
{activeTab === 'materials' ? (
|
{activeTab === 'materials' ? (
|
||||||
<PurchaseMaterialQueue needs={paginatedNeeds} />
|
<PurchaseMaterialQueue needs={paginatedNeeds} />
|
||||||
) : activeTab === 'mapping' ? (
|
) : activeTab === 'mapping' ? (
|
||||||
<table className="w-full min-w-[880px] table-fixed border-collapse">
|
<PurchaseMappingQueue needs={paginatedNeeds} />
|
||||||
<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>
|
|
||||||
) : (
|
|
||||||
<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">
|
<PurchaseServiceQueue needs={paginatedNeeds} />
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<PaginationControls
|
<PaginationControls
|
||||||
|
|||||||
Reference in New Issue
Block a user