Unify material supplier and balance controls
This commit is contained in:
@@ -10,6 +10,7 @@ const {
|
||||
deleteReceipt,
|
||||
getSupplySummary,
|
||||
listFabricPlans,
|
||||
listKnownSuppliers,
|
||||
saveMaterialDetails,
|
||||
listLots,
|
||||
listMovements,
|
||||
@@ -27,6 +28,14 @@ router.get('/supply', verifyToken, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/supply/suppliers', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await listKnownSuppliers());
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/supply/receipts', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await listReceipts());
|
||||
|
||||
@@ -158,6 +158,26 @@ const listMaterialDetails = async () => {
|
||||
return result.rows.map(mapMaterialDetails);
|
||||
};
|
||||
|
||||
const listKnownSuppliers = async () => {
|
||||
const result = await pool.query(`
|
||||
SELECT DISTINCT supplier
|
||||
FROM (
|
||||
SELECT preferred_supplier AS supplier FROM supply_material_details
|
||||
UNION ALL
|
||||
SELECT supplier FROM supply_lots
|
||||
UNION ALL
|
||||
SELECT supplier FROM supply_receipts
|
||||
UNION ALL
|
||||
SELECT supplier FROM supply_fabric_plans
|
||||
UNION ALL
|
||||
SELECT supplier FROM production_yield_supplier_recommendations
|
||||
) sources
|
||||
WHERE NULLIF(BTRIM(supplier), '') IS NOT NULL
|
||||
ORDER BY supplier;
|
||||
`);
|
||||
return result.rows.map(row => row.supplier);
|
||||
};
|
||||
|
||||
const saveMaterialDetails = async (materialKey, payload) => {
|
||||
const normalizedKey = normalizeKey(materialKey);
|
||||
if (!normalizedKey) throw createValidationError('Material inválido para edição.');
|
||||
@@ -1062,6 +1082,7 @@ module.exports = {
|
||||
buildPurchaseNeeds,
|
||||
listFabricPlans,
|
||||
listMaterialDetails,
|
||||
listKnownSuppliers,
|
||||
listLots,
|
||||
listMovements,
|
||||
listPurchaseNeeds,
|
||||
|
||||
125
src/components/StockGraphsBalanceModal.tsx
Normal file
125
src/components/StockGraphsBalanceModal.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { History, RotateCcw, Save, X } from 'lucide-react';
|
||||
import { deleteGraphsStockBalance, fetchGraphsStockBalanceHistory, saveGraphsStockBalance } from '../dataService';
|
||||
import type { GraphsStockBalanceHistoryItem, StockData } from '../types';
|
||||
|
||||
const formatNumber = (value: number, maximumFractionDigits = 0) => (
|
||||
new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value)
|
||||
);
|
||||
|
||||
const formatDate = (value: string | null | undefined) => {
|
||||
if (!value) return '—';
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? '—' : new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short' }).format(date);
|
||||
};
|
||||
|
||||
type StockGraphsBalanceModalProps = {
|
||||
stock: StockData;
|
||||
title?: string;
|
||||
onClose: () => void;
|
||||
onSaved: (stock: StockData) => void;
|
||||
};
|
||||
|
||||
const StockGraphsBalanceModal = ({ stock, title, onClose, onSaved }: StockGraphsBalanceModalProps) => {
|
||||
const [balance, setBalance] = useState(stock.graphs_balance === null || stock.graphs_balance === undefined ? '' : String(stock.graphs_balance));
|
||||
const [reason, setReason] = useState(stock.graphs_balance_reason || 'Conferência física de estoque');
|
||||
const [effectiveAt, setEffectiveAt] = useState(stock.graphs_balance_effective_at?.slice(0, 10) || new Date().toISOString().slice(0, 10));
|
||||
const [history, setHistory] = useState<GraphsStockBalanceHistoryItem[]>([]);
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const loadHistory = useCallback(async () => {
|
||||
setIsLoadingHistory(true);
|
||||
try {
|
||||
setHistory(await fetchGraphsStockBalanceHistory(stock.produto_id));
|
||||
} catch (historyError) {
|
||||
setError(historyError instanceof Error ? historyError.message : 'Não foi possível carregar o histórico.');
|
||||
} finally {
|
||||
setIsLoadingHistory(false);
|
||||
}
|
||||
}, [stock.produto_id]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => { void loadHistory(); }, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [loadHistory]);
|
||||
|
||||
const save = async () => {
|
||||
const normalizedBalance = Number(balance.replace(',', '.'));
|
||||
if (!Number.isFinite(normalizedBalance)) {
|
||||
setError('Informe um saldo físico válido.');
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const saved = await saveGraphsStockBalance(stock.produto_id, { balance: normalizedBalance, reason, effectiveAt });
|
||||
onSaved(saved);
|
||||
setBalance(String(saved.graphs_balance ?? normalizedBalance));
|
||||
await loadHistory();
|
||||
} catch (saveError) {
|
||||
setError(saveError instanceof Error ? saveError.message : 'Não foi possível salvar o saldo Graphs.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
if (!window.confirm('Remover o saldo Graphs e voltar a usar o saldo importado do Tiny no planejamento?')) return;
|
||||
setIsSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const saved = await deleteGraphsStockBalance(stock.produto_id);
|
||||
onSaved(saved);
|
||||
setBalance('');
|
||||
await loadHistory();
|
||||
} catch (removeError) {
|
||||
setError(removeError instanceof Error ? removeError.message : 'Não foi possível remover o saldo Graphs.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const planningBalance = Math.max(0, Number(stock.planning_balance ?? stock.saldo));
|
||||
const materialName = title || stock.nome;
|
||||
|
||||
return <div className="fixed inset-0 z-[60] flex h-dvh items-center justify-center overflow-y-auto bg-black/75 p-4 backdrop-blur-md" role="dialog" aria-modal="true" aria-label={`Saldo Graphs de ${materialName}`} onClick={onClose}>
|
||||
<div className="w-full max-w-2xl rounded-2xl border border-dark-border bg-dark-card shadow-2xl" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="flex items-start justify-between gap-4 border-b border-dark-border p-5">
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-[10px] font-bold uppercase tracking-widest text-dark-muted">Material #{stock.produto_id}</p>
|
||||
<h2 className="mt-1 truncate text-lg font-bold text-dark-text" title={materialName}>{materialName}</h2>
|
||||
<p className="mt-2 text-sm font-semibold text-dark-muted">O saldo Tiny permanece preservado para auditoria. O saldo Graphs passa a ser a base do planejamento.</p>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} title="Fechar" aria-label="Fechar" className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-brand-primary hover:text-dark-text cursor-pointer"><X className="h-4 w-4" /></button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 p-5 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/50 p-3"><p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Saldo Tiny</p><p className="mt-2 text-lg font-bold text-red-300">{formatNumber(Number(stock.saldo), 2)} un.</p></div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/50 p-3"><p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Saldo Graphs</p><p className="mt-2 text-lg font-bold text-brand-primary">{stock.graphs_balance === null || stock.graphs_balance === undefined ? 'Não informado' : `${formatNumber(Number(stock.graphs_balance), 2)} un.`}</p></div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/50 p-3"><p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Disponível no plano</p><p className="mt-2 text-lg font-bold text-emerald-300">{formatNumber(planningBalance, 2)} un.</p></div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 border-t border-dark-border p-5 md:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
|
||||
<div className="space-y-4">
|
||||
<label className="block space-y-2"><span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldo físico no Graphs</span><input type="text" inputMode="decimal" value={balance} onChange={(event) => setBalance(event.target.value)} placeholder="Ex.: 850,50" className="h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none transition-colors placeholder:text-dark-muted focus:border-brand-primary" /></label>
|
||||
<label className="block space-y-2"><span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Data da conferência</span><input type="date" value={effectiveAt} onChange={(event) => setEffectiveAt(event.target.value)} className="h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none transition-colors focus:border-brand-primary" /></label>
|
||||
<label className="block space-y-2"><span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Motivo</span><textarea value={reason} onChange={(event) => setReason(event.target.value)} rows={3} placeholder="Ex.: inventário físico do depósito" className="w-full resize-none rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-semibold text-dark-text outline-none transition-colors placeholder:text-dark-muted focus:border-brand-primary" /></label>
|
||||
{error && <p className="rounded-lg border border-red-400/30 bg-red-400/10 px-3 py-2 text-xs font-bold text-red-300">{error}</p>}
|
||||
<div className="flex flex-wrap justify-between gap-2"><button type="button" onClick={() => void remove()} disabled={isSaving || stock.graphs_balance === null || stock.graphs_balance === undefined} className="inline-flex h-10 items-center gap-2 rounded-lg border border-dark-border px-3 text-sm font-bold text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"><RotateCcw className="h-4 w-4" /> Usar Tiny</button><button type="button" onClick={() => void save()} disabled={isSaving} className="inline-flex h-10 items-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><Save className="h-4 w-4" /> {isSaving ? 'Salvando...' : 'Salvar saldo Graphs'}</button></div>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-xl border border-dark-border">
|
||||
<div className="flex items-center gap-2 border-b border-dark-border bg-dark-input/50 px-4 py-3"><History className="h-4 w-4 text-brand-primary" /><h3 className="text-sm font-bold text-dark-text">Histórico de alterações</h3></div>
|
||||
<div className="max-h-80 divide-y divide-dark-border overflow-y-auto">
|
||||
{isLoadingHistory && <p className="p-4 text-sm font-semibold text-dark-muted">Carregando histórico...</p>}
|
||||
{!isLoadingHistory && !history.length && <p className="p-4 text-sm font-semibold text-dark-muted">Nenhuma alteração manual registrada.</p>}
|
||||
{history.map(item => <div key={item.id} className="p-4"><div className="flex items-center justify-between gap-3"><p className="text-sm font-bold text-dark-text">{item.action === 'removed' ? 'Saldo Graphs removido' : `${formatNumber(Number(item.graphs_balance || 0), 2)} un.`}</p><span className="text-xs font-semibold text-dark-muted">{formatDate(item.created_at)}</span></div><p className="mt-1 text-xs font-semibold text-dark-muted">Tiny: {formatNumber(Number(item.tiny_balance || 0), 2)} un. · referência {formatDate(item.effective_at)}</p>{item.reason && <p className="mt-2 text-xs font-medium text-dark-text">{item.reason}</p>}{item.changed_by && <p className="mt-1 text-[11px] font-semibold text-dark-muted">Por {item.changed_by}</p>}</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default StockGraphsBalanceModal;
|
||||
@@ -525,6 +525,12 @@ export const fetchSupplySummary = async (): Promise<SupplySummary> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchSupplySuppliers = async (): Promise<string[]> => {
|
||||
const response = await authFetch('/supply/suppliers');
|
||||
if (!response.ok) throw new Error('Não foi possível carregar os fornecedores cadastrados.');
|
||||
return response.json() as Promise<string[]>;
|
||||
};
|
||||
|
||||
export const saveSupplyMaterialDetails = async (
|
||||
materialKey: string,
|
||||
payload: SupplyMaterialDetailsPayload
|
||||
|
||||
@@ -29,8 +29,9 @@ import { buildSkuEditPath } from '../catalogLinks';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import ProductTypeBadge from '../components/ProductTypeBadge';
|
||||
import StockGraphsBalanceModal from '../components/StockGraphsBalanceModal';
|
||||
import { classifyCutFamily } from '../analytics/cutting';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplySummary, isSuperAdmin, saveSupplyMaterialDetails } from '../dataService';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplySummary, fetchSupplySuppliers, isSuperAdmin, saveSupplyMaterialDetails } from '../dataService';
|
||||
import { parseProductName } from '../productParsing';
|
||||
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||
import { getPlanningStock } from '../planningStock';
|
||||
@@ -86,6 +87,7 @@ const formatDateTime = (value: string | null) => {
|
||||
};
|
||||
|
||||
const normalizeSearch = (value: string) => value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||
const normalizeMaterialKey = (value: string) => normalizeSearch(value).replace(/\s+/g, ' ').trim();
|
||||
|
||||
const exportCsv = (filename: string, rows: Array<Record<string, string | number | null>>) => {
|
||||
if (!rows.length) return;
|
||||
@@ -2314,12 +2316,18 @@ const getMaterialRegistrationPath = (material: string) => buildSkuEditPath({
|
||||
|
||||
const MaterialDetailsModal = ({
|
||||
need,
|
||||
linkedStock,
|
||||
supplierOptions,
|
||||
onClose,
|
||||
onSave,
|
||||
onEditGraphsBalance,
|
||||
}: {
|
||||
need: SupplyPurchaseNeed;
|
||||
linkedStock: StockData | null;
|
||||
supplierOptions: string[];
|
||||
onClose: () => void;
|
||||
onSave: (payload: { displayName: string; preferredSupplier: string; notes: string }) => Promise<void>;
|
||||
onEditGraphsBalance: (stock: StockData) => void;
|
||||
}) => {
|
||||
const [form, setForm] = useState({
|
||||
displayName: need.material,
|
||||
@@ -2363,8 +2371,19 @@ const MaterialDetailsModal = ({
|
||||
</label>
|
||||
<label className="block text-xs font-bold text-dark-muted">
|
||||
Fornecedor preferencial
|
||||
<input value={form.preferredSupplier} onChange={(event) => setForm(current => ({ ...current, preferredSupplier: event.target.value }))} placeholder="Opcional" className={`${inputClassName} mt-1`} />
|
||||
<input list="known-suppliers" value={form.preferredSupplier} onChange={(event) => setForm(current => ({ ...current, preferredSupplier: event.target.value }))} placeholder="Selecione ou informe um fornecedor" className={`${inputClassName} mt-1`} />
|
||||
<datalist id="known-suppliers">{supplierOptions.map(supplier => <option key={supplier} value={supplier} />)}</datalist>
|
||||
<span className="mt-1 block text-[11px] font-semibold text-dark-muted">Sugestões carregadas do histórico de fornecedores e recebimentos.</span>
|
||||
</label>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/50 p-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldo físico no Graphs</p>
|
||||
{linkedStock ? <><p className="mt-1 text-sm font-bold text-brand-primary">{linkedStock.graphs_balance === null || linkedStock.graphs_balance === undefined ? 'Não informado' : `${new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(Number(linkedStock.graphs_balance))} un.`}</p><p className="mt-1 text-[11px] font-semibold text-dark-muted">Tiny: {new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(Number(linkedStock.saldo))} un. · usado no plano: {new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(Math.max(0, Number(linkedStock.planning_balance ?? linkedStock.saldo)))} un.</p></> : <p className="mt-1 text-xs font-semibold text-dark-muted">Este material ainda não possui saldo importado do Tiny para conferência.</p>}
|
||||
</div>
|
||||
{linkedStock && <button type="button" onClick={() => onEditGraphsBalance(linkedStock)} className="inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-card px-3 text-xs font-bold text-brand-primary transition-colors hover:border-brand-primary cursor-pointer"><Pencil className="h-3.5 w-3.5" /> Ajustar saldo</button>}
|
||||
</div>
|
||||
</div>
|
||||
<label className="block text-xs font-bold text-dark-muted">
|
||||
Observações operacionais
|
||||
<textarea value={form.notes} onChange={(event) => setForm(current => ({ ...current, notes: event.target.value }))} placeholder="Opcional" className="mt-1 min-h-24 w-full resize-y rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" />
|
||||
@@ -2389,9 +2408,12 @@ const MaterialDetailsModal = ({
|
||||
const PurchaseNeedsScreen = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||
const [stockSnapshot, setStockSnapshot] = useState<StockData[]>([]);
|
||||
const [supplierOptions, setSupplierOptions] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [editingMaterial, setEditingMaterial] = useState<SupplyPurchaseNeed | null>(null);
|
||||
const [editingGraphsBalance, setEditingGraphsBalance] = useState<StockData | null>(null);
|
||||
const requestedTab = searchParams.get('tab');
|
||||
const requestedFilter = searchParams.get('filter');
|
||||
const activeTab: PurchaseNeedTab = requestedTab === 'services' ? 'services' : 'materials';
|
||||
@@ -2401,8 +2423,14 @@ const PurchaseNeedsScreen = () => {
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
const loadSummary = async () => {
|
||||
const nextSummary = await fetchSupplySummary();
|
||||
const [nextSummary, nextStockSnapshot, nextSuppliers] = await Promise.all([
|
||||
fetchSupplySummary(),
|
||||
fetchStock(),
|
||||
fetchSupplySuppliers(),
|
||||
]);
|
||||
setSummary(nextSummary);
|
||||
setStockSnapshot(nextStockSnapshot);
|
||||
setSupplierOptions(nextSuppliers);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2411,8 +2439,16 @@ const PurchaseNeedsScreen = () => {
|
||||
const load = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const nextSummary = await fetchSupplySummary();
|
||||
if (isMounted) setSummary(nextSummary);
|
||||
const [nextSummary, nextStockSnapshot, nextSuppliers] = await Promise.all([
|
||||
fetchSupplySummary(),
|
||||
fetchStock(),
|
||||
fetchSupplySuppliers(),
|
||||
]);
|
||||
if (isMounted) {
|
||||
setSummary(nextSummary);
|
||||
setStockSnapshot(nextStockSnapshot);
|
||||
setSupplierOptions(nextSuppliers);
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) setIsLoading(false);
|
||||
}
|
||||
@@ -2459,6 +2495,23 @@ const PurchaseNeedsScreen = () => {
|
||||
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)
|
||||
));
|
||||
const stocksByMaterialKey = useMemo(() => {
|
||||
const index = new Map<string, StockData>();
|
||||
stockSnapshot.forEach(stock => {
|
||||
index.set(normalizeMaterialKey(stock.nome), stock);
|
||||
index.set(normalizeMaterialKey(stock.produto_id), stock);
|
||||
});
|
||||
return index;
|
||||
}, [stockSnapshot]);
|
||||
const linkedEditingStock = editingMaterial
|
||||
? stocksByMaterialKey.get(normalizeMaterialKey(editingMaterial.materialKey)) || stocksByMaterialKey.get(normalizeMaterialKey(editingMaterial.material)) || null
|
||||
: null;
|
||||
const handleGraphsBalanceSaved = (updatedStock: StockData) => {
|
||||
setStockSnapshot(current => current.map(stock => (
|
||||
stock.produto_id === updatedStock.produto_id ? updatedStock : stock
|
||||
)));
|
||||
void loadSummary();
|
||||
};
|
||||
const totalPages = Math.ceil(visibleNeeds.length / itemsPerPage);
|
||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||
@@ -2581,13 +2634,24 @@ const PurchaseNeedsScreen = () => {
|
||||
<MaterialDetailsModal
|
||||
key={editingMaterial.materialKey}
|
||||
need={editingMaterial}
|
||||
linkedStock={linkedEditingStock}
|
||||
supplierOptions={supplierOptions}
|
||||
onClose={() => setEditingMaterial(null)}
|
||||
onEditGraphsBalance={setEditingGraphsBalance}
|
||||
onSave={async (payload) => {
|
||||
await saveSupplyMaterialDetails(editingMaterial.materialKey, payload);
|
||||
await loadSummary();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{editingGraphsBalance && (
|
||||
<StockGraphsBalanceModal
|
||||
stock={editingGraphsBalance}
|
||||
title={editingMaterial?.material || editingGraphsBalance.nome}
|
||||
onClose={() => setEditingGraphsBalance(null)}
|
||||
onSaved={handleGraphsBalanceSaved}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user