Prioritize supply operations dashboard
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 46s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 46s
This commit is contained in:
@@ -28,7 +28,7 @@ import DateRangePicker from '../components/DateRangePicker';
|
|||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import ProductTypeBadge from '../components/ProductTypeBadge';
|
import ProductTypeBadge from '../components/ProductTypeBadge';
|
||||||
import { classifyCutFamily } from '../analytics/cutting';
|
import { classifyCutFamily } from '../analytics/cutting';
|
||||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchStock, fetchSupplySummary, isSuperAdmin } from '../dataService';
|
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplySummary, isSuperAdmin } from '../dataService';
|
||||||
import { parseProductName } from '../productParsing';
|
import { parseProductName } from '../productParsing';
|
||||||
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||||
import { getPlanningStock } from '../planningStock';
|
import { getPlanningStock } from '../planningStock';
|
||||||
@@ -42,6 +42,10 @@ const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shado
|
|||||||
const buttonClassName = 'inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer';
|
const buttonClassName = 'inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer';
|
||||||
const inputClassName = 'h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary';
|
const inputClassName = 'h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary';
|
||||||
const emptyStateClassName = 'flex min-h-[190px] flex-col items-center justify-center gap-2 px-4 py-10 text-center';
|
const emptyStateClassName = 'flex min-h-[190px] flex-col items-center justify-center gap-2 px-4 py-10 text-center';
|
||||||
|
const allProductionOrdersRange = { start: new Date(2000, 0, 1), end: new Date(2100, 11, 31) };
|
||||||
|
const isServicePurchaseNeed = (need: SupplyPurchaseNeed) => (
|
||||||
|
/\b(?:SERVI[CÇ]O|FRETE|TINTURARIA|TECELAGEM|COSTURA|ESTAMPARIA|LAVANDERIA)\b/i.test(need.material)
|
||||||
|
);
|
||||||
|
|
||||||
const emptySupplySummary: SupplySummary = {
|
const emptySupplySummary: SupplySummary = {
|
||||||
receipts: [],
|
receipts: [],
|
||||||
@@ -170,15 +174,19 @@ const ModuleCard = ({
|
|||||||
description,
|
description,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
to,
|
to,
|
||||||
|
meta,
|
||||||
|
priority = false,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: typeof Package;
|
icon: typeof Package;
|
||||||
to: string;
|
to: string;
|
||||||
|
meta?: string;
|
||||||
|
priority?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RouterLink to={to} className={`${panelClassName} flex min-h-32 items-start gap-4 p-5 transition-colors hover:border-brand-primary`}>
|
<RouterLink to={to} className={`${panelClassName} flex min-h-36 items-start gap-4 p-5 transition-colors hover:border-brand-primary ${priority ? 'border-brand-primary/30 bg-brand-primary/[0.035]' : ''}`}>
|
||||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-brand-primary">
|
<div className={`flex h-11 w-11 items-center justify-center rounded-xl border ${priority ? 'border-brand-primary/30 bg-brand-primary/10 text-brand-primary' : 'border-dark-border bg-dark-input text-brand-primary'}`}>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -187,6 +195,7 @@ const ModuleCard = ({
|
|||||||
<ArrowRight className="mt-0.5 h-4 w-4 shrink-0 text-dark-muted" />
|
<ArrowRight className="mt-0.5 h-4 w-4 shrink-0 text-dark-muted" />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm font-semibold text-dark-muted">{description}</p>
|
<p className="mt-2 text-sm font-semibold text-dark-muted">{description}</p>
|
||||||
|
{meta && <p className={`mt-4 text-xs font-bold ${priority ? 'text-brand-primary' : 'text-dark-text'}`}>{meta}</p>}
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
);
|
);
|
||||||
@@ -196,6 +205,29 @@ const SuppliesHub = () => {
|
|||||||
const [isExportingDiagnostic, setIsExportingDiagnostic] = useState(false);
|
const [isExportingDiagnostic, setIsExportingDiagnostic] = useState(false);
|
||||||
const [diagnosticError, setDiagnosticError] = useState('');
|
const [diagnosticError, setDiagnosticError] = useState('');
|
||||||
const canExportDiagnostic = isSuperAdmin();
|
const canExportDiagnostic = isSuperAdmin();
|
||||||
|
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||||
|
const [productionCounts, setProductionCounts] = useState({ open: 0, inProgress: 0 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
const loadHub = async () => {
|
||||||
|
const [supplySummary, productionSummary] = await Promise.all([
|
||||||
|
fetchSupplySummary(),
|
||||||
|
fetchProductionOrders(allProductionOrdersRange),
|
||||||
|
]);
|
||||||
|
if (!isMounted) return;
|
||||||
|
setSummary(supplySummary);
|
||||||
|
setProductionCounts({ open: productionSummary.counts.open || 0, inProgress: productionSummary.counts.in_progress || 0 });
|
||||||
|
};
|
||||||
|
void loadHub();
|
||||||
|
return () => { isMounted = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const materialPurchaseNeeds = summary.purchaseNeeds.filter(need => !need.missingReference && !isServicePurchaseNeed(need) && need.purchaseKg > 0);
|
||||||
|
const mappingSkuCount = new Set(summary.purchaseNeeds
|
||||||
|
.filter(need => need.missingReference)
|
||||||
|
.flatMap(need => (need.products || []).map(product => product.productId))
|
||||||
|
.filter(Boolean)).size;
|
||||||
|
|
||||||
const handleExportDiagnostic = async () => {
|
const handleExportDiagnostic = async () => {
|
||||||
setIsExportingDiagnostic(true);
|
setIsExportingDiagnostic(true);
|
||||||
@@ -230,14 +262,30 @@ const SuppliesHub = () => {
|
|||||||
{diagnosticError}
|
{diagnosticError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<section>
|
||||||
<ModuleCard title="Planejamento Operacional" description="Prioridades de reposição, compra e revisão calculadas por demanda, estoque e cobertura." icon={PackageSearch} to="/supplies/current-planning" />
|
<div className="mb-3 flex items-end justify-between gap-4">
|
||||||
<ModuleCard title="Planejamento de Corte" description="Ruptura por cor e tamanho, montagem do corte e prioridade por cobertura." icon={Scissors} to="/cutting" />
|
<div>
|
||||||
<ModuleCard title="Controle de Estoque" description="Saldo, lotes, recebimentos, inventário e movimentações." icon={Package} to="/supplies/inventory" />
|
<h2 className="text-base font-bold text-dark-text">Ações de hoje</h2>
|
||||||
<ModuleCard title="Ordens de Produção" description="Ordens geradas pelo corte e acompanhamento de produção." icon={ClipboardList} to="/production-orders" />
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Siga o fluxo: comprar materiais, definir o corte e acompanhar a produção.</p>
|
||||||
<ModuleCard title="Planejamento de Malha" description="Fila de matéria-prima para compra, recebimento e abastecimento do corte." icon={Ruler} to="/supplies/fabric-planning" />
|
</div>
|
||||||
<ModuleCard title="Necessidade de Compra" description="Itens abaixo do mínimo e necessidade projetada para compra." icon={BarChart3} to="/supplies/purchase-needs" />
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||||
|
<ModuleCard title="Comprar materiais" description="Necessidades de matéria-prima calculadas a partir da demanda e da composição." icon={BarChart3} to="/supplies/purchase-needs" priority meta={`${formatNumber(materialPurchaseNeeds.length, 0)} materiais para comprar${mappingSkuCount ? ` · ${formatNumber(mappingSkuCount, 0)} SKUs para mapear` : ''}`} />
|
||||||
|
<ModuleCard title="Planejar corte" description="Defina quais SKUs, cores e tamanhos priorizar conforme demanda e disponibilidade de material." icon={Scissors} to="/cutting" priority meta="Revise bloqueios de material antes de gerar OPs" />
|
||||||
|
<ModuleCard title="Acompanhar produção" description="Consulte ordens geradas pelo corte e acompanhe o andamento da fabricação." icon={ClipboardList} to="/production-orders" priority meta={`${formatNumber(productionCounts.open, 0)} abertas · ${formatNumber(productionCounts.inProgress, 0)} em andamento`} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="border-t border-dark-border pt-6">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Gestão de materiais</h2>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Cadastros e controles que sustentam o planejamento operacional.</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||||
|
<ModuleCard title="Estoque e recebimentos" description="Saldo, lotes, recebimentos, inventário e movimentações de materiais." icon={Package} to="/supplies/inventory" meta={`${formatNumber(summary.stats.pendingReceipts, 0)} recebimentos pendentes · ${formatNumber(summary.stats.activeLots, 0)} lotes ativos`} />
|
||||||
|
<ModuleCard title="Planejamento de malha" description="Organize a fila de matéria-prima para abastecer compras e corte." icon={Ruler} to="/supplies/fabric-planning" />
|
||||||
|
<ModuleCard title="Prioridades de estoque" description="Analise reposição, cobertura e itens que precisam de revisão de cadastro." icon={PackageSearch} to="/supplies/current-planning" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1792,10 +1840,6 @@ const getNeedPendingLabel = (need: SupplyPurchaseNeed) => {
|
|||||||
|
|
||||||
type PurchaseNeedTab = 'materials' | 'services' | 'mapping';
|
type PurchaseNeedTab = 'materials' | 'services' | 'mapping';
|
||||||
|
|
||||||
const isServicePurchaseNeed = (need: SupplyPurchaseNeed) => (
|
|
||||||
/\b(?:SERVI[CÇ]O|FRETE|TINTURARIA|TECELAGEM|COSTURA|ESTAMPARIA|LAVANDERIA)\b/i.test(need.material)
|
|
||||||
);
|
|
||||||
|
|
||||||
const sumNeedProducts = (
|
const sumNeedProducts = (
|
||||||
need: SupplyPurchaseNeed,
|
need: SupplyPurchaseNeed,
|
||||||
field: 'suggestedQuantity' | 'quantitySold' | 'stockQuantity'
|
field: 'suggestedQuantity' | 'quantitySold' | 'stockQuantity'
|
||||||
|
|||||||
Reference in New Issue
Block a user