Redesign purchase needs workflow
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 53s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 53s
This commit is contained in:
@@ -1840,13 +1840,8 @@ const getNeedPendingLabel = (need: SupplyPurchaseNeed) => {
|
|||||||
|
|
||||||
const hasNegativeMaterialBalance = (need: SupplyPurchaseNeed) => need.stockKg < 0;
|
const hasNegativeMaterialBalance = (need: SupplyPurchaseNeed) => need.stockKg < 0;
|
||||||
|
|
||||||
const getNeedStatusLabel = (need: SupplyPurchaseNeed) => {
|
|
||||||
if (hasNegativeMaterialBalance(need)) return 'Saldo negativo';
|
|
||||||
if (need.missingReference) return 'Sem referência';
|
|
||||||
return purchaseStatusLabels[need.status];
|
|
||||||
};
|
|
||||||
|
|
||||||
type PurchaseNeedTab = 'materials' | 'services' | 'mapping';
|
type PurchaseNeedTab = 'materials' | 'services' | 'mapping';
|
||||||
|
type MaterialPurchaseFilter = 'to_buy' | 'negative_balance' | 'no_stock' | 'all';
|
||||||
|
|
||||||
const sumNeedProducts = (
|
const sumNeedProducts = (
|
||||||
need: SupplyPurchaseNeed,
|
need: SupplyPurchaseNeed,
|
||||||
@@ -1859,11 +1854,107 @@ const countImpactedSkus = (needs: SupplyPurchaseNeed[]) => (
|
|||||||
new Set(needs.flatMap(need => (need.products || []).map(product => product.productId).filter(Boolean))).size
|
new Set(needs.flatMap(need => (need.products || []).map(product => product.productId).filter(Boolean))).size
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PurchaseMaterialQueue = ({ 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(300px,1.35fr)_minmax(380px,1fr)_150px]">
|
||||||
|
<span>Material e impacto</span>
|
||||||
|
<span className="hidden xl:block">Como a compra foi calculada</span>
|
||||||
|
<span className="hidden text-right xl:block">Decisão</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-dark-border">
|
||||||
|
{needs.map(need => {
|
||||||
|
const isNegative = hasNegativeMaterialBalance(need);
|
||||||
|
const isNoStock = need.stockKg === 0;
|
||||||
|
const isCovered = need.purchaseKg <= 0;
|
||||||
|
const statusTone = isNegative
|
||||||
|
? 'border-red-400/35 bg-red-400/10 text-red-300'
|
||||||
|
: isNoStock
|
||||||
|
? 'border-red-400/30 bg-red-400/10 text-red-300'
|
||||||
|
: isCovered
|
||||||
|
? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
|
||||||
|
: 'border-amber-400/30 bg-amber-400/10 text-amber-300';
|
||||||
|
const statusLabel = isNegative
|
||||||
|
? 'Saldo negativo'
|
||||||
|
: isNoStock
|
||||||
|
? 'Sem saldo'
|
||||||
|
: isCovered
|
||||||
|
? 'Coberto'
|
||||||
|
: 'Comprar';
|
||||||
|
const accent = isNegative || isNoStock ? 'border-l-red-400' : isCovered ? 'border-l-emerald-400' : 'border-l-amber-400';
|
||||||
|
const products = need.products || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article key={need.material} className={`border-l-2 ${accent} px-4 py-4 transition-colors hover:bg-dark-input/35`}>
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[minmax(300px,1.35fr)_minmax(380px,1fr)_150px] 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 ${statusTone}`}>{statusLabel}</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-3 gap-2 sm:grid-cols-4">
|
||||||
|
<div className="rounded-lg bg-dark-input/75 px-3 py-2">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Necessário</p>
|
||||||
|
<p className="mt-1 text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.plannedKg)}</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</p>
|
||||||
|
<p className={`mt-1 text-sm font-bold ${isNegative ? 'text-red-300' : 'text-dark-text'}`}>{formatNeedQuantity(need, need.stockKg)}</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">A receber</p>
|
||||||
|
<p className="mt-1 text-sm font-bold text-dark-text">{formatNeedQuantity(need, need.pendingKg)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 rounded-lg border border-brand-primary/20 bg-brand-primary/[0.06] px-3 py-2 sm:col-span-1">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-brand-primary">Comprar</p>
|
||||||
|
<p className={`mt-1 text-sm font-bold ${isCovered ? 'text-emerald-300' : 'text-brand-primary'}`}>{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 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);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState<PurchaseNeedTab>('materials');
|
const [activeTab, setActiveTab] = useState<PurchaseNeedTab>('materials');
|
||||||
|
const [materialFilter, setMaterialFilter] = useState<MaterialPurchaseFilter>('to_buy');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
@@ -1895,8 +1986,17 @@ const PurchaseNeedsScreen = () => {
|
|||||||
const materialNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need));
|
const materialNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need));
|
||||||
const serviceNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && isServicePurchaseNeed(need));
|
const serviceNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && isServicePurchaseNeed(need));
|
||||||
const mappingNeeds = summary.purchaseNeeds.filter(need => need.missingReference);
|
const mappingNeeds = summary.purchaseNeeds.filter(need => need.missingReference);
|
||||||
|
const purchaseNeeds = materialNeeds.filter(need => need.purchaseKg > 0);
|
||||||
|
const negativeBalanceNeeds = materialNeeds.filter(hasNegativeMaterialBalance);
|
||||||
|
const noStockNeeds = materialNeeds.filter(need => need.stockKg === 0 && need.purchaseKg > 0);
|
||||||
|
const filteredMaterialNeeds = materialNeeds.filter(need => {
|
||||||
|
if (materialFilter === 'to_buy') return need.purchaseKg > 0;
|
||||||
|
if (materialFilter === 'negative_balance') return hasNegativeMaterialBalance(need);
|
||||||
|
if (materialFilter === 'no_stock') return need.stockKg === 0 && need.purchaseKg > 0;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
const activeNeeds = activeTab === 'materials'
|
const activeNeeds = activeTab === 'materials'
|
||||||
? [...materialNeeds].sort((a, b) => {
|
? [...filteredMaterialNeeds].sort((a, b) => {
|
||||||
if (hasNegativeMaterialBalance(a) !== hasNegativeMaterialBalance(b)) return hasNegativeMaterialBalance(a) ? -1 : 1;
|
if (hasNegativeMaterialBalance(a) !== hasNegativeMaterialBalance(b)) return hasNegativeMaterialBalance(a) ? -1 : 1;
|
||||||
if (a.status !== b.status) return a.status === 'critical' ? -1 : b.status === 'critical' ? 1 : 0;
|
if (a.status !== b.status) return a.status === 'critical' ? -1 : b.status === 'critical' ? 1 : 0;
|
||||||
return b.purchaseKg - a.purchaseKg;
|
return b.purchaseKg - a.purchaseKg;
|
||||||
@@ -1910,34 +2010,61 @@ const PurchaseNeedsScreen = () => {
|
|||||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||||
const paginatedNeeds = visibleNeeds.slice(startIndex, startIndex + itemsPerPage);
|
const paginatedNeeds = visibleNeeds.slice(startIndex, startIndex + itemsPerPage);
|
||||||
const purchaseItemCount = materialNeeds.filter(need => need.purchaseKg > 0).length;
|
const purchaseItemCount = purchaseNeeds.length;
|
||||||
const suggestedPurchaseSummary = summarizePurchaseNeeds(materialNeeds);
|
const suggestedPurchaseSummary = summarizePurchaseNeeds(purchaseNeeds);
|
||||||
const impactedSkuCount = countImpactedSkus(mappingNeeds);
|
const impactedSkuCount = countImpactedSkus(mappingNeeds);
|
||||||
const pageTitle = 'Necessidade de Compra';
|
const pageTitle = 'Necessidade de Compra';
|
||||||
const pageSubtitle = 'Materiais, serviços terceirizados e pendências de cadastro em filas separadas.';
|
const pageSubtitle = 'Materiais, serviços terceirizados e pendências de cadastro em filas separadas.';
|
||||||
const panelTitle = activeTab === 'materials' ? 'Compra de materiais' : activeTab === 'services' ? 'Serviços terceirizados' : 'Pendências de composição';
|
const materialFilters: Array<{ id: MaterialPurchaseFilter; label: string; description: string; count: number }> = [
|
||||||
|
{ id: 'to_buy', label: 'Comprar agora', description: 'Materiais com saldo insuficiente para a demanda.', count: purchaseItemCount },
|
||||||
|
{ id: 'negative_balance', label: 'Saldo negativo', description: 'Divergências de estoque que precisam de conferência.', count: negativeBalanceNeeds.length },
|
||||||
|
{ id: 'no_stock', label: 'Sem saldo', description: 'Materiais sem saldo disponível no Tiny.', count: noStockNeeds.length },
|
||||||
|
{ id: 'all', label: 'Todos os materiais', description: 'Inclui itens cobertos para auditoria da conta.', count: materialNeeds.length },
|
||||||
|
];
|
||||||
|
const activeMaterialFilter = materialFilters.find(filter => filter.id === materialFilter);
|
||||||
|
const panelTitle = activeTab === 'materials' ? activeMaterialFilter?.label || 'Compra de materiais' : activeTab === 'services' ? 'Serviços terceirizados' : 'Pendências de composição';
|
||||||
const panelSubtitle = activeTab === 'materials'
|
const panelSubtitle = activeTab === 'materials'
|
||||||
? 'Demanda de produto → material → saldo disponível → quantidade para comprar.'
|
? activeMaterialFilter?.description || 'Demanda de produto → material → saldo disponível → quantidade para comprar.'
|
||||||
: activeTab === 'services'
|
: activeTab === 'services'
|
||||||
? 'Necessidades de beneficiamento e serviços. Não entram no total de compra de materiais.'
|
? 'Necessidades de beneficiamento e serviços. Não entram no total de compra de materiais.'
|
||||||
: 'SKUs sem consumo confiável. Corrija o cadastro antes de usar estes valores para comprar.';
|
: 'SKUs sem consumo confiável. Corrija o cadastro antes de usar estes valores para comprar.';
|
||||||
const stats = [
|
|
||||||
{ label: 'Itens para comprar', value: `${purchaseItemCount} materiais` },
|
|
||||||
{ label: 'Compra sugerida', value: suggestedPurchaseSummary },
|
|
||||||
{ label: 'Serviços a revisar', value: `${serviceNeeds.filter(need => need.purchaseKg > 0).length}` },
|
|
||||||
{ label: 'SKUs sem mapeamento', value: `${impactedSkuCount || mappingNeeds.length}` },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pageClassName}>
|
<div className={pageClassName}>
|
||||||
<Header title={pageTitle} subtitle={pageSubtitle} backTo="/supplies" />
|
<Header title={pageTitle} subtitle={pageSubtitle} backTo="/supplies" />
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
{stats.map(stat => (
|
<button
|
||||||
<div key={stat.label} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
type="button"
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">{stat.label}</p>
|
onClick={() => { setActiveTab('materials'); setMaterialFilter('to_buy'); setSearch(''); setCurrentPage(1); }}
|
||||||
<p className="mt-2 text-3xl font-bold text-dark-text">{stat.value}</p>
|
className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'materials' && materialFilter === 'to_buy' ? 'border-brand-primary/45 bg-brand-primary/[0.07]' : 'border-dark-border bg-dark-card hover:border-brand-primary/35'}`}
|
||||||
|
>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Materiais para comprar</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{purchaseItemCount}</p>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">itens com falta real</p>
|
||||||
|
</button>
|
||||||
|
<div 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">Compra sugerida</p>
|
||||||
|
<p className="mt-2 text-2xl font-bold text-brand-primary">{suggestedPurchaseSummary}</p>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">necessidade após saldo e recebimentos</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setActiveTab('materials'); setMaterialFilter('negative_balance'); setSearch(''); setCurrentPage(1); }}
|
||||||
|
className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'materials' && materialFilter === 'negative_balance' ? 'border-red-400/45 bg-red-400/[0.07]' : 'border-dark-border bg-dark-card hover:border-red-400/35'}`}
|
||||||
|
>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldos a conferir</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-red-300">{negativeBalanceNeeds.length}</p>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">materiais com estoque negativo</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setActiveTab('mapping'); setSearch(''); setCurrentPage(1); }}
|
||||||
|
className={`rounded-2xl border p-5 text-left shadow-sm transition-colors ${activeTab === 'mapping' ? 'border-sky-400/45 bg-sky-400/[0.07]' : 'border-dark-border bg-dark-card hover:border-sky-400/35'}`}
|
||||||
|
>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Dados bloqueando o plano</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-sky-300">{impactedSkuCount || mappingNeeds.length}</p>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">SKUs sem consumo mapeado</p>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${panelClassName} p-5`}>
|
<div className={`${panelClassName} p-5`}>
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
@@ -1969,7 +2096,19 @@ const PurchaseNeedsScreen = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative mt-4 block">
|
<div className="mt-5 flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||||
|
<div className="inline-flex w-fit rounded-xl border border-dark-border bg-dark-input p-1">
|
||||||
|
{[
|
||||||
|
{ id: 'materials' as const, label: `Compra (${purchaseItemCount})` },
|
||||||
|
{ id: 'services' as const, label: `Serviços (${serviceNeeds.length})` },
|
||||||
|
{ id: 'mapping' as const, label: `Mapear dados (${mappingNeeds.length})` },
|
||||||
|
].map(tab => (
|
||||||
|
<button key={tab.id} type="button" onClick={() => { setActiveTab(tab.id); setSearch(''); setCurrentPage(1); }} className={`rounded-lg px-3 py-2 text-xs font-bold transition-colors cursor-pointer ${activeTab === tab.id ? 'bg-brand-primary text-brand-contrast' : 'text-dark-muted hover:text-dark-text'}`}>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<label className="relative block w-full xl:max-w-md">
|
||||||
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
|
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
|
||||||
<input
|
<input
|
||||||
value={search}
|
value={search}
|
||||||
@@ -1981,17 +2120,22 @@ const PurchaseNeedsScreen = () => {
|
|||||||
placeholder={activeTab === 'mapping' ? 'Buscar por SKU, produto ou pendência...' : activeTab === 'services' ? 'Buscar por serviço ou SKU impactado...' : 'Buscar por material, fornecedor ou cor...'}
|
placeholder={activeTab === 'mapping' ? 'Buscar por SKU, produto ou pendência...' : activeTab === 'services' ? 'Buscar por serviço ou SKU impactado...' : 'Buscar por material, fornecedor ou cor...'}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-4 inline-flex rounded-xl border border-dark-border bg-dark-input p-1">
|
</div>
|
||||||
{[
|
{activeTab === 'materials' && (
|
||||||
{ id: 'materials' as const, label: `Materiais (${materialNeeds.length} linhas)` },
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
{ id: 'services' as const, label: `Serviços (${serviceNeeds.length})` },
|
{materialFilters.map(filter => (
|
||||||
{ id: 'mapping' as const, label: `Mapear dados (${impactedSkuCount || mappingNeeds.length} SKUs)` },
|
<button
|
||||||
].map(tab => (
|
key={filter.id}
|
||||||
<button key={tab.id} type="button" onClick={() => { setActiveTab(tab.id); setSearch(''); setCurrentPage(1); }} className={`rounded-lg px-3 py-2 text-xs font-bold transition-colors cursor-pointer ${activeTab === tab.id ? 'bg-brand-primary text-brand-contrast' : 'text-dark-muted hover:text-dark-text'}`}>
|
type="button"
|
||||||
{tab.label}
|
onClick={() => { setMaterialFilter(filter.id); setCurrentPage(1); }}
|
||||||
|
className={`rounded-lg border px-3 py-2 text-left text-xs font-bold transition-colors ${materialFilter === filter.id ? 'border-brand-primary/40 bg-brand-primary/10 text-brand-primary' : 'border-dark-border bg-dark-input text-dark-muted hover:border-brand-primary/35 hover:text-dark-text'}`}
|
||||||
|
title={filter.description}
|
||||||
|
>
|
||||||
|
{filter.label} <span className="ml-1 opacity-75">{filter.count}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className={emptyStateClassName}>
|
<div className={emptyStateClassName}>
|
||||||
<BarChart3 className="h-8 w-8 text-brand-primary" />
|
<BarChart3 className="h-8 w-8 text-brand-primary" />
|
||||||
@@ -2000,7 +2144,9 @@ const PurchaseNeedsScreen = () => {
|
|||||||
) : visibleNeeds.length ? (
|
) : visibleNeeds.length ? (
|
||||||
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
|
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{activeTab === 'mapping' ? (
|
{activeTab === 'materials' ? (
|
||||||
|
<PurchaseMaterialQueue needs={paginatedNeeds} />
|
||||||
|
) : activeTab === 'mapping' ? (
|
||||||
<table className="w-full min-w-[880px] table-fixed border-collapse">
|
<table className="w-full min-w-[880px] table-fixed border-collapse">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col className="w-[34%]" />
|
<col className="w-[34%]" />
|
||||||
@@ -2079,11 +2225,11 @@ const PurchaseNeedsScreen = () => {
|
|||||||
<thead className="border-b border-dark-border bg-dark-input/40 text-xs font-bold uppercase tracking-widest text-dark-muted">
|
<thead className="border-b border-dark-border bg-dark-input/40 text-xs font-bold uppercase tracking-widest text-dark-muted">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-4 py-3 text-left">Demanda</th>
|
<th scope="col" className="px-4 py-3 text-left">Demanda</th>
|
||||||
<th scope="col" className="px-4 py-3 text-left">{activeTab === 'services' ? 'Serviço' : 'Material'}</th>
|
<th scope="col" className="px-4 py-3 text-left">Serviço</th>
|
||||||
<th scope="col" className="px-4 py-3 text-right">{activeTab === 'services' ? 'Volume previsto' : 'Necessário'}</th>
|
<th scope="col" className="px-4 py-3 text-right">Volume previsto</th>
|
||||||
<th scope="col" className="px-4 py-3 text-right">{activeTab === 'services' ? 'Agendado' : 'Estoque atual'}</th>
|
<th scope="col" className="px-4 py-3 text-right">Agendado</th>
|
||||||
<th scope="col" className="px-4 py-3 text-right">{activeTab === 'services' ? 'Em aberto' : 'Pendente'}</th>
|
<th scope="col" className="px-4 py-3 text-right">Em aberto</th>
|
||||||
<th scope="col" className="px-4 py-3 text-right">{activeTab === 'services' ? 'Contratar' : 'Comprar'}</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>
|
<th scope="col" className="px-4 py-3 text-left">Situação</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -2127,24 +2273,14 @@ const PurchaseNeedsScreen = () => {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</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">{formatNeedQuantity(need, need.plannedKg)}</td>
|
||||||
<td className={`px-4 py-4 text-right align-middle text-sm font-bold ${hasNegativeMaterialBalance(need) ? 'text-red-300' : 'text-dark-text'}`}>{activeTab === 'services' ? '—' : formatNeedQuantity(need, need.stockKg)}</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">{activeTab === 'services' ? '—' : formatNeedQuantity(need, need.pendingKg)}</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 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">
|
<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 ${
|
<span className={`inline-flex whitespace-nowrap rounded-full border px-2.5 py-1 text-xs font-bold ${
|
||||||
activeTab === 'services'
|
'border-sky-400/30 bg-sky-400/10 text-sky-300'
|
||||||
? 'border-sky-400/30 bg-sky-400/10 text-sky-300'
|
|
||||||
: hasNegativeMaterialBalance(need)
|
|
||||||
? 'border-red-400/30 bg-red-400/10 text-red-300'
|
|
||||||
: 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'
|
|
||||||
}`}>
|
}`}>
|
||||||
{activeTab === 'services' ? 'Programar serviço' : getNeedStatusLabel(need)}
|
Programar serviço
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -2160,7 +2296,7 @@ const PurchaseNeedsScreen = () => {
|
|||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
pageSize={itemsPerPage}
|
pageSize={itemsPerPage}
|
||||||
pageSizeOptions={[10, 20, 50, 100]}
|
pageSizeOptions={[10, 20, 50, 100]}
|
||||||
itemLabel="materiais"
|
itemLabel={activeTab === 'materials' ? 'materiais' : activeTab === 'services' ? 'serviços' : 'pendências'}
|
||||||
pageSizeLabel="itens por página"
|
pageSizeLabel="itens por página"
|
||||||
startIndex={startIndex}
|
startIndex={startIndex}
|
||||||
endIndex={Math.min(startIndex + itemsPerPage, visibleNeeds.length)}
|
endIndex={Math.min(startIndex + itemsPerPage, visibleNeeds.length)}
|
||||||
|
|||||||
Reference in New Issue
Block a user