Clarify supply execution states
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m47s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m47s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
||||||
import { AlertTriangle, ArrowLeft, ClipboardList, Download, Eye, Layers3, Palette, Pencil, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react';
|
import { AlertTriangle, ArrowLeft, ClipboardList, Download, Eye, Layers3, Pencil, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import ProductColorBadge from '../components/ProductColorBadge';
|
import ProductColorBadge from '../components/ProductColorBadge';
|
||||||
@@ -327,6 +327,24 @@ const Cutting = () => {
|
|||||||
filteredRows.filter(row => row.suggestedCutQuantity > 0 && materialReadinessByProductId.get(row.id)?.status === 'ready')
|
filteredRows.filter(row => row.suggestedCutQuantity > 0 && materialReadinessByProductId.get(row.id)?.status === 'ready')
|
||||||
), [filteredRows, materialReadinessByProductId]);
|
), [filteredRows, materialReadinessByProductId]);
|
||||||
|
|
||||||
|
const cutExecutionSummary = useMemo(() => {
|
||||||
|
const rowsWithNeed = cutPlan.needRows.filter(row => row.suggestedCutQuantity > 0);
|
||||||
|
const readyRows = rowsWithNeed.filter(row => materialReadinessByProductId.get(row.id)?.status === 'ready');
|
||||||
|
const blockedByMaterial = rowsWithNeed.filter(row => {
|
||||||
|
const status = materialReadinessByProductId.get(row.id)?.status;
|
||||||
|
return status === 'blocked' || status === 'missing';
|
||||||
|
});
|
||||||
|
const blockedByData = rowsWithNeed.filter(row => materialReadinessByProductId.get(row.id)?.status === 'material_ready');
|
||||||
|
return {
|
||||||
|
rawUnits: rowsWithNeed.reduce((total, row) => total + row.suggestedCutQuantity, 0),
|
||||||
|
readyUnits: readyRows.reduce((total, row) => total + row.suggestedCutQuantity, 0),
|
||||||
|
readyRows: readyRows.length,
|
||||||
|
blockedRows: blockedByMaterial.length + blockedByData.length,
|
||||||
|
blockedByMaterial: blockedByMaterial.length,
|
||||||
|
blockedByData: blockedByData.length,
|
||||||
|
};
|
||||||
|
}, [cutPlan.needRows, materialReadinessByProductId]);
|
||||||
|
|
||||||
const orderPreviewTotalUnits = useMemo(() => (
|
const orderPreviewTotalUnits = useMemo(() => (
|
||||||
rowsAvailableForOrder.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
rowsAvailableForOrder.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
||||||
), [rowsAvailableForOrder]);
|
), [rowsAvailableForOrder]);
|
||||||
@@ -1007,8 +1025,8 @@ const Cutting = () => {
|
|||||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Unidades a cortar</p>
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Necessidade bruta</p>
|
||||||
<p className="mt-2 text-3xl font-bold text-red-300">{formatNumber(cutPlan.summary.suggestedCutQuantity)}</p>
|
<p className="mt-2 text-3xl font-bold text-red-300">{formatNumber(cutExecutionSummary.rawUnits)}</p>
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(cutPlan.summary.skuCount)} SKUs com necessidade</p>
|
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(cutPlan.summary.skuCount)} SKUs com necessidade</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-red-400/25 bg-red-400/10 p-3 text-red-300">
|
<div className="rounded-xl border border-red-400/25 bg-red-400/10 p-3 text-red-300">
|
||||||
@@ -1020,11 +1038,9 @@ const Cutting = () => {
|
|||||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Famílias</p>
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Pronto para corte</p>
|
||||||
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(cutPlan.summary.familiesWithNeed)}</p>
|
<p className="mt-2 text-3xl font-bold text-emerald-300">{formatNumber(cutExecutionSummary.readyUnits)}</p>
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(cutExecutionSummary.readyRows)} SKUs liberados para gerar OP</p>
|
||||||
{cutPlan.summary.estimatedRolls === null ? 'Rolos pendentes de rendimento' : `${formatNumber(cutPlan.summary.estimatedRolls)} rolos estimados`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-brand-primary/25 bg-brand-primary/10 p-3 text-brand-primary">
|
<div className="rounded-xl border border-brand-primary/25 bg-brand-primary/10 p-3 text-brand-primary">
|
||||||
<Layers3 className="h-5 w-5" />
|
<Layers3 className="h-5 w-5" />
|
||||||
@@ -1035,14 +1051,12 @@ const Cutting = () => {
|
|||||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Cores / tamanhos</p>
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Bloqueios</p>
|
||||||
<p className="mt-2 text-3xl font-bold text-sky-300">
|
<p className="mt-2 text-3xl font-bold text-amber-300">{formatNumber(cutExecutionSummary.blockedRows)}</p>
|
||||||
{formatNumber(cutPlan.summary.colorsWithNeed)} / {formatNumber(cutPlan.summary.sizesWithNeed)}
|
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(cutExecutionSummary.blockedByMaterial)} material · {formatNumber(cutExecutionSummary.blockedByData)} dados</p>
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">Com corte sugerido</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-sky-400/25 bg-sky-400/10 p-3 text-sky-300">
|
<div className="rounded-xl border border-amber-400/25 bg-amber-400/10 p-3 text-amber-300">
|
||||||
<Palette className="h-5 w-5" />
|
<AlertTriangle className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1050,17 +1064,27 @@ const Cutting = () => {
|
|||||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">OP aberta</p>
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Famílias</p>
|
||||||
<p className="mt-2 text-3xl font-bold text-amber-300">{formatNumber(cutPlan.summary.openProductionQuantity)}</p>
|
<p className="mt-2 text-3xl font-bold text-brand-primary">{formatNumber(cutPlan.summary.familiesWithNeed)}</p>
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">Unidades abatidas quando há match</p>
|
<p className="mt-1 text-xs font-semibold text-dark-muted">{cutPlan.summary.estimatedRolls === null ? 'Rendimento ainda não configurado' : `${formatNumber(cutPlan.summary.estimatedRolls)} rolos estimados`}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-amber-400/25 bg-amber-400/10 p-3 text-amber-300">
|
<div className="rounded-xl border border-brand-primary/25 bg-brand-primary/10 p-3 text-brand-primary">
|
||||||
<ClipboardList className="h-5 w-5" />
|
<Layers3 className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{cutExecutionSummary.rawUnits > 0 && cutExecutionSummary.readyUnits === 0 && (
|
||||||
|
<div className="flex flex-col gap-3 rounded-2xl border border-amber-400/30 bg-amber-400/10 p-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-amber-300">Há demanda, mas nenhum SKU está liberado para corte.</p>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">Corrija materiais bloqueados e rendimentos antes de gerar ordens de produção.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={() => setActiveCuttingView('issues')} className="h-9 rounded-lg border border-amber-400/30 bg-dark-card px-3 text-xs font-bold text-amber-300 hover:border-amber-400 cursor-pointer">Ver pendências</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-2 shadow-sm">
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-2 shadow-sm">
|
||||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||||
<button
|
<button
|
||||||
@@ -1072,11 +1096,11 @@ const Cutting = () => {
|
|||||||
<span className="flex items-center gap-3">
|
<span className="flex items-center gap-3">
|
||||||
<Scissors className="h-4 w-4" />
|
<Scissors className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
<span className="block text-sm font-bold">Plano</span>
|
<span className="block text-sm font-bold">Necessidades</span>
|
||||||
<span className="block text-xs font-semibold opacity-80">SKUs para cortar</span>
|
<span className="block text-xs font-semibold opacity-80">Demanda por SKU</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-bold">{formatNumber(rowsAvailableForOrder.length)}</span>
|
<span className="text-sm font-bold">{formatNumber(cutPlan.summary.skuCount)}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ const ProductionOrders = () => {
|
|||||||
const progressCount = summary.counts.in_progress || 0;
|
const progressCount = summary.counts.in_progress || 0;
|
||||||
const finishedCount = summary.counts.finished || 0;
|
const finishedCount = summary.counts.finished || 0;
|
||||||
const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0);
|
const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0);
|
||||||
|
const activeOrders = summary.orders.filter(order => order.status !== 'finished' && order.status !== 'canceled');
|
||||||
|
|
||||||
const handleManualRefresh = () => {
|
const handleManualRefresh = () => {
|
||||||
void loadProductionOrders({ force: true });
|
void loadProductionOrders({ force: true });
|
||||||
@@ -352,6 +353,7 @@ const ProductionOrders = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeOrders.length > 0 && (
|
||||||
<form onSubmit={handleProductionExit} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
<form onSubmit={handleProductionExit} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -373,7 +375,7 @@ const ProductionOrders = () => {
|
|||||||
className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary"
|
className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary"
|
||||||
>
|
>
|
||||||
<option value="">Selecione...</option>
|
<option value="">Selecione...</option>
|
||||||
{summary.orders.filter(order => order.status !== 'finished' && order.status !== 'canceled').map(order => (
|
{activeOrders.map(order => (
|
||||||
<option key={order.id} value={order.id}>
|
<option key={order.id} value={order.id}>
|
||||||
{order.number || `#${order.id}`} · {order.productDescription}
|
{order.number || `#${order.id}`} · {order.productDescription}
|
||||||
</option>
|
</option>
|
||||||
@@ -429,6 +431,7 @@ const ProductionOrders = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
||||||
<div className="border-b border-dark-border p-5">
|
<div className="border-b border-dark-border p-5">
|
||||||
@@ -655,6 +658,13 @@ const ProductionOrders = () => {
|
|||||||
<p className="mt-1 max-w-md text-sm font-medium text-dark-muted">
|
<p className="mt-1 max-w-md text-sm font-medium text-dark-muted">
|
||||||
Gere ordens pelo Plano de Corte para acompanhar status, produto, quantidade e baixa de material dentro do Graphs.
|
Gere ordens pelo Plano de Corte para acompanhar status, produto, quantidade e baixa de material dentro do Graphs.
|
||||||
</p>
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/cutting"
|
||||||
|
className="mt-5 inline-flex h-10 items-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-4 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20"
|
||||||
|
>
|
||||||
|
<ClipboardList className="h-4 w-4" />
|
||||||
|
Ir para Plano de Corte
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1838,6 +1838,14 @@ const getNeedPendingLabel = (need: SupplyPurchaseNeed) => {
|
|||||||
return /^Cadastrar rendimento:/i.test(need.material) ? 'Rendimento' : 'Referência';
|
return /^Cadastrar rendimento:/i.test(need.material) ? 'Rendimento' : 'Referência';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
const sumNeedProducts = (
|
const sumNeedProducts = (
|
||||||
@@ -1887,7 +1895,13 @@ 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 activeNeeds = activeTab === 'materials' ? materialNeeds : activeTab === 'services' ? serviceNeeds : mappingNeeds;
|
const activeNeeds = activeTab === 'materials'
|
||||||
|
? [...materialNeeds].sort((a, b) => {
|
||||||
|
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;
|
||||||
|
return b.purchaseKg - a.purchaseKg;
|
||||||
|
})
|
||||||
|
: activeTab === 'services' ? serviceNeeds : mappingNeeds;
|
||||||
const normalizedSearch = normalizeSearch(search);
|
const normalizedSearch = normalizeSearch(search);
|
||||||
const visibleNeeds = activeNeeds.filter(need => (
|
const visibleNeeds = activeNeeds.filter(need => (
|
||||||
!normalizedSearch || normalizeSearch(`${need.material} ${getNeedDisplayName(need)} ${need.suppliers.join(' ')} ${need.colors.join(' ')} ${(need.products || []).map(product => `${product.productId} ${product.name}`).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)
|
||||||
@@ -1908,7 +1922,7 @@ const PurchaseNeedsScreen = () => {
|
|||||||
? '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 = [
|
const stats = [
|
||||||
{ label: 'Materiais a comprar', value: `${purchaseItemCount}` },
|
{ label: 'Itens para comprar', value: `${purchaseItemCount} materiais` },
|
||||||
{ label: 'Compra sugerida', value: suggestedPurchaseSummary },
|
{ label: 'Compra sugerida', value: suggestedPurchaseSummary },
|
||||||
{ label: 'Serviços a revisar', value: `${serviceNeeds.filter(need => need.purchaseKg > 0).length}` },
|
{ label: 'Serviços a revisar', value: `${serviceNeeds.filter(need => need.purchaseKg > 0).length}` },
|
||||||
{ label: 'SKUs sem mapeamento', value: `${impactedSkuCount || mappingNeeds.length}` },
|
{ label: 'SKUs sem mapeamento', value: `${impactedSkuCount || mappingNeeds.length}` },
|
||||||
@@ -1969,9 +1983,9 @@ const PurchaseNeedsScreen = () => {
|
|||||||
</label>
|
</label>
|
||||||
<div className="mt-4 inline-flex rounded-xl border border-dark-border bg-dark-input p-1">
|
<div className="mt-4 inline-flex rounded-xl border border-dark-border bg-dark-input p-1">
|
||||||
{[
|
{[
|
||||||
{ id: 'materials' as const, label: `Materiais (${materialNeeds.length})` },
|
{ id: 'materials' as const, label: `Materiais (${materialNeeds.length} linhas)` },
|
||||||
{ id: 'services' as const, label: `Serviços (${serviceNeeds.length})` },
|
{ id: 'services' as const, label: `Serviços (${serviceNeeds.length})` },
|
||||||
{ id: 'mapping' as const, label: `Mapear dados (${mappingNeeds.length})` },
|
{ id: 'mapping' as const, label: `Mapear dados (${impactedSkuCount || mappingNeeds.length} SKUs)` },
|
||||||
].map(tab => (
|
].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'}`}>
|
<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}
|
{tab.label}
|
||||||
@@ -2070,7 +2084,7 @@ const PurchaseNeedsScreen = () => {
|
|||||||
<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">{activeTab === 'services' ? 'Agendado' : 'Estoque atual'}</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">{activeTab === 'services' ? 'Em aberto' : 'Pendente'}</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">{activeTab === 'services' ? 'Contratar' : 'Comprar'}</th>
|
||||||
<th scope="col" className="px-4 py-3 text-left">{activeTab === 'services' ? 'Ação' : 'Cobertura'}</th>
|
<th scope="col" className="px-4 py-3 text-left">Situação</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-dark-border bg-dark-card">
|
<tbody className="divide-y divide-dark-border bg-dark-card">
|
||||||
@@ -2083,7 +2097,10 @@ const PurchaseNeedsScreen = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-dark-text">{need.products.length} {need.products.length === 1 ? 'SKU impactado' : 'SKUs impactados'}</p>
|
<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 text-xs font-semibold text-dark-muted">Demanda: {formatNumber(sumNeedProducts(need, 'suggestedQuantity'))} un.</p>
|
||||||
<p className="mt-1 truncate font-mono text-[10px] text-dark-muted" title={need.products.map(product => product.productId).join(', ')}>{need.products.slice(0, 3).map(product => product.productId).join(', ')}{need.products.length > 3 ? ` +${need.products.length - 3}` : ''}</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>
|
</div>
|
||||||
) : <span className="text-xs font-semibold text-dark-muted">Plano manual</span>}
|
) : <span className="text-xs font-semibold text-dark-muted">Plano manual</span>}
|
||||||
</td>
|
</td>
|
||||||
@@ -2110,13 +2127,15 @@ 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 text-dark-text">{activeTab === 'services' ? '—' : formatNeedQuantity(need, need.stockKg)}</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">{activeTab === 'services' ? '—' : formatNeedQuantity(need, need.pendingKg)}</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 ${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'
|
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
|
: need.missingReference
|
||||||
? 'border-red-400/30 bg-red-400/10 text-red-300'
|
? 'border-red-400/30 bg-red-400/10 text-red-300'
|
||||||
: need.status === 'critical'
|
: need.status === 'critical'
|
||||||
@@ -2125,7 +2144,7 @@ const PurchaseNeedsScreen = () => {
|
|||||||
? 'border-amber-400/30 bg-amber-400/10 text-amber-300'
|
? 'border-amber-400/30 bg-amber-400/10 text-amber-300'
|
||||||
: 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
|
: 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
|
||||||
}`}>
|
}`}>
|
||||||
{activeTab === 'services' ? 'Programar serviço' : need.missingReference ? 'Sem referência' : purchaseStatusLabels[need.status]}
|
{activeTab === 'services' ? 'Programar serviço' : getNeedStatusLabel(need)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user