/* ═══════════════════════════════════════════════ DATA TABLE VIEW v2 — Updated filters + Export ═══════════════════════════════════════════════ */ const DataTableView = () => { const [, setTick] = React.useState(0); React.useEffect(() => { const handler = () => setTick(t => t + 1); window.addEventListener('vencidos_data_changed', handler); return () => window.removeEventListener('vencidos_data_changed', handler); }, []); const getCatalogStock = (p) => { const lookup = window.MOCK.CATALOG_LOOKUP; if (!lookup) return null; const catSuc = (window.MOCK.UBICACION_TO_CATALOG || {})[p.sucursal]; if (!catSuc) return null; return lookup[`${p.codigo}_${catSuc}`] || null; }; // Categoría efectiva (ruta completa): la del catálogo Odoo o, si falta, la del registro const getCat1 = (p) => { const cat = getCatalogStock(p); if (cat) { const path = [cat.cat1, cat.cat2, cat.cat3, cat.cat4].filter(Boolean).join(' / '); if (path) return path; } return p.cat1 || ''; }; const products = window.MOCK.MOCK_PRODUCTS; const [filters, setFilters] = React.useState(() => { try { const saved = localStorage.getItem('vencidos_filters'); if (saved) { return { categoria: 'Todos', proveedor: 'Todos', origen: 'Todos', abc: 'Todos', etiqueta: 'Todos', abcSucursal: 'Todos', cobertura: 'Todos', sucursal: 'Todos', estado: 'Todos', tipo: 'Todos', accion: 'Todos', search: '', vencido: 'Todos', ...JSON.parse(saved) }; } } catch (e) { console.error("Error loading filters from localStorage", e); } return { categoria: 'Todos', proveedor: 'Todos', origen: 'Todos', abc: 'Todos', etiqueta: 'Todos', abcSucursal: 'Todos', cobertura: 'Todos', sucursal: 'Todos', estado: 'Todos', tipo: 'Todos', accion: 'Todos', search: '', vencido: 'Todos', }; }); React.useEffect(() => { try { localStorage.setItem('vencidos_filters', JSON.stringify(filters)); } catch (e) { console.error("Error saving filters to localStorage", e); } }, [filters]); const [sortKey, setSortKey] = React.useState('id'); const [sortDir, setSortDir] = React.useState('desc'); const [page, setPage] = React.useState(0); const [showFilters, setShowFilters] = React.useState(true); const pageSize = 25; const currentUser = React.useMemo(() => { try { return JSON.parse(localStorage.getItem('vencidos_user') || 'null'); } catch { return null; } }, []); const isAdmin = currentUser?.role === 'admin'; const isSupervisor = currentUser?.role === 'supervisor'; const canManageProducts = isAdmin || isSupervisor; const [editingProduct, setEditingProduct] = React.useState(null); const [savingEdit, setSavingEdit] = React.useState(false); const [editForm, setEditForm] = React.useState({ cantidad: 1, estado: 'proximo_vencer', accion: '', fechaVencimiento: '', nota: '', tipo: 'vencido', estadoDanado: '', proveedor: '', codigo: '', }); const activeFilterCount = Object.entries(filters).filter(([k, v]) => k !== 'search' && v !== 'Todos').length; const filtered = products.filter(p => { if (filters.categoria !== 'Todos' && getCat1(p) !== filters.categoria) return false; if (filters.proveedor !== 'Todos' && p.proveedor !== filters.proveedor) return false; if (filters.origen !== 'Todos' && p.origen !== filters.origen) return false; if (filters.abc !== 'Todos' && p.abc !== filters.abc) return false; if (filters.etiqueta !== 'Todos' && p.etiqueta !== filters.etiqueta) return false; if (filters.abcSucursal !== 'Todos' && p.abcSucursal !== filters.abcSucursal) return false; if (filters.cobertura !== 'Todos' && p.cobertura !== filters.cobertura) return false; if (filters.sucursal !== 'Todos' && window.MOCK.resolveSalaId(p.sucursal) !== filters.sucursal) return false; if (filters.estado !== 'Todos' && p.estado !== filters.estado) return false; if (filters.tipo !== 'Todos' && p.tipo !== filters.tipo) return false; if (filters.accion !== 'Todos' && p.accion !== filters.accion) return false; if (filters.vencido === 'Si' && !(p.diasRestantes < 0)) return false; if (filters.search) { const s = filters.search.toLowerCase(); return p.nombre.toLowerCase().includes(s) || p.codigo.includes(s) || p.proveedor.toLowerCase().includes(s); } return true; }); const sorted = [...filtered].sort((a, b) => { let va = a[sortKey], vb = b[sortKey]; if (typeof va === 'number' && typeof vb === 'number') return sortDir === 'asc' ? va - vb : vb - va; if (typeof va === 'string') { va = va.toLowerCase(); vb = (vb||'').toLowerCase(); } if (va < vb) return sortDir === 'asc' ? -1 : 1; if (va > vb) return sortDir === 'asc' ? 1 : -1; return 0; }); const totalPages = Math.ceil(sorted.length / pageSize); const paged = sorted.slice(page * pageSize, (page + 1) * pageSize); const toggleSort = (key) => { if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc'); else { setSortKey(key); setSortDir('asc'); } }; const SortIcon = ({ k }) => ( {sortKey === k && sortDir === 'desc' ? '▼' : '▲'} ); const handleExport = () => { const headers = ['Código','Producto','Proveedor','Categoría','Sucursal','Tipo','Cantidad', 'Fecha Vencimiento','Vence en (días)','Estado Vencimiento','Estado','Acción','ABC','ABC Sucursal', 'Etiqueta','Cobertura','Cobertura Días','Origen','Stock','Ventas','Reportado por','Fecha Registro']; const rows = filtered.map(p => [ p.codigo, p.nombre, p.proveedor, getCat1(p), p.sucursalName, p.tipo, p.cantidad, p.fechaVencimiento, p.diasRestantes, p.estadoVencimiento, p.estado, p.accion, p.abc, p.abcSucursal, p.etiqueta, p.cobertura, p.coverageDays, p.origen, p.stock, p.sales, p.reportadoPor, p.fechaRegistro, ]); const generatedAt = new Date(); const fileStamp = generatedAt.toISOString().split('T')[0]; const colWidths = [18, 38, 28, 22, 16, 12, 12, 18, 16, 24, 22, 28, 8, 14, 16, 14, 14, 14, 10, 10, 20, 18]; downloadExcelWorkbook([ { name: 'Productos Vencidos', header: headers, autoFilterRef: `A1:${toExcelColumnName(headers.length - 1)}${rows.length + 1}`, columnWidths: colWidths, rows, }, ], `productos_vencidos_${fileStamp}.xlsx`, generatedAt); }; const setFilter = (key, val) => { setFilters(f => ({...f, [key]: val})); setPage(0); }; const clearFilters = () => { setFilters({ categoria: 'Todos', proveedor: 'Todos', origen: 'Todos', abc: 'Todos', etiqueta: 'Todos', abcSucursal: 'Todos', cobertura: 'Todos', sucursal: 'Todos', estado: 'Todos', tipo: 'Todos', accion: 'Todos', search: '', vencido: 'Todos', }); setPage(0); }; const openEdit = (p) => { setEditingProduct(p); setEditForm({ cantidad: p.cantidad || 1, estado: p.estado || 'proximo_vencer', accion: p.accion || '', fechaVencimiento: p.fechaVencimiento || '', nota: p.nota || '', tipo: p.tipo || 'vencido', estadoDanado: p.estadoDanado || '', proveedor: p.proveedor || '', codigo: p.codigo || '', }); }; const closeEdit = () => { setEditingProduct(null); setSavingEdit(false); }; const handleSaveEdit = async () => { if (!editingProduct) return; const cantidadNum = parseInt(editForm.cantidad, 10); if (!Number.isFinite(cantidadNum) || cantidadNum <= 0) { alert('La cantidad debe ser mayor a 0'); return; } setSavingEdit(true); try { const updated = await window.MOCK.updateProduct(editingProduct.id, { cantidad: cantidadNum, estado: editForm.estado, accion: editForm.accion.trim(), fechaVencimiento: editForm.fechaVencimiento, nota: editForm.nota || '', tipo: editForm.tipo, estadoDanado: editForm.tipo === 'dañado' ? editForm.estadoDanado : '', proveedor: editForm.proveedor.trim(), codigo: editForm.codigo.trim(), }, currentUser); if (currentUser) { window.MOCK.addAuditEntry( 'Editó producto', `${updated.nombre} en ${updated.sucursalName || updated.sucursal}`, currentUser.name, currentUser.role, currentUser.avatar ); } closeEdit(); } catch (e) { alert('Error al guardar: ' + e.message); } finally { setSavingEdit(false); } }; const handleDeleteProduct = async (p) => { if (!p) return; if (!window.confirm(`¿Eliminar "${p.nombre}"? Esta acción no se puede deshacer.`)) return; try { await window.MOCK.deleteProduct(p.id, currentUser); if (currentUser) { window.MOCK.addAuditEntry( 'Eliminó producto', `${p.nombre} de ${p.sucursalName || p.sucursal}`, currentUser.name, currentUser.role, currentUser.avatar ); } } catch (e) { alert('Error al eliminar: ' + e.message); } }; const catCounts = {}; products.forEach(p => { const c = getCat1(p); if (c) catCounts[c] = (catCounts[c] || 0) + 1; }); const categoriaOpts = [ { value: 'Todos', label: 'Todos' }, ...Object.keys(catCounts).sort().map(c => ({ value: c, label: `${c} (${catCounts[c]})` })), ]; const FILTER_DEFS = [ { key: 'categoria', label: 'Categoría', icon: 'M4 6h16M4 12h16M4 18h16', opts: categoriaOpts, wide: true }, { key: 'proveedor', label: 'Proveedor', icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4', opts: ['Todos', ...new Set(products.map(p => p.proveedor).filter(Boolean).sort())] }, { key: 'sucursal', label: 'Sucursal', icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4', opts: [{ value: 'Todos', label: 'Todos' }, ...window.MOCK.SUCURSALES.map(s => ({ value: s.id, label: s.name }))] }, { key: 'origen', label: 'Origen', icon: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z', opts: ['Todos', ...window.MOCK.ORIGENES], hidden: true }, { key: 'abc', label: 'Análisis ABC', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', opts: ['Todos', ...window.MOCK.ABC_VALUES], hidden: true }, { key: 'etiqueta', label: 'Etiqueta', icon: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z', opts: ['Todos', ...window.MOCK.ETIQUETAS], hidden: true }, { key: 'abcSucursal', label: 'ABC Sucursal', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', opts: ['Todos', ...window.MOCK.ABC_VALUES], hidden: true }, { key: 'cobertura', label: 'Cobertura', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', opts: ['Todos', ...window.MOCK.COBERTURA_RANGES] }, { key: 'estado', label: 'Estado', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', opts: ['Todos', ...window.MOCK.ESTADOS.map(e => e.id)] }, { key: 'tipo', label: 'Tipo', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10', opts: ['Todos', 'vencido', 'dañado'] }, { key: 'accion', label: 'Plan de acción', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2', opts: ['Todos', ...new Set(products.map(p => p.accion).filter(Boolean).sort())] }, { key: 'vencido', label: 'Ya vencidos', icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z', opts: ['Todos', 'Si'] }, ]; const ts = { container: { padding: '24px', maxWidth: '100%', margin: '0 auto' }, toolbar: { display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center', }, searchWrap: { flex: '1 1 280px', minWidth: 200 }, filterToggle: { display: 'flex', alignItems: 'center', gap: 6, padding: '10px 16px', borderRadius: 'var(--radius-md)', background: showFilters ? 'var(--accent-subtle)' : 'var(--bg-tertiary)', color: showFilters ? 'var(--accent)' : 'var(--text-secondary)', border: `1px solid ${showFilters ? 'var(--accent-muted)' : 'var(--border-color)'}`, cursor: 'pointer', fontWeight: 600, fontSize: 13, fontFamily: 'var(--font-body)', transition: 'all var(--transition-fast)', position: 'relative', }, filterBadge: { position: 'absolute', top: -6, right: -6, width: 18, height: 18, borderRadius: '50%', background: 'var(--accent)', color: 'white', fontSize: 10, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', }, filtersPanel: { display: showFilters ? 'flex' : 'none', flexDirection: 'column', gap: 12, marginBottom: 20, padding: '16px 18px', background: 'var(--card-bg)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-lg)', animation: showFilters ? 'slideDown 0.25s ease both' : 'none', position: 'relative', zIndex: 40, }, filtersGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(170px, 1fr))', gap: 10, }, filterItem: { display: 'flex', flexDirection: 'column', gap: 4, }, filterLabel: { fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.05em', display: 'flex', alignItems: 'center', gap: 4, }, filterClear: { display: 'flex', justifyContent: 'flex-end', }, resultBar: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, fontSize: 13, color: 'var(--text-secondary)', }, tableWrap: { overflow: 'hidden', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-color)', background: 'var(--card-bg)', }, table: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, th: { textAlign: 'left', padding: '12px 14px', fontWeight: 600, color: 'var(--text-secondary)', borderBottom: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap', fontSize: 12, position: 'sticky', top: 0, zIndex: 2, }, td: { padding: '10px 14px', borderBottom: '1px solid var(--border-subtle)', verticalAlign: 'middle', whiteSpace: 'nowrap', }, row: { transition: 'background var(--transition-fast)' }, pager: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderTop: '1px solid var(--border-color)', fontSize: 13, color: 'var(--text-secondary)', }, pageBtn: (disabled) => ({ padding: '6px 14px', borderRadius: 'var(--radius-sm)', background: disabled ? 'var(--bg-tertiary)' : 'var(--bg-secondary)', border: '1px solid var(--border-color)', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.4 : 1, fontFamily: 'var(--font-body)', fontSize: 13, color: 'var(--text-primary)', }), statusDot: (estado) => { const e = window.MOCK.ESTADOS.find(x => x.id === estado); return { width: 8, height: 8, borderRadius: '50%', display: 'inline-block', marginRight: 6, background: e ? e.color : 'var(--text-tertiary)' }; }, tipoBadge: (tipo) => ({ padding: '2px 8px', borderRadius: 'var(--radius-full)', fontSize: 11, fontWeight: 600, background: tipo === 'dañado' ? 'var(--status-danger-bg)' : 'var(--status-warning-bg)', color: tipo === 'dañado' ? 'var(--status-danger)' : 'var(--status-warning)', }), accionBadge: (accion) => { let bg = 'var(--bg-tertiary)'; let color = 'var(--text-secondary)'; let border = '1px solid var(--border-color)'; const acc = (accion || '').toLowerCase(); if (acc.includes('descuento') || acc.includes('impulsar')) { bg = 'var(--status-warning-bg)'; color = 'var(--status-warning)'; border = '1px solid oklch(0.75 0.14 80 / 0.15)'; } else if (acc.includes('traspaso') || acc.includes('cambio')) { bg = 'var(--status-info-bg)'; color = 'var(--status-info)'; border = '1px solid oklch(0.7 0.13 250 / 0.15)'; } else if (acc.includes('baja') || acc.includes('tester')) { bg = 'var(--status-danger-bg)'; color = 'var(--status-danger)'; border = '1px solid oklch(0.7 0.18 25 / 0.15)'; } return { display: 'inline-block', padding: '4px 10px', borderRadius: 'var(--radius-sm)', fontSize: '11px', fontWeight: 700, background: bg, color: color, border: border, whiteSpace: 'normal', lineHeight: '1.3', maxWidth: '220px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', }; }, actionButtons: { display: 'flex', gap: 6 }, actionBtn: { border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-secondary)', borderRadius: 'var(--radius-sm)', fontSize: 11, padding: '4px 8px', cursor: 'pointer', }, modalOverlay: { position: 'fixed', inset: 0, zIndex: 1200, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, }, modalCard: { width: '100%', maxWidth: 520, background: 'var(--card-bg)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-xl)', padding: 20, display: 'flex', flexDirection: 'column', gap: 12, }, modalTitle: { fontSize: 16, fontWeight: 700 }, modalGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }, modalFooter: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 4 }, }; return (
setFilter('search', e.target.value)} />
{activeFilterCount > 0 &&
{activeFilterCount}
}
{FILTER_DEFS.filter(f => !f.hidden).map(f => { const fActive = filters[f.key] && filters[f.key] !== 'Todos'; return (
{f.label}
setFilter(f.key, val)} active={fActive} style={{ fontSize: 13 }} options={f.opts.map(o => (typeof o === 'object' ? o : { value: o, label: o }))} />
); })}
{activeFilterCount > 0 && (
)}
{filtered.length} productos encontrados Página {page + 1} de {totalPages || 1}
{[ ['nombre', 'Producto'], ['proveedor', 'Proveedor'], ['sucursalName', 'Sucursal'], ['tipo', 'Tipo'], ['cantidad', 'Cant.'], ['fechaVencimiento', 'Vence'], ['diasRestantes', 'Vence en'], ['stock', 'Stock'], ['coverageDays', 'Cobertura'], ['estado', 'Estado'], ['accion', 'Acción'], ...(canManageProducts ? [['admin_actions', 'Acciones']] : []), ].map(([k, label]) => ( ))} {paged.length === 0 && ( )} {paged.map(p => ( e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> {canManageProducts && ( )} ))}
k !== 'admin_actions' && toggleSort(k)}> {label}{k !== 'admin_actions' && }
Sin productos
No hay productos que coincidan con los filtros aplicados.
{p.nombre}
{p.codigo}
{p.proveedor} {p.sucursalName} {p.tipo === 'vencido' ? 'Vencido' : 'Dañado'} {p.cantidad} {p.fechaVencimiento} {p.diasRestantes}d {(() => { const cat = getCatalogStock(p); const stock = cat ? cat.stock : p.stock; return stock > 0 ? {stock} u : ; })()} {(() => { const cat = getCatalogStock(p); const days = cat ? cat.coverageDays : p.coverageDays; return days > 0 ? 120 ? 'var(--status-danger-bg)' : days > 60 ? 'var(--status-warning-bg)' : 'var(--status-success-bg)', color: days > 120 ? 'var(--status-danger)' : days > 60 ? 'var(--status-warning)' : 'var(--status-success)', }}>{days}d : ; })()} {window.MOCK.ESTADOS.find(x => x.id === p.estado)?.label || p.estado} {p.accion || 'Sin acción'}
{page * pageSize + 1}–{Math.min((page + 1) * pageSize, sorted.length)} de {sorted.length}
{canManageProducts && editingProduct && (
e.stopPropagation()}>
Editar producto
{editingProduct.nombre}
setEditForm(f => ({ ...f, cantidad: e.target.value }))} />
setEditForm(f => ({ ...f, tipo: val, estadoDanado: val === 'dañado' ? f.estadoDanado : '' }))} options={[{ value: 'vencido', label: 'Vencido' }, { value: 'dañado', label: 'Dañado' }]} />
setEditForm(f => ({ ...f, estado: val }))} options={window.MOCK.ESTADOS.map(e => ({ value: e.id, label: e.label }))} />
setEditForm(f => ({ ...f, fechaVencimiento: e.target.value }))} />
setEditForm(f => ({ ...f, proveedor: e.target.value }))} placeholder="Nombre del proveedor..." />
setEditForm(f => ({ ...f, codigo: e.target.value }))} placeholder="Código de barras..." />
{editForm.tipo === 'dañado' && (
setEditForm(f => ({ ...f, estadoDanado: val }))} placeholder="Seleccionar..." options={[ { value: '', label: 'Sin definir' }, { value: 'USO PARA TESTER', label: 'USO PARA TESTER' }, { value: 'DESECHO/INSERVIBLE', label: 'DESECHO/INSERVIBLE' }, { value: 'VENTA INTERNA/CLIENTE', label: 'VENTA INTERNA/CLIENTE' }, { value: 'TRASPASO', label: 'TRASPASO' }, ]} />
)}
setEditForm(f => ({ ...f, accion: val }))} options={[ { value: '', label: 'Sin acción' }, ...[...new Set([...window.MOCK.ACCIONES_VENCIDOS, ...window.MOCK.ACCIONES_DANADOS, 'Impulsar venta en sala'])].map(a => ({ value: a, label: a })), ]} />
Se recalcula sola si cambias Tipo, Proveedor, Estado dañado, Código o Vence. Elige un valor aquí solo para forzar una excepción manual.
setEditForm(f => ({ ...f, nota: e.target.value }))} placeholder="Observación..." />
)}
); }; window.DataTableView = DataTableView;