import React, { useState, useEffect, useMemo, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import { LayoutDashboard, Package, PackagePlus, ClipboardList, Users, Wrench, Building2, BarChart3, Settings, ArrowLeft, Plus, Search, X, Upload, Download, FileText, Trash2, Edit2, Check, AlertTriangle, Image as ImageIcon, ChevronLeft, Save, RotateCcw, Loader2, Printer, Ban, FileSpreadsheet, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Inbox, } from 'lucide-react'; const DB = window.InvDB; /* ============================================================================ Utilidades y componentes chicos reutilizados en todas las pantallas ============================================================================ */ function clsx(...xs) { return xs.filter(Boolean).join(' '); } function useToasts() { const [toasts, setToasts] = useState([]); function toast(mensaje, tipo = 'ok') { const id = DB.uid(); setToasts((t) => [...t, { id, mensaje, tipo }]); setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3200); } const ToastHost = () => (
{toasts.map((t) => (
{t.mensaje}
))}
); return { toast, ToastHost }; } function ConfirmModal({ abierto, titulo, mensaje, textoConfirmar = 'Confirmar', peligro, onConfirmar, onCancelar }) { if (!abierto) return null; return (

{titulo}

{mensaje}
); } function EmptyState({ icono: Icono, titulo, descripcion, accion }) { return (

{titulo}

{descripcion &&

{descripcion}

} {accion}
); } function Badge({ color, children }) { return ( {children} ); } function EstadoStock({ stock, minimo }) { let color = 'bg-green-100 text-green-700', texto = 'OK'; if (stock <= 0) { color = 'bg-red-100 text-red-700'; texto = 'Agotado'; } else if (stock <= minimo) { color = 'bg-red-100 text-red-700'; texto = 'Bajo mínimo'; } else if (stock <= minimo * 1.3) { color = 'bg-amber-100 text-amber-700'; texto = 'Por agotarse'; } return {texto}; } /* ============================================================================ Encabezado + barra lateral ============================================================================ */ const MODULOS = [ { id: 'panel', label: 'Panel', icono: LayoutDashboard }, { id: 'productos', label: 'Productos', icono: Package }, { id: 'entradas', label: 'Entradas', icono: PackagePlus }, { id: 'entregas', label: 'Entregas', icono: ClipboardList }, { id: 'requisiciones', label: 'Requisiciones', icono: Inbox }, { id: 'colaboradores', label: 'Colaboradores', icono: Users }, { id: 'ajustes', label: 'Ajustes', icono: Wrench }, { id: 'proveedores', label: 'Proveedores', icono: Building2 }, { id: 'reportes', label: 'Reportes', icono: BarChart3 }, { id: 'configuracion', label: 'Configuración', icono: Settings }, ]; function Sidebar({ vista, irA, config }) { return ( ); } /* ============================================================================ Pantalla de bienvenida (primera vez: semilla o vacío) ============================================================================ */ function PantallaBienvenida({ onListo, toast }) { const [cargando, setCargando] = useState(false); async function conSemilla() { setCargando(true); try { const { entrada } = await DB.cargarSemilla(); toast(`Catálogo de ejemplo cargado · Entrada ${entrada.consecutivo} por ${DB.money(entrada.total)}`); onListo(); } catch (e) { toast('No se pudo cargar la semilla: ' + e.message, 'error'); setCargando(false); } } async function sinSemilla() { setCargando(true); await DB.iniciarVacio(); toast('Listo, empezaste en limpio.'); onListo(); } return (
Ecocosecha

Inventario de Insumos

Primera vez que abres esta herramienta en este navegador. ¿Cómo quieres empezar?

Todo se guarda en este navegador (IndexedDB). Puedes exportar un respaldo desde Configuración.

); } /* ============================================================================ Panel principal (versión Fase 1 — tarjetas y alertas; gráficas en Fase 4) ============================================================================ */ function Panel({ productos, categorias, irA }) { const valorInventario = productos.reduce((s, p) => s + (p.stockActual || 0) * (p.costoPromedio || 0), 0); const activos = productos.filter((p) => p.activo); const bajoMinimo = activos.filter((p) => (p.stockActual || 0) <= (p.stockMinimo || 0)); const tarjetas = [ { label: 'Valor del inventario', valor: DB.money(valorInventario), color: 'text-green-700' }, { label: 'Productos activos', valor: activos.length, color: 'text-slate-800' }, { label: 'Bajo stock mínimo', valor: bajoMinimo.length, color: bajoMinimo.length ? 'text-red-600' : 'text-slate-800' }, { label: 'Entregas del mes', valor: '—', color: 'text-slate-400', nota: 'Disponible en la Fase 3' }, { label: 'Actas sin firmar', valor: '—', color: 'text-slate-400', nota: 'Disponible en la Fase 3' }, ]; return (

Panel

Resumen del inventario de insumos de Ecocosecha SAS

{tarjetas.map((t) => (
{t.label}
{t.valor}
{t.nota &&
{t.nota}
}
))}

Alertas de stock

{bajoMinimo.length === 0 ? (

No hay productos por debajo del stock mínimo.

) : ( )}
Las gráficas de consumo mensual, top de productos entregados y consumo por área llegan en la Fase 4, cuando ya existan entregas registradas.
); } /* ============================================================================ Productos — catálogo, formulario y fichas técnicas ============================================================================ */ const UNIDADES_SUGERIDAS = ['UNIDAD', 'PAQUETE', 'CAJA', 'PACA', 'GALÓN', 'RESMA', 'LIBRA', 'FRASCO', 'ROLLO', 'BOLSA']; const TIPOS_FICHA = ['Ficha técnica', 'Hoja de seguridad SDS/FDS', 'Certificado', 'Otro']; const MIME_PERMITIDOS = ['application/pdf', 'image/jpeg', 'image/png', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; function ProductoForm({ producto, categorias, onGuardar, onCancelar, modoCrear }) { const esNuevo = modoCrear || !producto; const [f, setF] = useState( producto || { codigoProveedor: '', descripcion: '', descripcionCorta: '', categoriaId: categorias[0]?.id || '', marca: '', referencia: '', unidadCompra: 'UNIDAD', unidadEntrega: 'UNIDAD', factorConversion: 1, ivaPct: 19, stockMinimo: 0, ubicacion: '', requiereFirma: true, esEPP: false, vidaUtilDias: '', activo: true, } ); const campo = (k) => (e) => setF((s) => ({ ...s, [k]: e.target.value })); const campoBool = (k) => (e) => setF((s) => ({ ...s, [k]: e.target.checked })); function submit(e) { e.preventDefault(); if (!f.descripcion.trim()) return; onGuardar({ ...f, factorConversion: Number(f.factorConversion) || 1, ivaPct: Number(f.ivaPct) || 0, stockMinimo: Number(f.stockMinimo) || 0, vidaUtilDias: f.esEPP && f.vidaUtilDias !== '' ? Number(f.vidaUtilDias) : null, }); } const inputCls = 'w-full p-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600'; const labelCls = 'block text-xs font-semibold text-slate-600 mb-1'; return (
{UNIDADES_SUGERIDAS.map((u) =>

Ej: 1 PACA = {f.factorConversion || 1} {f.unidadEntrega || 'unidades de entrega'}.

{f.esEPP && (
)}
); } function FichasTecnicas({ producto, toast }) { const [fichas, setFichas] = useState([]); const [cargando, setCargando] = useState(true); const [subiendo, setSubiendo] = useState(false); const [tipo, setTipo] = useState('Ficha técnica'); const [vencimiento, setVencimiento] = useState(''); const [config, setConfig] = useState(null); const [confirmarBorrar, setConfirmarBorrar] = useState(null); const inputRef = useRef(null); async function cargar() { setCargando(true); const [fs, cfg] = await Promise.all([DB.listarFichas(producto.id), DB.getConfiguracion()]); setFichas(fs); setConfig(cfg); setCargando(false); } useEffect(() => { cargar(); /* eslint-disable-next-line */ }, [producto.id]); async function subirArchivo(file) { if (!file) return; const maxMB = config?.tamanoMaxArchivoMB || 10; if (!MIME_PERMITIDOS.includes(file.type)) { toast('Formato no permitido. Usa PDF, JPG, PNG o DOCX.', 'error'); return; } if (file.size > maxMB * 1024 * 1024) { toast(`El archivo pesa más de ${maxMB} MB.`, 'error'); return; } setSubiendo(true); try { const mismasFichas = fichas.filter((f) => f.tipo === tipo); const version = mismasFichas.length ? Math.max(...mismasFichas.map((f) => f.version || 1)) + 1 : 1; await DB.guardarFicha(producto.id, file, { tipo, version, fechaVencimiento: vencimiento || null }); toast('Documento cargado.'); setVencimiento(''); await cargar(); } catch (e) { toast('No se pudo cargar el archivo: ' + e.message, 'error'); } setSubiendo(false); } function verArchivo(ficha) { const url = URL.createObjectURL(ficha.archivo); window.open(url, '_blank'); setTimeout(() => URL.revokeObjectURL(url), 30000); } function descargarArchivo(ficha) { const url = URL.createObjectURL(ficha.archivo); const a = document.createElement('a'); a.href = url; a.download = ficha.nombreArchivo; a.click(); setTimeout(() => URL.revokeObjectURL(url), 4000); } async function borrarFicha(id) { await DB.eliminarFicha(id); setConfirmarBorrar(null); toast('Documento eliminado.'); cargar(); } return (
e.preventDefault()} onDrop={(e) => { e.preventDefault(); subirArchivo(e.dataTransfer.files?.[0]); }} >

Arrastra el archivo aquí, o elígelo del equipo (PDF, JPG, PNG o DOCX)

setVencimiento(e.target.value)} title="Fecha de vencimiento (opcional)" className="p-2 border border-slate-300 rounded-lg text-sm bg-white" /> { subirArchivo(e.target.files?.[0]); e.target.value = ''; }} />
{cargando ? (

Cargando documentos…

) : fichas.length === 0 ? ( ) : ( )} setConfirmarBorrar(null)} onConfirmar={() => borrarFicha(confirmarBorrar)} />
); } const ETIQUETA_TIPO_MOV = { ENTRADA: ['Entrada', '#16a34a'], SALIDA: ['Salida', '#dc2626'], AJUSTE_POSITIVO: ['Ajuste (+)', '#2563eb'], AJUSTE_NEGATIVO: ['Ajuste (−)', '#b45309'], ANULACION: ['Anulación', '#64748b'], }; function Kardex({ producto }) { const [movimientos, setMovimientos] = useState([]); const [desde, setDesde] = useState(''); const [hasta, setHasta] = useState(''); useEffect(() => { DB.queryIndex('movimientos', 'productoId', producto.id).then((ms) => setMovimientos(ms.sort((a, b) => new Date(a.fecha) - new Date(b.fecha))) ); }, [producto.id]); const filtrados = movimientos.filter((m) => { const dia = (m.fecha || '').slice(0, 10); if (desde && dia < desde) return false; if (hasta && dia > hasta) return false; return true; }); function exportar() { const filas = [ ['Fecha', 'Tipo', 'Documento', 'Cantidad', 'Costo unitario', 'Saldo cantidad', 'Costo promedio', 'Saldo valor', 'Observación'], ...filtrados.map((m) => [ DB.fechaCO(m.fecha), ETIQUETA_TIPO_MOV[m.tipo]?.[0] || m.tipo, `${m.documentoTipo || ''} ${m.consecutivo || ''}`.trim(), m.cantidad, m.costoUnitario, m.saldoCantidad, m.saldoCostoPromedio, m.saldoValor, m.observacion || '', ]), ]; const ws = window.XLSX.utils.aoa_to_sheet(filas); const wb = window.XLSX.utils.book_new(); window.XLSX.utils.book_append_sheet(wb, ws, 'Kardex'); window.XLSX.writeFile(wb, `Kardex_${producto.codigoInterno}_${DB.hoyBogota()}.xlsx`); } return (
setDesde(e.target.value)} className="p-2 border border-slate-300 rounded-lg text-sm" />
setHasta(e.target.value)} className="p-2 border border-slate-300 rounded-lg text-sm" />
{filtrados.length === 0 ? ( ) : (
{filtrados.map((m) => { const [etiqueta, color] = ETIQUETA_TIPO_MOV[m.tipo] || [m.tipo, '#64748b']; return ( ); })}
FechaTipoDocumento CantidadCosto unit. Saldo cant.Costo prom.Saldo valor
{DB.fechaCO(m.fecha)} {etiqueta} {m.documentoTipo} {m.consecutivo} = 0 ? 'text-green-700' : 'text-red-600')}>{m.cantidad >= 0 ? '+' : ''}{m.cantidad} {DB.money(m.costoUnitario)} {m.saldoCantidad} {DB.money(m.saldoCostoPromedio)} {DB.money(m.saldoValor)}
)}
); } function ProductoDetalle({ producto, categoria, onVolver, onActualizado, toast }) { const [tab, setTab] = useState('info'); const [editando, setEditando] = useState(false); const [conteoFichas, setConteoFichas] = useState(null); useEffect(() => { DB.listarFichas(producto.id).then((f) => setConteoFichas(f.length)); }, [producto.id, tab]); async function guardar(datos) { const actualizado = await DB.actualizarProducto(producto.id, datos); toast('Producto actualizado.'); setEditando(false); onActualizado(actualizado); } const TABS = [ { id: 'info', label: 'Información' }, { id: 'fichas', label: `Fichas técnicas${conteoFichas ? ` (${conteoFichas})` : ''}` }, { id: 'kardex', label: 'Kardex' }, { id: 'entregas', label: 'Entregas' }, ]; return (
{producto.codigoInterno} {categoria && {categoria.nombre}} {!producto.activo && Inactivo} {producto.esEPP && EPP}

{producto.descripcion}

{!editando && ( )}
Stock actual
{producto.stockActual} {producto.unidadEntrega}
Costo promedio
{DB.money(producto.costoPromedio)}
Valor en inventario
{DB.money(producto.stockActual * producto.costoPromedio)}
Stock mínimo
{producto.stockMinimo} {producto.unidadEntrega}
{TABS.map((t) => ( ))}
{tab === 'info' && (editando ? ( setEditando(false)} /> ) : (
{[ ['Código proveedor', producto.codigoProveedor || '—'], ['Marca', producto.marca || '—'], ['Referencia', producto.referencia || '—'], ['Unidad de compra', producto.unidadCompra], ['Unidad de entrega', producto.unidadEntrega], ['Factor de conversión', `1 ${producto.unidadCompra} = ${producto.factorConversion} ${producto.unidadEntrega}`], ['IVA', `${producto.ivaPct}%`], ['Ubicación', producto.ubicacion || '—'], ['Requiere firma', producto.requiereFirma ? 'Sí' : 'No'], ['Vida útil EPP', producto.esEPP ? (producto.vidaUtilDias ? `${producto.vidaUtilDias} días` : 'No definida') : '—'], ].map(([k, v]) => (
{k}
{v}
))}
))} {tab === 'fichas' && } {tab === 'kardex' && } {tab === 'entregas' && ( )}
); } function Productos({ productos, categorias, recargar, toast }) { const [busqueda, setBusqueda] = useState(''); const [filtroCategoria, setFiltroCategoria] = useState(''); const [filtroStock, setFiltroStock] = useState(''); const [seleccionado, setSeleccionado] = useState(null); const [modalNuevo, setModalNuevo] = useState(false); const [importando, setImportando] = useState(false); const [confirmarInactivar, setConfirmarInactivar] = useState(null); const catPorId = useMemo(() => Object.fromEntries(categorias.map((c) => [c.id, c])), [categorias]); const filtrados = useMemo(() => { const q = busqueda.trim().toLowerCase(); return productos.filter((p) => { if (filtroCategoria && p.categoriaId !== filtroCategoria) return false; if (filtroStock === 'bajo' && !((p.stockActual || 0) <= (p.stockMinimo || 0))) return false; if (filtroStock === 'ok' && (p.stockActual || 0) <= (p.stockMinimo || 0)) return false; if (!q) return true; return (p.descripcion + ' ' + p.codigoProveedor + ' ' + p.codigoInterno + ' ' + (p.marca || '')).toLowerCase().includes(q); }).sort((a, b) => a.descripcion.localeCompare(b.descripcion, 'es')); }, [productos, busqueda, filtroCategoria, filtroStock]); async function crear(datos) { const p = await DB.crearProducto(datos); toast(`Producto creado: ${p.codigoInterno}`); setModalNuevo(false); await recargar(); } async function inactivar(id) { await DB.actualizarProducto(id, { activo: false }); setConfirmarInactivar(null); toast('Producto inactivado.'); await recargar(); } if (seleccionado) { const actual = productos.find((p) => p.id === seleccionado.id) || seleccionado; return ( setSeleccionado(null)} onActualizado={async () => { await recargar(); }} toast={toast} /> ); } return (

Productos

{productos.length} en el catálogo · {productos.filter(p=>p.activo).length} activos

setBusqueda(e.target.value)} placeholder="Buscar por código, descripción o marca…" className="w-full pl-9 pr-3 py-2 border border-slate-300 rounded-lg text-sm" />
{filtrados.length === 0 ? ( ) : (
{filtrados.map((p) => { const cat = catPorId[p.categoriaId]; return ( setSeleccionado(p)}> ); })}
Código Descripción Categoría Unidad Stock Mínimo Costo prom. Valor Ficha
{p.codigoInterno}
{p.codigoProveedor}
{p.descripcion} {cat && {cat.nombre}} {p.unidadEntrega} {p.stockActual} {p.stockMinimo} {DB.money(p.costoPromedio)} {DB.money(p.stockActual * p.costoPromedio)} e.stopPropagation()}> {p.activo && ( )}
)}
{modalNuevo && (

Nuevo producto

setModalNuevo(false)} />
)} setConfirmarInactivar(null)} onConfirmar={() => inactivar(confirmarInactivar)} /> {importando && ( setImportando(false)} /> )}
); } function FichaIndicador({ productoId }) { const [tiene, setTiene] = useState(null); useEffect(() => { DB.listarFichas(productoId).then((f) => setTiene(f.length > 0)); }, [productoId]); if (tiene === null) return null; return tiene ? : —; } /* ============================================================================ Buscador de producto (por código de proveedor, código interno o descripción) ============================================================================ */ function BuscadorProducto({ productos, onElegir, placeholder }) { const [q, setQ] = useState(''); const [abierto, setAbierto] = useState(false); const ref = useRef(null); useEffect(() => { function fuera(e) { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); } document.addEventListener('mousedown', fuera); return () => document.removeEventListener('mousedown', fuera); }, []); const norm = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); const term = norm(q.trim()); const resultados = term ? productos.filter((p) => norm(`${p.codigoProveedor} ${p.codigoInterno} ${p.descripcion} ${p.marca || ''}`).includes(term)).slice(0, 30) : []; return (
{ setQ(e.target.value); setAbierto(true); }} onFocus={() => setAbierto(true)} placeholder={placeholder || 'Buscar producto por código o descripción…'} className="w-full pl-9 pr-3 py-2 border border-slate-300 rounded-lg text-sm" />
{abierto && resultados.length > 0 && (
    {resultados.map((p) => (
  • ))}
)}
); } /* ============================================================================ Entradas — recepción de mercancía del proveedor ============================================================================ */ function LineaEntradaEditor({ linea, producto, onCambiar, onQuitar }) { const factor = producto?.factorConversion || 1; const cantidadEntrega = DB.round0((linea.cantidadCompra || 0) * factor * 1000) / 1000; const valorLinea = DB.round0((linea.cantidadCompra || 0) * (linea.valorUnitarioCompra || 0)); const inputCls = 'w-full p-1.5 border border-slate-300 rounded text-sm text-right'; return (
{producto?.descripcion}
{producto?.codigoProveedor} · {producto?.unidadCompra} → {producto?.unidadEntrega} (×{factor})
onCambiar({ ...linea, cantidadCompra: Number(e.target.value) })} /> {DB.round0(cantidadEntrega * 1000) / 1000} {producto?.unidadEntrega} onCambiar({ ...linea, valorUnitarioCompra: Number(e.target.value) })} /> {DB.money(valorLinea)} ); } function EntradaForm({ productos, categorias, proveedores, valoresIniciales, recargarProductos, recargarProveedores, toast, onGuardada, onCancelar }) { const [proveedorId, setProveedorId] = useState(valoresIniciales?.proveedorId || proveedores[0]?.id || ''); const [fechaRecepcion, setFechaRecepcion] = useState(valoresIniciales?.fechaRecepcion || DB.hoyBogota()); const [tipoSoporte, setTipoSoporte] = useState(valoresIniciales?.tipoSoporte || 'Remisión'); const [numeroRemision, setNumeroRemision] = useState(valoresIniciales?.numeroRemision || ''); const [numeroFactura, setNumeroFactura] = useState(valoresIniciales?.numeroFactura || ''); const [numeroPedido, setNumeroPedido] = useState(valoresIniciales?.numeroPedido || ''); const [totalFactura, setTotalFactura] = useState(''); const [observaciones, setObservaciones] = useState(valoresIniciales?.observaciones || ''); const [recibidoPor, setRecibidoPor] = useState('Director de Operaciones'); const [lineas, setLineas] = useState( (valoresIniciales?.lineas || []).map((l) => ({ ...l, key: DB.uid() })) ); const [adjuntos, setAdjuntos] = useState([]); const [guardando, setGuardando] = useState(false); const [nuevoProveedor, setNuevoProveedor] = useState(false); const [productoRapido, setProductoRapido] = useState(null); // {descripcion, codigoProveedor} pendiente de crear const productosPorId = useMemo(() => Object.fromEntries(productos.map((p) => [p.id, p])), [productos]); function agregarProducto(p) { if (lineas.some((l) => l.productoId === p.id)) { toast('Ese producto ya está en la lista.', 'warn'); return; } setLineas((ls) => [...ls, { key: DB.uid(), productoId: p.id, cantidadCompra: 1, valorUnitarioCompra: 0, ivaPct: p.ivaPct || 0 }]); } function cambiarLinea(key, nueva) { setLineas((ls) => ls.map((l) => (l.key === key ? nueva : l))); } function quitarLinea(key) { setLineas((ls) => ls.filter((l) => l.key !== key)); } const totales = useMemo(() => DB.calcularTotales(lineas.map((l) => ({ ...l, ivaPct: l.ivaPct }))), [lineas]); const diferenciaFactura = totalFactura !== '' ? Math.round(totales.total - Number(totalFactura)) : 0; async function crearProveedorRapido(datos) { const p = { id: DB.uid(), ...datos }; await DB.put('proveedores', p); setProveedorId(p.id); setNuevoProveedor(false); await recargarProveedores(); toast('Proveedor creado.'); } async function crearProductoRapido(datos) { const p = await DB.crearProducto({ ...datos, codigoProveedor: productoRapido.codigoProveedor || datos.codigoProveedor, descripcion: datos.descripcion || productoRapido.descripcion }); await recargarProductos(); setProductoRapido(null); agregarProducto(p); toast(`Producto creado: ${p.codigoInterno}`); } function manejarAdjuntos(files) { const arr = Array.from(files || []); Promise.resolve().then(() => setAdjuntos((a) => [...a, ...arr.map((f) => ({ nombreArchivo: f.name, mime: f.type, archivo: f }))])); } async function guardar(e) { e.preventDefault(); if (!proveedorId) { toast('Elige el proveedor.', 'error'); return; } if (!lineas.length) { toast('Agrega al menos un producto.', 'error'); return; } if (lineas.some((l) => !l.cantidadCompra || l.cantidadCompra <= 0)) { toast('Hay líneas con cantidad en cero.', 'error'); return; } // Advertencia de posible duplicado (misma factura/remisión del mismo proveedor) const entradasProv = await DB.queryIndex('entradas', 'proveedorId', proveedorId); const posibleDup = entradasProv.find( (e) => e.estado !== 'Anulada' && ( (numeroFactura && e.numeroFactura === numeroFactura) || (numeroRemision && e.numeroRemision === numeroRemision) ) ); if (posibleDup) { const continuar = window.confirm( `Ya existe la entrada ${posibleDup.consecutivo} con ese mismo número de factura o remisión para este proveedor. ¿Registrar de todas formas?` ); if (!continuar) return; } setGuardando(true); try { const { entrada } = await DB.registrarEntrada( { proveedorId, fechaRecepcion, tipoSoporte, numeroRemision, numeroFactura, numeroPedido, observaciones, recibidoPor, adjuntos, }, lineas.map((l) => ({ productoId: l.productoId, cantidadCompra: l.cantidadCompra, valorUnitarioCompra: l.valorUnitarioCompra, ivaPct: l.ivaPct, codigoProveedor: productosPorId[l.productoId]?.codigoProveedor })) ); toast(`Entrada ${entrada.consecutivo} guardada por ${DB.money(entrada.total)}.`); await recargarProductos(); onGuardada(entrada); } catch (err) { toast('No se pudo guardar: ' + err.message, 'error'); } setGuardando(false); } const inputCls = 'w-full p-2.5 border border-slate-300 rounded-lg text-sm'; const labelCls = 'block text-xs font-semibold text-slate-600 mb-1'; return ( <>

Datos de la entrada

setFechaRecepcion(e.target.value)} />
setNumeroRemision(e.target.value)} />
setNumeroFactura(e.target.value)} />
setNumeroPedido(e.target.value)} />
setRecibidoPor(e.target.value)} />
manejarAdjuntos(e.target.files)} className="text-sm" />
{adjuntos.length > 0 &&

{adjuntos.length} archivo(s): {adjuntos.map((a) => a.nombreArchivo).join(', ')}

}