diff --git a/backend/db.js b/backend/db.js index f2aff01..b1fda97 100644 --- a/backend/db.js +++ b/backend/db.js @@ -236,6 +236,20 @@ const initDB = async () => { ); `); + await pool.query(` + CREATE TABLE IF NOT EXISTS supply_fabric_plans ( + id SERIAL PRIMARY KEY, + material TEXT NOT NULL, + color VARCHAR(120), + quantity_kg NUMERIC(14, 4) NOT NULL, + supplier TEXT, + priority VARCHAR(40) NOT NULL DEFAULT 'Normal', + status VARCHAR(30) NOT NULL DEFAULT 'active', + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + ); + `); + await pool.query(` ALTER TABLE production_orders ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo', @@ -303,6 +317,14 @@ const initDB = async () => { ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP; `).catch(() => {}); + await pool.query(` + ALTER TABLE supply_fabric_plans + ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo', + ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP, + ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo', + ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP; + `).catch(() => {}); + await pool.query(` CREATE TABLE IF NOT EXISTS app_users ( id SERIAL PRIMARY KEY, @@ -379,6 +401,7 @@ const initDB = async () => { await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_receipts_created_at ON supply_receipts (created_at DESC);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_stock_lots_status ON supply_stock_lots (status);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_movements_created_at ON supply_movements (created_at DESC);`); + await pool.query(`CREATE INDEX IF NOT EXISTS idx_supply_fabric_plans_status ON supply_fabric_plans (status);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`); await pool.query(` CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome diff --git a/backend/routes/supplyRoutes.js b/backend/routes/supplyRoutes.js index 26ff3e6..45554f5 100644 --- a/backend/routes/supplyRoutes.js +++ b/backend/routes/supplyRoutes.js @@ -2,11 +2,15 @@ const express = require('express'); const { verifyToken } = require('../auth'); const { approveReceipt, + createFabricPlan, createReceipt, + deleteFabricPlan, deleteReceipt, getSupplySummary, + listFabricPlans, listLots, listMovements, + listPurchaseNeeds, listReceipts } = require('../services/supplyService'); @@ -69,4 +73,37 @@ router.get('/supply/movements', verifyToken, async (req, res, next) => { } }); +router.get('/supply/fabric-plans', verifyToken, async (req, res, next) => { + try { + res.json(await listFabricPlans()); + } catch (error) { + next(error); + } +}); + +router.post('/supply/fabric-plans', verifyToken, async (req, res, next) => { + try { + res.status(201).json(await createFabricPlan(req.body || {})); + } catch (error) { + next(error); + } +}); + +router.delete('/supply/fabric-plans/:id', verifyToken, async (req, res, next) => { + try { + await deleteFabricPlan(req.params.id); + res.status(204).end(); + } catch (error) { + next(error); + } +}); + +router.get('/supply/purchase-needs', verifyToken, async (req, res, next) => { + try { + res.json(await listPurchaseNeeds()); + } catch (error) { + next(error); + } +}); + module.exports = router; diff --git a/backend/services/supplyService.js b/backend/services/supplyService.js index cf4c148..3b5c54c 100644 --- a/backend/services/supplyService.js +++ b/backend/services/supplyService.js @@ -8,6 +8,11 @@ const normalizeNumber = (value) => { return Number.isFinite(number) && number > 0 ? number : null; }; +const normalizeKey = (value) => normalizeText(value) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); + const mapReceipt = (row) => ({ id: row.id, category: row.category, @@ -50,6 +55,18 @@ const mapMovement = (row) => ({ createdAt: row.created_at }); +const mapFabricPlan = (row) => ({ + id: row.id, + material: row.material, + color: row.color || '', + quantityKg: Number(row.quantity_kg), + supplier: row.supplier || '', + priority: row.priority, + status: row.status, + createdAt: row.created_at, + updatedAt: row.updated_at +}); + const createValidationError = (message) => { const error = new Error(message); error.statusCode = 400; @@ -84,7 +101,87 @@ const listMovements = async () => { return result.rows.map(mapMovement); }; -const buildStats = (receipts, lots) => { +const listFabricPlans = async () => { + const result = await pool.query(` + SELECT id, material, color, quantity_kg, supplier, priority, status, created_at, updated_at + FROM supply_fabric_plans + WHERE status = 'active' + ORDER BY + CASE priority + WHEN 'Crítico' THEN 1 + WHEN 'Atenção' THEN 2 + ELSE 3 + END, + created_at DESC, + id DESC + `); + return result.rows.map(mapFabricPlan); +}; + +const buildPurchaseNeeds = (plans, lots, receipts) => { + const needsByMaterial = new Map(); + + plans.forEach(plan => { + const key = normalizeKey(plan.material); + if (!key) return; + + const current = needsByMaterial.get(key) || { + material: plan.material, + plannedKg: 0, + stockKg: 0, + pendingKg: 0, + purchaseKg: 0, + priority: 'Normal', + suppliers: new Set(), + colors: new Set() + }; + + current.plannedKg += plan.quantityKg; + if (plan.supplier) current.suppliers.add(plan.supplier); + if (plan.color) current.colors.add(plan.color); + if (plan.priority === 'Crítico') current.priority = 'Crítico'; + if (plan.priority === 'Atenção' && current.priority !== 'Crítico') current.priority = 'Atenção'; + needsByMaterial.set(key, current); + }); + + lots.forEach(lot => { + if (lot.unit !== 'kg') return; + const need = needsByMaterial.get(normalizeKey(lot.product)); + if (need) need.stockKg += lot.quantity; + }); + + receipts.forEach(receipt => { + if (receipt.status !== 'pending' || receipt.unit !== 'kg') return; + const need = needsByMaterial.get(normalizeKey(receipt.product)); + if (need) need.pendingKg += receipt.quantity; + }); + + return Array.from(needsByMaterial.values()) + .map(need => { + const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0); + let status = 'ok'; + if (purchaseKg > 0 && (need.priority === 'Crítico' || need.stockKg === 0)) status = 'critical'; + else if (purchaseKg > 0) status = 'attention'; + + return { + material: need.material, + plannedKg: need.plannedKg, + stockKg: need.stockKg, + pendingKg: need.pendingKg, + purchaseKg, + priority: need.priority, + status, + suppliers: Array.from(need.suppliers), + colors: Array.from(need.colors) + }; + }) + .sort((a, b) => { + const statusOrder = { critical: 1, attention: 2, ok: 3 }; + return statusOrder[a.status] - statusOrder[b.status] || b.purchaseKg - a.purchaseKg || a.material.localeCompare(b.material); + }); +}; + +const buildStats = (receipts, lots, purchaseNeeds) => { const totalQuantityKg = lots.reduce((total, lot) => ( lot.unit === 'kg' ? total + lot.quantity : total ), 0); @@ -93,27 +190,41 @@ const buildStats = (receipts, lots) => { totalQuantityKg, activeLots: lots.length, rolls: lots.filter(lot => lot.unit === 'rolos').reduce((total, lot) => total + lot.quantity, 0), - alerts: 0, + alerts: purchaseNeeds.filter(need => need.status !== 'ok').length, pendingReceipts: receipts.filter(receipt => receipt.status === 'pending').length, approvedReceipts: receipts.filter(receipt => receipt.status === 'approved').length }; }; const getSupplySummary = async () => { - const [receipts, lots, movements] = await Promise.all([ + const [receipts, lots, movements, fabricPlans] = await Promise.all([ listReceipts(), listLots(), - listMovements() + listMovements(), + listFabricPlans() ]); + const purchaseNeeds = buildPurchaseNeeds(fabricPlans, lots, receipts); return { receipts, lots, movements, - stats: buildStats(receipts, lots) + fabricPlans, + purchaseNeeds, + stats: buildStats(receipts, lots, purchaseNeeds) }; }; +const listPurchaseNeeds = async () => { + const [plans, lots, receipts] = await Promise.all([ + listFabricPlans(), + listLots(), + listReceipts() + ]); + + return buildPurchaseNeeds(plans, lots, receipts); +}; + const createReceipt = async (payload) => { const category = normalizeText(payload.category); const product = normalizeText(payload.product); @@ -233,12 +344,49 @@ const deleteReceipt = async (id) => { } }; +const createFabricPlan = async (payload) => { + const material = normalizeText(payload.material); + const quantityKg = normalizeNumber(payload.quantityKg); + + if (!material) throw createValidationError('Malha ou tecido é obrigatório.'); + if (!quantityKg) throw createValidationError('Quantidade deve ser maior que zero.'); + + const result = await pool.query(` + INSERT INTO supply_fabric_plans ( + material, color, quantity_kg, supplier, priority, status, updated_at + ) + VALUES ($1, $2, $3, $4, $5, 'active', CURRENT_TIMESTAMP) + RETURNING id, material, color, quantity_kg, supplier, priority, status, created_at, updated_at + `, [ + material, + normalizeText(payload.color) || 'Todas as cores', + quantityKg, + normalizeText(payload.supplier) || null, + normalizeText(payload.priority) || 'Normal' + ]); + + return mapFabricPlan(result.rows[0]); +}; + +const deleteFabricPlan = async (id) => { + await pool.query(` + UPDATE supply_fabric_plans + SET status = 'removed', + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [id]); +}; + module.exports = { approveReceipt, + createFabricPlan, createReceipt, + deleteFabricPlan, deleteReceipt, getSupplySummary, + listFabricPlans, listLots, listMovements, + listPurchaseNeeds, listReceipts }; diff --git a/src/dataService.ts b/src/dataService.ts index 0e1061d..05dc078 100644 --- a/src/dataService.ts +++ b/src/dataService.ts @@ -1,4 +1,4 @@ -import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderSummary, RfmAnalytics, StockData, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types'; +import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderSummary, RfmAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyPurchaseNeed, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types'; import { formatDateParam } from './dateRanges'; const API_URL = import.meta.env.VITE_API_URL || '/api'; @@ -290,37 +290,29 @@ export const deleteConsumptionReference = async (id: number): Promise => { }; export const fetchSupplySummary = async (): Promise => { + const emptySummary: SupplySummary = { + receipts: [], + lots: [], + movements: [], + fabricPlans: [], + purchaseNeeds: [], + stats: { + totalQuantityKg: 0, + activeLots: 0, + rolls: 0, + alerts: 0, + pendingReceipts: 0, + approvedReceipts: 0 + } + }; + try { const response = await authFetch('/supply'); - if (!response.ok) return { - receipts: [], - lots: [], - movements: [], - stats: { - totalQuantityKg: 0, - activeLots: 0, - rolls: 0, - alerts: 0, - pendingReceipts: 0, - approvedReceipts: 0 - } - }; + if (!response.ok) return emptySummary; return await response.json(); } catch (error) { console.error('Fetch supply summary failed', error); - return { - receipts: [], - lots: [], - movements: [], - stats: { - totalQuantityKg: 0, - activeLots: 0, - rolls: 0, - alerts: 0, - pendingReceipts: 0, - approvedReceipts: 0 - } - }; + return emptySummary; } }; @@ -362,6 +354,51 @@ export const deleteSupplyReceipt = async (id: number): Promise => { } }; +export const fetchSupplyFabricPlans = async (): Promise => { + try { + const response = await authFetch('/supply/fabric-plans'); + if (!response.ok) return []; + return await response.json(); + } catch (error) { + console.error('Fetch supply fabric plans failed', error); + return []; + } +}; + +export const createSupplyFabricPlan = async (payload: SupplyFabricPlanPayload): Promise => { + const response = await authFetch('/supply/fabric-plans', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const data = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(data?.error || 'Não foi possível salvar o plano de malha.'); + } + + return data as SupplyFabricPlan; +}; + +export const deleteSupplyFabricPlan = async (id: number): Promise => { + const response = await authFetch(`/supply/fabric-plans/${id}`, { method: 'DELETE' }); + if (!response.ok) { + const data = await response.json().catch(() => null); + throw new Error(data?.error || 'Não foi possível remover o plano de malha.'); + } +}; + +export const fetchSupplyPurchaseNeeds = async (): Promise => { + try { + const response = await authFetch('/supply/purchase-needs'); + if (!response.ok) return []; + return await response.json(); + } catch (error) { + console.error('Fetch supply purchase needs failed', error); + return []; + } +}; + export const fetchDashboardAnalytics = async (dateRange: DateRange, options?: CacheOptions): Promise => { const path = `/analytics/dashboard?${buildDateRangeParams(dateRange).toString()}`; return getCachedAnalytics(path, async () => { diff --git a/src/pages/Supplies.tsx b/src/pages/Supplies.tsx index 6ab501a..68a98e9 100644 --- a/src/pages/Supplies.tsx +++ b/src/pages/Supplies.tsx @@ -20,19 +20,11 @@ import { Truck, Warehouse, } from 'lucide-react'; -import { approveSupplyReceipt, createSupplyReceipt, deleteSupplyReceipt, fetchSupplySummary } from '../dataService'; -import type { SupplyLot, SupplyMovement, SupplyReceipt, SupplySummary } from '../types'; +import { approveSupplyReceipt, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchSupplySummary } from '../dataService'; +import type { SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types'; type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements'; type ReceiptView = 'new' | 'pending' | 'history'; -type FabricPlan = { - id: string; - material: string; - color: string; - quantityKg: number; - supplier: string; - priority: string; -}; const pageClassName = 'mx-auto flex w-full max-w-7xl flex-col gap-6'; const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm'; @@ -44,6 +36,8 @@ const emptySupplySummary: SupplySummary = { receipts: [], lots: [], movements: [], + fabricPlans: [], + purchaseNeeds: [], stats: { totalQuantityKg: 0, activeLots: 0, @@ -93,17 +87,6 @@ const exportCsv = (filename: string, rows: Array { - try { - const rawPlans = localStorage.getItem('nexstar_fabric_plans'); - if (!rawPlans) return []; - const parsed = JSON.parse(rawPlans); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -}; - const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarChart3 }> = [ { id: 'dashboard', name: 'Dashboard', icon: Warehouse }, { id: 'balance', name: 'Saldo', icon: BarChart3 }, @@ -796,7 +779,7 @@ const InventoryScreen = () => { }; const FabricPlanningScreen = () => { - const [plans, setPlans] = useState(loadFabricPlans); + const [plans, setPlans] = useState([]); const [form, setForm] = useState({ material: '', color: '', @@ -804,36 +787,84 @@ const FabricPlanningScreen = () => { supplier: '', priority: 'Normal', }); + const [isLoading, setIsLoading] = useState(true); + const [isBusy, setIsBusy] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); const totalKg = plans.reduce((total, plan) => total + plan.quantityKg, 0); - const savePlans = (nextPlans: FabricPlan[]) => { - setPlans(nextPlans); - localStorage.setItem('nexstar_fabric_plans', JSON.stringify(nextPlans)); + const loadPlans = async () => { + setErrorMessage(''); + const summary = await fetchSupplySummary(); + setPlans(summary.fabricPlans); }; - const handleSubmit = (event: FormEvent) => { + useEffect(() => { + let isMounted = true; + + const load = async () => { + setIsLoading(true); + try { + const summary = await fetchSupplySummary(); + if (isMounted) setPlans(summary.fabricPlans); + } finally { + if (isMounted) setIsLoading(false); + } + }; + + load(); + + return () => { + isMounted = false; + }; + }, []); + + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const material = form.material.trim(); const quantityKg = parseDecimal(form.quantityKg); if (!material || quantityKg <= 0) return; - const nextPlan: FabricPlan = { - id: `${Date.now()}`, - material, - color: form.color.trim() || 'Todas as cores', - quantityKg, - supplier: form.supplier.trim() || 'Sem fornecedor', - priority: form.priority, - }; + setIsBusy(true); + setErrorMessage(''); + try { + await createSupplyFabricPlan({ + material, + color: form.color.trim() || 'Todas as cores', + quantityKg, + supplier: form.supplier.trim() || 'Sem fornecedor', + priority: form.priority, + }); + await loadPlans(); + setForm({ material: '', color: '', quantityKg: '', supplier: '', priority: 'Normal' }); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : 'Não foi possível salvar o plano de malha.'); + } finally { + setIsBusy(false); + } + }; - savePlans([nextPlan, ...plans]); - setForm({ material: '', color: '', quantityKg: '', supplier: '', priority: 'Normal' }); + const removePlan = async (planId: number) => { + setIsBusy(true); + setErrorMessage(''); + try { + await deleteSupplyFabricPlan(planId); + await loadPlans(); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : 'Não foi possível remover o plano de malha.'); + } finally { + setIsBusy(false); + } }; return (
+ {errorMessage && ( +
+ {errorMessage} +
+ )}

Planos ativos

@@ -878,9 +909,9 @@ const FabricPlanningScreen = () => { Fornecedor setForm(current => ({ ...current, supplier: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Opcional" /> -
@@ -890,7 +921,12 @@ const FabricPlanningScreen = () => {

Planos de malha

Itens planejados para compra ou recebimento.

- {plans.length ? ( + {isLoading ? ( +
+ +

Carregando planos...

+
+ ) : plans.length ? (
{plans.map(plan => (
@@ -901,7 +937,7 @@ const FabricPlanningScreen = () => {

{plan.supplier}

{formatNumber(plan.quantityKg)} kg

{plan.priority} -
@@ -920,42 +956,152 @@ const FabricPlanningScreen = () => { ); }; -const PurchaseNeedsScreen = () => ( -
-
-
- {[ - { label: 'Itens críticos', value: '0' }, - { label: 'Compra sugerida', value: '0 kg' }, - { label: 'Fornecedores pendentes', value: '0' }, - ].map(stat => ( -
-

{stat.label}

-

{stat.value}

-
- ))} -
-
-
-
-

Necessidade por material

-

Calculada com estoque atual, mínimo configurado e consumo dos planos.

-
- +const purchaseStatusLabels: Record = { + critical: 'Crítico', + attention: 'Atenção', + ok: 'Coberto', +}; + +const PurchaseNeedsScreen = () => { + const [summary, setSummary] = useState(emptySupplySummary); + const [isLoading, setIsLoading] = useState(true); + const [search, setSearch] = useState(''); + + const loadSummary = async () => { + const nextSummary = await fetchSupplySummary(); + setSummary(nextSummary); + }; + + useEffect(() => { + let isMounted = true; + + const load = async () => { + setIsLoading(true); + try { + const nextSummary = await fetchSupplySummary(); + if (isMounted) setSummary(nextSummary); + } finally { + if (isMounted) setIsLoading(false); + } + }; + + load(); + + return () => { + isMounted = false; + }; + }, []); + + const normalizedSearch = normalizeSearch(search); + const visibleNeeds = summary.purchaseNeeds.filter(need => ( + !normalizedSearch || normalizeSearch(`${need.material} ${need.suppliers.join(' ')} ${need.colors.join(' ')}`).includes(normalizedSearch) + )); + const criticalCount = summary.purchaseNeeds.filter(need => need.status === 'critical').length; + const suggestedPurchaseKg = summary.purchaseNeeds.reduce((total, need) => total + need.purchaseKg, 0); + const pendingSupplierCount = new Set(summary.purchaseNeeds.flatMap(need => need.suppliers)).size; + + return ( +
+
+
+ {[ + { label: 'Itens críticos', value: `${criticalCount}` }, + { label: 'Compra sugerida', value: `${formatNumber(suggestedPurchaseKg)} kg` }, + { label: 'Fornecedores envolvidos', value: `${pendingSupplierCount}` }, + ].map(stat => ( +
+

{stat.label}

+

{stat.value}

+
+ ))}
-
- -

Nenhuma necessidade de compra

-

Configure mínimos e registre estoque para gerar sugestões.

+
+
+
+

Necessidade por material

+

Planejado - estoque aprovado - recebimentos pendentes.

+
+
+ + +
+
+ + {isLoading ? ( +
+ +

Calculando necessidade...

+
+ ) : visibleNeeds.length ? ( +
+
+ Material + Planejado + Estoque + Pendente + Comprar + Status +
+
+ {visibleNeeds.map(need => ( +
+
+

{need.material}

+

+ {(need.colors.length ? need.colors.join(', ') : 'Todas as cores')} · {(need.suppliers.length ? need.suppliers.join(', ') : 'Sem fornecedor')} +

+
+

{formatNumber(need.plannedKg)} kg

+

{formatNumber(need.stockKg)} kg

+

{formatNumber(need.pendingKg)} kg

+

0 ? 'text-red-300' : 'text-emerald-300'}`}>{formatNumber(need.purchaseKg)} kg

+ + {purchaseStatusLabels[need.status]} + +
+ ))} +
+
+ ) : ( +
+ +

{summary.purchaseNeeds.length ? 'Nenhuma necessidade encontrada' : 'Nenhuma necessidade de compra'}

+

{summary.purchaseNeeds.length ? 'Ajuste a busca.' : 'Cadastre planos de malha para gerar demanda e aprove recebimentos para abater estoque.'}

+
+ )} +
+
+

Como o fluxo está conectado

+

Plano de malha cria demanda. Recebimento pendente entra como material a receber. Aprovar recebimento move para estoque e movimentações.

-
-

Estoque mínimo por item

-

Defina o ponto de pedido de cada matéria-prima para aparecer aqui quando ficar abaixo do mínimo.

- -
-
-); + ); +}; const Supplies = () => { const { section } = useParams<{ section?: string }>(); diff --git a/src/types.ts b/src/types.ts index ddadf43..84d3b38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -210,6 +210,30 @@ export interface SupplyMovement { createdAt: string; } +export interface SupplyFabricPlan { + id: number; + material: string; + color: string; + quantityKg: number; + supplier: string; + priority: string; + status: string; + createdAt: string; + updatedAt: string; +} + +export interface SupplyPurchaseNeed { + material: string; + plannedKg: number; + stockKg: number; + pendingKg: number; + purchaseKg: number; + priority: string; + status: 'critical' | 'attention' | 'ok'; + suppliers: string[]; + colors: string[]; +} + export interface SupplyStats { totalQuantityKg: number; activeLots: number; @@ -223,6 +247,8 @@ export interface SupplySummary { receipts: SupplyReceipt[]; lots: SupplyLot[]; movements: SupplyMovement[]; + fabricPlans: SupplyFabricPlan[]; + purchaseNeeds: SupplyPurchaseNeed[]; stats: SupplyStats; } @@ -236,6 +262,14 @@ export type SupplyReceiptPayload = { notes?: string; }; +export type SupplyFabricPlanPayload = { + material: string; + color?: string; + quantityKg: number | string; + supplier?: string; + priority?: string; +}; + export interface DateRange { start: Date; end: Date;