Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgs86nj40skweyzindexPlain Text
<!DOCTYPE html>
<html lang="es">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chechare Wash | Administración</title>
<style>
*{box-sizing:border-box}
:root{--navy:#101827;--bg:#f3f5f9;--text:#172033;--muted:#64748b;--line:#e2e8f0;--green:#166534}
body{margin:0;font-family:Arial,sans-serif;background:var(--bg);color:var(--text)}
.app{display:flex;height:100vh;min-height:100vh;overflow:hidden}
.sidebar{width:250px;flex-shrink:0;height:100vh;position:sticky;top:0;overflow-y:auto;background:#101827;color:#fff;padding:22px 14px;display:flex;flex-direction:column;gap:22px}
.brand{padding:0 10px}.brand h1{font-size:19px;margin:0;letter-spacing:.4px}.brand p{font-size:12px;color:#cbd5e1;margin:7px 0 0}
.nav-label{font-size:10px;color:#94a3b8;font-weight:bold;letter-spacing:1.2px;padding:0 10px;margin-bottom:7px}
.nav-btn,.subnav-btn{width:100%;display:flex;align-items:center;gap:10px;text-align:left;border:0;background:transparent;color:#dbe4f0;padding:11px 12px;border-radius:8px;font-size:14px;font-weight:500}
.nav-btn:hover,.subnav-btn:hover{background:#253247;color:white}
.nav-btn.active,.subnav-btn.active{background:#dcece5;color:#183e32;font-weight:bold}
.nav-btn .chevron{margin-left:auto}
.submenu{padding:5px 0 7px 13px}
.subnav-btn{font-size:13px;padding:10px 12px}
.sidebar-foot{margin-top:auto;padding:12px 10px;color:#94a3b8;font-size:11px;line-height:1.5}
.main-area{flex:1;min-width:0;height:100vh;overflow-y:auto;overflow-x:hidden}
header{background:#fff;padding:20px 30px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:16px}
header h2{margin:0;font-size:21px}header p{margin:5px 0 0;color:var(--muted);font-size:13px}
.status-pill{background:#dcfce7;color:#166534;padding:6px 10px;border-radius:20px;font-size:11px;font-weight:bold}
main{max-width:1400px;margin:0 auto;padding:28px}
.welcome{margin-bottom:22px}.welcome h2{margin:0 0 8px;font-size:25px}.welcome p,.description{color:var(--muted);line-height:1.5}
.welcome p{margin:0}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:16px;margin-bottom:22px}
.summary,.card{background:#fff;padding:22px;border-radius:14px;border:1px solid var(--line);box-shadow:0 3px 12px rgba(15,23,42,.04);margin-bottom:20px}
.summary{margin:0}.summary h3{margin:0 0 12px;font-size:13px;color:var(--muted);font-weight:600}.number{font-size:30px;font-weight:bold}
.summary-link{display:block;width:100%;text-align:left;color:inherit;background:white;border:1px solid var(--line);border-radius:14px;padding:22px}
.summary-link:hover{border-color:#94b8a9;transform:translateY(-1px)}
.summary-link h3{margin:0 0 12px;font-size:13px;color:var(--muted)}.summary-link .number{font-size:30px}
.card h2{margin:0 0 10px;font-size:21px}.description{margin-top:0}
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px}
label{display:block;font-size:14px;font-weight:600}
input,select,textarea{display:block;width:100%;padding:12px;margin-top:6px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;font:inherit;color:var(--text)}
textarea{min-height:85px;resize:vertical}
button{border:0;border-radius:8px;padding:12px 18px;cursor:pointer;font-size:14px;font-weight:600}
button:disabled{opacity:.6;cursor:wait}
.primary{background:var(--navy);color:white}.secondary{background:#e2e8f0;color:var(--text)}.entry{background:#166534;color:white}.consume{background:#b91c1c;color:white}.attended{background:#166534;color:white}.pending-btn{background:#b45309;color:white}.incentive-actions{display:flex;gap:8px;align-items:center}.incentive-choice{padding:8px 16px;border:1px solid #cbd5e1;background:#fff;color:#334155;min-width:68px}.incentive-choice.is-si{background:#166534;border-color:#166534;color:#fff}.incentive-choice.is-no{background:#b91c1c;border-color:#b91c1c;color:#fff}.incentive-choice:focus-visible{outline:3px solid #93c5fd;outline-offset:2px}.incentive-bulk{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}.incentive-na{color:#64748b;font-size:13px;font-weight:600}
.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px}
.table-wrap{overflow-x:auto;margin-top:18px}
table{width:100%;border-collapse:collapse;text-align:left;font-size:14px}
th,td{padding:12px;border-bottom:1px solid var(--line);white-space:nowrap}th{background:#f1f5f9}
.notes-table td.note-content{white-space:pre-wrap;overflow-wrap:anywhere;min-width:220px;max-width:400px}
.notes-table td.note-actions{white-space:normal;min-width:170px}
.state-label{display:inline-block;padding:5px 9px;border-radius:20px;font-size:12px;font-weight:bold}.pending{color:#92400e;background:#fef3c7}.done{color:#166534;background:#dcfce7}
.stock-low{color:#b91c1c;font-weight:bold}.stock-ok{color:#166534;font-weight:bold}
.machine-status{display:inline-block;padding:5px 9px;border-radius:20px;font-size:12px;font-weight:bold;background:#e2e8f0;color:#334155}
.machine-status.operativa{background:#dcfce7;color:#166534}.machine-status.respaldo{background:#dbeafe;color:#1d4ed8}
.machine-status.danada{background:#fee2e2;color:#991b1b}.machine-status.reparacion{background:#fef3c7;color:#92400e}
.document-status{display:inline-block;padding:5px 8px;border-radius:16px;font-size:12px;font-weight:bold;background:#dcfce7;color:#166534}.document-status.faltante{background:#fef3c7;color:#92400e}.document-status.vencido{background:#fee2e2;color:#991b1b}.document-status.no-aplica{background:#e2e8f0;color:#475569}
.message{margin-bottom:0;overflow-wrap:anywhere;line-height:1.5}.help{font-size:13px;color:var(--muted);margin-top:8px}
footer{text-align:center;color:var(--muted);font-size:12px;padding:20px 15px 30px}
.view-section[hidden],.submenu[hidden]{display:none!important}
.mobile-menu{display:none;background:var(--navy);color:#fff}
@media(max-width:800px){.sidebar{width:220px}main{padding:20px}header{padding:18px 20px}}
@media(max-width:650px){.app{display:block;height:auto;min-height:100vh;overflow:visible}.sidebar{width:100%;height:auto;position:relative;top:auto;overflow:visible;padding:14px;gap:14px}.main-area{height:auto;overflow:visible}.brand{display:flex;align-items:center;justify-content:space-between}.sidebar nav{display:grid;grid-template-columns:1fr 1fr;gap:3px}.nav-label{grid-column:1/-1}.submenu{grid-column:1/-1;padding-left:10px}.sidebar-foot{display:none}header{padding:16px 18px}main{padding:16px}.card,.summary{padding:17px}.summary-link{padding:17px}th,td{padding:9px}}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand"><h1>CHECHARE WASH</h1><p>Panel de administración</p></div>
<nav aria-label="Menú principal">
<div class="nav-label">MENÚ PRINCIPAL</div>
<button class="nav-btn active" type="button" data-view="inicio"><span>⌂</span> Inicio</button>
<button class="nav-btn" id="toggleInventario" type="button" aria-expanded="true"><span>▣</span> Inventario <span class="chevron" id="chevronInventario">⌄</span></button>
<div class="submenu" id="submenuInventario">
<button class="subnav-btn" type="button" data-view="productos">Registrar producto</button>
<button class="subnav-btn" type="button" data-view="consulta">Consultar productos</button>
<button class="subnav-btn" type="button" data-view="movimientos">Movimientos de inventario</button>
<button class="subnav-btn" type="button" data-view="entrada">Registrar entrada</button>
<button class="subnav-btn" type="button" data-view="consumo">Registrar consumo</button>
</div>
<button class="nav-btn" id="togglePersonal" type="button" aria-expanded="true"><span>♙</span> Personal / Lavadores <span class="chevron" id="chevronPersonal">⌄</span></button>
<div class="submenu" id="submenuPersonal">
<button class="subnav-btn" type="button" data-personal-panel="registrar">Registrar lavador</button>
<button class="subnav-btn" type="button" data-personal-panel="consultar">Consultar lavadores</button>
<button class="subnav-btn" type="button" data-personal-panel="documentos">Documentación</button>
<button class="subnav-btn" type="button" data-personal-panel="estadoDocumentacion">Estado de documentación</button>
<button class="subnav-btn" type="button" data-personal-panel="semanal">Registro semanal</button>
<button class="subnav-btn" type="button" data-personal-panel="incentivo">Incentivo</button>
<button class="subnav-btn" type="button" data-personal-panel="bonos">Bonos semestrales</button>
<button class="subnav-btn" type="button" data-personal-panel="historial">Historial individual</button>
</div>
<button class="nav-btn" id="toggleMaquinas" type="button" aria-expanded="true"><span>⚙</span> Máquinas <span class="chevron" id="chevronMaquinas">⌄</span></button>
<div class="submenu" id="submenuMaquinas">
<button class="subnav-btn" type="button" data-maquina-panel="consulta">Consultar máquinas</button>
<button class="subnav-btn" type="button" data-maquina-panel="registrar">Registrar máquina</button>
<button class="subnav-btn" type="button" data-maquina-panel="asignar">Asignar máquina</button>
<button class="subnav-btn" type="button" data-maquina-panel="devolver">Registrar devolución</button>
<button class="subnav-btn" type="button" data-maquina-panel="estado">Cambiar estado</button>
<button class="subnav-btn" type="button" data-maquina-panel="historial">Historial de movimientos</button>
</div>
<button class="nav-btn" type="button" data-view="enviarNota"><span>✉</span> Enviar nota</button>
<button class="nav-btn" type="button" data-view="notas"><span>☷</span> Bandeja de notas</button>
<button class="nav-btn" type="button" data-view="configuracion"><span>⚙</span> Configuración</button>
</nav>
<div class="sidebar-foot">Chechare Wash<br>Sistema interno de gestión</div>
</aside>
<div class="main-area">
<header><div><h2 id="pageTitle">Inicio</h2><p id="pageSubtitle">Resumen general del inventario y las solicitudes.</p></div><span class="status-pill">SISTEMA INTERNO</span></header>
<main>
<section class="view-section" id="view-inicio">
<div class="welcome"><h2>Resumen general</h2><p>Consulta rápidamente el estado del inventario, las notas y el personal.</p></div>
<div class="cards">
<button class="summary-link" type="button" data-view="consulta"><h3>Productos registrados</h3><div class="number" id="totalProductos">—</div></button>
<button class="summary-link" type="button" data-view="consulta"><h3>Stock bajo</h3><div class="number" id="totalBajos">—</div></button>
<button class="summary-link" type="button" data-view="notas"><h3>Notas pendientes</h3><div class="number" id="totalNotasDashboard">—</div></button>
<button class="summary-link" type="button" data-view="personal"><h3>Lavadores activos</h3><div class="number" id="totalLavadoresDashboard">—</div></button>
<button class="summary-link" type="button" data-alerta="faltantes"><h3>Documentos faltantes</h3><div class="number" id="totalFaltantesDashboard">—</div></button>
<button class="summary-link" type="button" data-alerta="porVencer"><h3>Por vencer en 30 días</h3><div class="number" id="totalPorVencerDashboard">—</div></button>
<button class="summary-link" type="button" data-alerta="vencidos"><h3>Documentos vencidos</h3><div class="number" id="totalVencidosDashboard">—</div></button>
</div>
<section class="card">
<h2>Estado de las máquinas</h2>
<p class="description">Resumen del equipamiento y de las máquinas asignadas actualmente.</p>
<div class="cards">
<button class="summary-link" type="button" data-view="maquinas"><h3>Total de máquinas</h3><div class="number" id="totalMaquinasDashboard">—</div></button>
<button class="summary-link" type="button" data-view="maquinas"><h3>Operativas</h3><div class="number" id="maquinasOperativasDashboard">—</div></button>
<button class="summary-link" type="button" data-view="maquinas"><h3>De respaldo</h3><div class="number" id="maquinasRespaldoDashboard">—</div></button>
<button class="summary-link" type="button" data-view="maquinas"><h3>Con problemas</h3><div class="number" id="maquinasProblemasDashboard">—</div></button>
</div>
<div class="table-wrap">
<table><thead><tr><th>Código</th><th>Máquina</th><th>Estado</th><th>Lavador asignado</th></tr></thead>
<tbody id="tablaResumenMaquinasDashboard"><tr><td colspan="4">Cargando máquinas...</td></tr></tbody>
</table>
</div>
<div class="actions"><button class="secondary" type="button" data-view="maquinas">Abrir módulo de máquinas</button></div>
<p id="mensajeResumenMaquinasDashboard" class="message" role="status" aria-live="polite"></p>
</section>
<section class="card">
<h2>Documentos faltantes por tipo</h2>
<p class="description">Cantidad de lavadores activos a los que les falta cada documento obligatorio.</p>
<div class="table-wrap"><table><thead><tr><th>Documento</th><th>Faltantes</th></tr></thead><tbody id="tablaResumenDocumentos"><tr><td colspan="2">Cargando resumen...</td></tr></tbody></table></div>
<p id="mensajeResumenPersonal" class="message" role="status" aria-live="polite"></p>
</section>
<section class="card">
<h2>Accesos rápidos</h2><p class="description">Entra directamente a las tareas más habituales.</p>
<div class="actions">
<button class="primary" type="button" data-view="productos">Registrar producto</button>
<button class="entry" type="button" data-view="entrada">Registrar entrada</button>
<button class="consume" type="button" data-view="consumo">Registrar consumo</button>
<button class="secondary" type="button" data-view="notas">Ver notas pendientes</button>
</div>
</section>
</section>
<section class="view-section" id="view-productos" hidden>
<section class="card">
<h2>Registrar producto</h2><p class="description">Define la unidad en la que controlarás este producto. Todas las entradas y consumos posteriores usarán esa misma unidad.</p>
<form id="formProducto">
<div class="form-grid">
<label>Código del producto *<input name="codigo" required maxlength="50" placeholder="Ej.: PROD-001"></label>
<label>Nombre del producto *<input name="nombre" required maxlength="150" placeholder="Ej.: Shampoo para autos"></label>
<label>Categoría *<input name="categoria" required maxlength="100" placeholder="Ej.: Limpieza"></label>
<label>Unidad de control *
<select name="unidad" id="unidadProducto" required>
<option value="">Seleccionar unidad</option><option value="Unidad">Unidad</option><option value="Litro">Litro</option><option value="Mililitro">Mililitro</option><option value="Kilogramo">Kilogramo</option><option value="Gramo">Gramo</option><option value="Metro">Metro</option><option value="Centímetro">Centímetro</option><option value="Par">Par</option><option value="Rollo">Rollo</option><option value="Pack">Pack</option><option value="Caja">Caja</option><option value="Bolsa">Bolsa</option><option value="Bidón">Bidón</option><option value="Otro">Otra unidad...</option>
</select>
</label>
<label id="otraUnidadContenedor" hidden>Escribe la unidad *<input id="otraUnidad" maxlength="30" placeholder="Ej.: Paquete"></label>
<label>Stock inicial<input name="stockInicial" type="number" min="0" step="any" value="0" required></label>
<label>Stock mínimo *<input name="stockMinimo" type="number" min="0" step="any" value="0" required></label>
</div>
<p class="help">Ejemplo: shampoo en litros, paños en unidades y bolsas en unidades o bolsas, según cómo quieras controlarlas.</p>
<div class="actions"><button id="btnRegistrar" class="primary" type="submit">Registrar producto</button></div>
<p id="mensajeProducto" class="message" role="status" aria-live="polite"></p>
</form>
</section>
</section>
<section class="view-section" id="view-consulta" hidden>
<section class="card">
<h2>Consultar productos</h2><p class="description">Consulta las existencias actuales y las alertas de reposición.</p>
<div class="actions" style="align-items:end">
<button id="btnActualizar" type="button" class="primary" onclick="cargarInventario()">Actualizar inventario</button>
<label style="min-width:220px;flex:1">Buscar producto<input id="buscarProductoConsulta" type="search" autocomplete="off" placeholder="Código, nombre, categoría o unidad"></label>
<label style="min-width:190px;flex:1">Ordenar por
<select id="ordenProductos">
<option value="codigo">Código</option>
<option value="producto" selected>Nombre del producto</option>
<option value="categoria">Categoría</option>
<option value="unidad">Unidad</option>
<option value="stockActual">Stock actual</option>
<option value="stockMinimo">Stock mínimo</option>
<option value="estado">Estado del stock</option>
</select>
</label>
<label style="min-width:160px">Orden
<select id="direccionOrdenProductos">
<option value="asc" selected>Ascendente (A-Z / menor a mayor)</option>
<option value="desc">Descendente (Z-A / mayor a menor)</option>
</select>
</label>
</div>
<div class="table-wrap"><table><thead><tr><th>Código</th><th>Producto</th><th>Categoría</th><th>Unidad</th><th>Stock actual</th><th>Stock mínimo</th><th>Estado</th></tr></thead><tbody id="tablaProductos"><tr><td colspan="7">Cargando inventario...</td></tr></tbody></table></div>
<p id="estadoInventario" class="message" role="status" aria-live="polite"></p>
</section>
</section>
<section class="view-section" id="view-movimientos" hidden>
<section class="card">
<h2>Movimientos de inventario</h2><p class="description">Selecciona el producto y escribe directamente la cantidad que entra o que consumes. No se realizan conversiones por cajas, bolsas o bidones.</p>
<form id="formMovimiento">
<div class="form-grid">
<label>Buscar producto *<input id="buscarProductoMovimiento" type="search" autocomplete="off" placeholder="Código, nombre, categoría o unidad"></label>
<label>Producto *<select id="productoMovimiento" required><option value="">Cargando productos...</option></select></label>
<label>Tipo de movimiento *<select id="tipoMovimiento" required><option value="Entrada">Entrada: sumar stock</option><option value="Consumo">Consumo: descontar stock</option></select></label>
<label id="etiquetaCantidad">Cantidad que entra *<input id="cantidadMovimiento" type="number" min="0.000001" step="any" required></label>
</div>
<p id="ayudaUnidad" class="help">Selecciona un producto para ver su unidad y stock actual.</p>
<label style="margin-top:16px">Motivo *<input id="motivoMovimiento" maxlength="250" required placeholder="Ej.: Compra, reposición o uso en un vehículo"></label>
<div class="actions"><button id="btnMovimiento" class="entry" type="submit">Registrar entrada</button></div>
<p id="mensajeMovimiento" class="message" role="status" aria-live="polite"></p>
</form>
</section>
<section class="card">
<h2>Historial de movimientos</h2>
<p class="description">Consulta todas las entradas y salidas registradas, con el stock antes y después de cada operación.</p>
<div class="form-grid">
<label>Filtrar por tipo
<select id="filtroTipoMovimiento">
<option value="Todos">Todos los movimientos</option>
<option value="Entrada">Entradas</option>
<option value="Consumo">Consumos</option>
<option value="Stock inicial">Stock inicial</option>
</select>
</label>
<label>Buscar producto
<input id="buscarHistorialMovimiento" type="search" placeholder="Código o nombre del producto">
</label>
<div class="actions" style="align-items:end;margin-top:0">
<button id="btnActualizarHistorialMovimientos" type="button" class="secondary">Actualizar historial</button>
</div>
</div>
<p id="mensajeHistorialMovimientos" class="message" role="status" aria-live="polite"></p>
<div class="table-wrap">
<table>
<thead><tr><th>Fecha y hora</th><th>Tipo</th><th>Código</th><th>Producto</th><th>Cantidad</th><th>Stock anterior</th><th>Stock resultante</th><th>Motivo</th></tr></thead>
<tbody id="tablaHistorialMovimientos"><tr><td colspan="8">Cargando historial...</td></tr></tbody>
</table>
</div>
</section>
</section>
<section class="view-section" id="view-entrada" hidden>
<section class="card"><h2>Registrar entrada</h2><p class="description">Registra una compra o reposición para sumar existencias.</p><button type="button" class="entry" onclick="abrirMovimiento('Entrada')">Abrir formulario de entrada</button></section>
</section>
<section class="view-section" id="view-consumo" hidden>
<section class="card"><h2>Registrar consumo</h2><p class="description">Registra el uso de un producto para descontarlo del stock.</p><button type="button" class="consume" onclick="abrirMovimiento('Consumo')">Abrir formulario de consumo</button></section>
</section>
<section class="view-section" id="view-personal" hidden>
<div class="personal-panel" id="panel-personal-registrar">
<section class="card">
<h2>Registrar lavador</h2>
<p class="description">Registra los datos de la persona y la fecha real en que comenzó a trabajar.</p>
<form id="formLavador">
<div class="form-grid">
<label>Nombre completo del lavador *<input id="nombreLavador" maxlength="150" required placeholder="Nombre y apellido"></label>
<label>Teléfono (opcional)<input id="telefonoLavador" maxlength="50" placeholder="Teléfono de contacto"></label>
<label>Fecha de ingreso laboral *<input id="fechaIngresoLavador" type="date" required></label>
</div>
<div class="actions"><button class="primary" id="btnRegistrarLavador" type="submit">Registrar lavador</button><button class="secondary" id="btnActualizarLavadores" type="button">Actualizar lista</button></div>
<p id="mensajeLavador" class="message" role="status" aria-live="polite"></p>
</form>
</section>
</div>
<div class="personal-panel" id="panel-personal-consultar" hidden>
<section class="card">
<h2>Consultar lavadores</h2>
<p class="description">Consulta el personal registrado, su antigüedad y estado. Desde aquí puedes actualizar la fecha real de ingreso.</p>
<div class="actions"><button class="primary" type="button" id="btnActualizarListaPersonal">Actualizar lista</button></div>
<div class="table-wrap"><table><thead><tr><th>Lavador</th><th>Teléfono</th><th>Fecha de ingreso</th><th>Antigüedad</th><th>Estado</th><th>Evaluación semanal</th><th>Motivo</th><th>Acción</th></tr></thead><tbody id="tablaPersonal"><tr><td colspan="8">Cargando personal...</td></tr></tbody></table></div>
<p id="mensajeListaPersonal" class="message" role="status" aria-live="polite"></p>
</section>
</div>
<div class="personal-panel" id="panel-personal-documentos" hidden>
<section class="card">
<h2>Documentación del lavador</h2>
<p class="description">Selecciona una persona para consultar sus documentos y cargar archivos o renovaciones.</p>
<label>Lavador *
<select id="selectorLavador">
<option value="">Cargando lavadores...</option>
</select>
</label>
<div class="actions" style="margin-top:12px">
<button class="secondary" id="btnEliminarLavador" type="button">Desactivar lavador seleccionado</button><button class="consume" id="btnEliminarLavadorDefinitivo" type="button">Eliminar definitivamente</button>
</div>
<div id="panelDocumentosLavador" hidden>
<div class="table-wrap"><table><thead><tr><th>Documento</th><th>Requisito</th><th>Estado</th><th>Emisión</th><th>Vencimiento</th><th>Archivo</th></tr></thead><tbody id="tablaDocumentosLavador"></tbody></table></div>
<h3 style="margin-top:26px">Cargar o renovar documento</h3>
<form id="formDocumentoLavador">
<div class="form-grid">
<label>Tipo de documento *<select id="tipoDocumentoLavador" required>
<option value="Licencia de conducir">Licencia de conducir</option>
<option value="Seguro">Seguro</option>
<option value="Certificado de antecedentes">Certificado de antecedentes</option>
<option value="Carnet de lavador">Carnet de lavador</option>
</select></label>
<label>Fecha de emisión (opcional)<input id="fechaEmisionDocumento" type="date"></label>
<label>Fecha de vencimiento<input id="fechaVencimientoDocumento" type="date"></label>
<label>Foto o archivo del documento *<input id="archivoDocumentoLavador" type="file" accept="image/*,.pdf" required></label>
</div>
<label style="margin-top:14px"><input id="documentoNoVence" type="checkbox" style="display:inline-block;width:auto;margin-right:8px">No vence</label>
<p class="help">La carga del archivo es obligatoria, incluso cuando marcas «No vence». Formatos recomendados: imagen o PDF, hasta 6 MB.</p>
<div class="actions"><button class="primary" id="btnGuardarDocumentoLavador" type="submit">Guardar documento</button></div>
<p id="mensajeDocumentoLavador" class="message" role="status" aria-live="polite"></p>
</form>
</div>
</section>
<section class="card">
<h2>Alertas de documentación</h2>
<p class="description">El correo diario agrupado se enviará a turnoschechare@gmail.com. Incluirá documentos faltantes, vencidos y avisos de vencimiento a 30, 15 y 7 días.</p>
<div class="actions"><button class="secondary" id="btnProbarAvisoPersonal" type="button">Ejecutar revisión ahora</button></div>
<p id="mensajeAvisoPersonal" class="message" role="status" aria-live="polite"></p>
</section>
</div>
<div class="personal-panel" id="panel-personal-estadoDocumentacion" hidden>
<section class="card">
<h2>Estado de documentación</h2>
<p class="description">Consulta los cuatro documentos de cada lavador activo. Los documentos marcados como «No aplica» no se cuentan como pendientes.</p>
<div class="form-grid">
<label>Buscar lavador<input id="buscarEstadoDocumentacion" type="search" placeholder="Escribe el nombre del lavador"></label>
<label>Documento
<select id="filtroDocumentoEstado">
<option value="Todos">Todos los documentos</option>
<option value="Carnet de lavador">Carnet de lavador</option>
<option value="Licencia de conducir">Licencia de conducir</option>
<option value="Seguro">Seguro</option>
<option value="Certificado de antecedentes">Certificado de antecedentes</option>
</select>
</label>
<label>Estado
<select id="filtroEstadoDocumento">
<option value="Todos">Todos los estados</option>
<option value="Faltante">Faltante</option>
<option value="Vencido">Vencido</option>
<option value="Completo">Completo</option>
<option value="No aplica">No aplica</option>
<option value="Sin datos">Sin datos</option>
</select>
</label>
<div class="actions" style="align-items:end;margin-top:0"><button class="secondary" id="btnActualizarEstadoDocumentacion" type="button">Actualizar estado</button></div>
</div>
<p id="mensajeEstadoDocumentacion" class="message" role="status" aria-live="polite">Cargando estado de documentación...</p>
<div class="table-wrap"><table>
<thead><tr><th>Lavador</th><th>Carnet de lavador</th><th>Licencia de conducir</th><th>Seguro</th><th>Certificado de antecedentes</th><th>Estado general</th></tr></thead>
<tbody id="tablaEstadoDocumentacion"><tr><td colspan="6">Cargando...</td></tr></tbody>
</table></div>
</section>
</div>
<div class="personal-panel" id="panel-personal-semanal" hidden>
<section class="card">
<h2>Registro semanal de incentivos</h2>
<p class="description">Selecciona el lunes de la semana que deseas registrar. Los lavadores excluidos por función y quienes aún no habían ingresado aparecerán automáticamente como No aplica.</p>
<div class="form-grid">
<label>Semana (lunes) *<input type="date" id="fechaSemanaIncentivo"></label>
</div>
<p id="rangoSemanaIncentivo" class="help">Selecciona una fecha para consultar la semana.</p>
<div class="incentive-bulk">
<button class="entry" type="button" id="btnTodosIncentivoSi">Marcar todos Sí</button>
<button class="consume" type="button" id="btnTodosIncentivoNo">Marcar todos No</button>
</div>
<div class="table-wrap"><table><thead><tr><th>Lavador</th><th>Resultado semanal</th></tr></thead><tbody id="tablaIncentivosSemana"><tr><td colspan="2">Selecciona una semana.</td></tr></tbody></table></div>
<div class="actions"><button class="primary" type="button" id="btnGuardarIncentivos">Guardar semana</button></div>
<p id="mensajeIncentivos" class="message" role="status" aria-live="polite"></p>
</section>
</div>
<div class="personal-panel" id="panel-personal-incentivo" hidden>
<section class="card">
<h2>Estado de incentivos</h2>
<p class="description">Consulta quién tiene derecho al 60 % durante una semana determinada. El estado se obtiene del resultado Sí o No de la semana inmediatamente anterior.</p>
<div class="form-grid">
<label>Semana en la que aplica el incentivo *<input type="date" id="fechaConsultaIncentivo"></label>
<label>Buscar lavador<input type="search" id="buscarLavadorIncentivo" placeholder="Escribe parte del nombre, por ejemplo: ale"></label>
<label>Mostrar<select id="filtroEstadoIncentivo"><option value="Todos">Todos</option><option value="Sí">Tiene incentivo (60 %)</option><option value="No">No tiene incentivo (50 %)</option><option value="Sin registrar">Sin resultado registrado</option><option value="No aplica">No aplica</option></select></label>
</div>
<p id="rangoConsultaIncentivo" class="help">Selecciona la semana que deseas consultar.</p>
<p id="resumenConsultaIncentivo" class="help"></p>
<div class="table-wrap"><table><thead><tr><th>Lavador</th><th>Estado del incentivo</th><th>Resultado registrado</th></tr></thead><tbody id="tablaEstadoIncentivo"><tr><td colspan="3">Selecciona una semana.</td></tr></tbody></table></div>
<p id="mensajeConsultaIncentivo" class="message" role="status" aria-live="polite"></p>
</section>
</div>
<div class="personal-panel" id="panel-personal-bonos" hidden>
<section class="card">
<h2>Bonos semestrales</h2>
<p class="description">Cada lavador tiene su propio período de seis meses. Se cuentan las semanas completas registradas con «No». Con 9 o más faltas, el bono no corresponde. Los lavadores configurados como No aplica quedan excluidos de esta evaluación.</p>
<div class="actions"><button class="primary" type="button" id="btnActualizarBonos">Actualizar evaluación</button></div>
<div class="table-wrap"><table><thead><tr><th>Lavador</th><th>Antigüedad</th><th>Inicio del ciclo</th><th>Evaluación</th><th>Último pago</th><th>Semanas sin incentivo</th><th>Estado</th><th>Acción</th></tr></thead><tbody id="tablaBonos"><tr><td colspan="8">Cargando bonos...</td></tr></tbody></table></div>
<p id="mensajeBonos" class="message" role="status" aria-live="polite"></p>
</section>
</div>
<div class="personal-panel" id="panel-personal-historial" hidden>
<section class="card">
<h2>Historial individual</h2>
<p class="description">Consulta los registros semanales y los movimientos de bonos de una persona.</p>
<label>Lavador *<select id="selectorHistorialPersonal"><option value="">Seleccionar lavador</option></select></label>
<div class="table-wrap"><table><thead><tr><th>Semana desde</th><th>Semana hasta</th><th>Incentivo</th><th>Registrado</th></tr></thead><tbody id="tablaHistorialIncentivos"><tr><td colspan="4">Selecciona un lavador.</td></tr></tbody></table></div>
<div class="table-wrap"><table><thead><tr><th>Movimiento</th><th>Fecha</th><th>Período</th><th>Faltas</th><th>Estado</th><th>Observaciones</th></tr></thead><tbody id="tablaHistorialBonos"><tr><td colspan="6">Selecciona un lavador.</td></tr></tbody></table></div>
<p id="mensajeHistorialPersonal" class="message" role="status" aria-live="polite"></p>
</section>
</div>
</section>
<section class="view-section" id="view-enviarNota" hidden>
<section class="card"><h2>Enviar una nota al administrador</h2><p class="description">Comunica una incidencia o una solicitud relacionada con el inventario.</p>
<form id="formSolicitud"><div class="form-grid">
<label>Tu nombre<input id="nombreSolicitud" maxlength="100" placeholder="Nombre"></label>
<label>Categoría *<select id="categoriaSolicitud" required><option value="">Seleccionar categoría</option><option value="Stock">Stock</option><option value="Información">Información</option><option value="Máquina">Máquina</option><option value="Personal">Personal</option><option value="Incentivos / bonos">Incentivos / bonos</option><option value="Otro">Otro</option></select></label>
<label>Nota *<textarea id="textoSolicitud" maxlength="2000" required placeholder="Escribe tu mensaje..."></textarea></label>
</div><div class="actions"><button id="btnSolicitud" class="secondary" type="submit">Enviar nota</button></div><p id="mensajeSolicitud" class="message" role="status" aria-live="polite"></p></form>
</section>
</section>
<section class="view-section" id="view-notas" hidden>
<section class="card"><h2>Bandeja de notas</h2><p class="description">Consulta las notas recibidas y marca cada una como pendiente o atendida.</p>
<div class="cards">
<article class="summary"><h3>Notas pendientes</h3><div class="number" id="totalNotasPendientes">—</div></article>
<article class="summary"><h3>Notas atendidas</h3><div class="number" id="totalNotasAtendidas">—</div></article>
</div>
<div class="actions"><button id="btnActualizarNotas" type="button" class="primary" onclick="cargarSolicitudes()">Actualizar bandeja</button>
<label style="min-width:200px;flex:1">Filtrar notas<select id="filtroNotas"><option value="Todas">Todas las notas</option><option value="Pendiente">Solo pendientes</option><option value="Atendida">Solo atendidas</option></select></label>
</div>
<p id="mensajeBandeja" class="message" role="status" aria-live="polite"></p>
<div class="table-wrap"><table class="notes-table"><thead><tr><th>Fecha</th><th>Persona</th><th>Categoría</th><th>Nota</th><th>Estado</th><th>Acciones</th></tr></thead><tbody id="tablaSolicitudes"><tr><td colspan="5">Cargando notas...</td></tr></tbody></table></div>
</section>
</section>
<section class="view-section" id="view-maquinas" hidden>
<div class="machine-panel" id="panel-maquinas-consulta">
<div class="welcome"><h2>Gestión de máquinas</h2><p>Registra equipos, controla las asignaciones, devoluciones, estados y movimientos históricos.</p></div>
<div class="cards">
<article class="summary"><h3>Total de máquinas</h3><div class="number" id="totalMaquinasModulo">—</div></article>
<article class="summary"><h3>Operativas</h3><div class="number" id="maquinasOperativasModulo">—</div></article>
<article class="summary"><h3>De respaldo</h3><div class="number" id="maquinasRespaldoModulo">—</div></article>
<article class="summary"><h3>Dañadas / En reparación</h3><div class="number" id="maquinasProblemasModulo">—</div></article>
</div>
<section class="card">
<h2>Máquinas registradas</h2>
<p class="description">Consulta el estado actual y quién tiene cada equipo asignado.</p>
<div class="actions"><button class="primary" id="btnActualizarMaquinas" type="button">Actualizar lista</button></div>
<div class="table-wrap"><table><thead><tr><th>Código</th><th>Tipo</th><th>Estado</th><th>Lavador asignado</th><th>Fecha de registro</th></tr></thead><tbody id="tablaMaquinas"><tr><td colspan="5">Cargando máquinas...</td></tr></tbody></table></div>
<p id="mensajeListaMaquinas" class="message" role="status" aria-live="polite"></p>
</section>
</div>
<div class="machine-panel" id="panel-maquinas-registrar" hidden>
<section class="card">
<h2>Registrar máquina</h2><p class="description">El equipo nuevo se registra inicialmente como De respaldo.</p>
<form id="formRegistrarMaquina">
<div class="form-grid">
<label>Código único *<input name="codigo" required maxlength="30" placeholder="Ej.: H2 o A3"></label>
<label>Tipo de máquina *<select name="tipo" required><option value="">Seleccionar tipo</option><option value="Hidrolavadora">Hidrolavadora</option><option value="Aspiradora">Aspiradora</option><option value="Otra">Otra</option></select></label>
</div>
<label style="margin-top:14px">Observaciones<textarea name="observaciones" maxlength="1000" placeholder="Detalles del equipo, accesorios o condición al registrarlo"></textarea></label>
<div class="actions"><button class="primary" id="btnRegistrarMaquina" type="submit">Registrar máquina</button></div>
<p id="mensajeRegistrarMaquina" class="message" role="status" aria-live="polite"></p>
</form>
</section>
</div>
<div class="machine-panel" id="panel-maquinas-asignar" hidden>
<section class="card">
<h2>Asignar máquina a un lavador</h2><p class="description">Solo aparecen lavadores activos y máquinas disponibles para asignación.</p>
<form id="formAsignarMaquina">
<div class="form-grid">
<label>Máquina disponible *<select id="maquinaParaAsignar" required><option value="">Cargando máquinas...</option></select></label>
<label>Lavador activo *<select id="lavadorParaAsignar" required><option value="">Cargando lavadores...</option></select></label>
</div>
<label style="margin-top:14px">Observaciones<textarea id="observacionesAsignacion" maxlength="1000" placeholder="Condición de entrega, accesorios, etc."></textarea></label>
<div class="actions"><button class="primary" id="btnAsignarMaquina" type="submit">Registrar asignación</button></div>
<p id="mensajeAsignarMaquina" class="message" role="status" aria-live="polite"></p>
</form>
</section>
</div>
<div class="machine-panel" id="panel-maquinas-devolver" hidden>
<section class="card">
<h2>Registrar devolución</h2><p class="description">Selecciona una máquina que tenga una asignación activa. Si está dañada o en reparación, conservará ese estado al devolverla.</p>
<form id="formDevolverMaquina">
<div class="form-grid">
<label>Máquina asignada *<select id="maquinaParaDevolver" required><option value="">Cargando máquinas...</option></select></label>
<label>Motivo de devolución *<input id="motivoDevolucionMaquina" required maxlength="250" placeholder="Ej.: Cambio de equipo o fin de asignación"></label>
</div>
<label style="margin-top:14px">Observaciones<textarea id="observacionesDevolucionMaquina" maxlength="1000"></textarea></label>
<div class="actions"><button class="secondary" id="btnDevolverMaquina" type="submit">Registrar devolución</button></div>
<p id="mensajeDevolverMaquina" class="message" role="status" aria-live="polite"></p>
</form>
</section>
</div>
<div class="machine-panel" id="panel-maquinas-estado" hidden>
<section class="card">
<h2>Cambiar estado de una máquina</h2><p class="description">Registra daños, envíos a reparación o la confirmación de un equipo reparado.</p>
<form id="formEstadoMaquina">
<div class="form-grid">
<label>Máquina *<select id="maquinaParaEstado" required><option value="">Seleccionar máquina</option></select></label>
<label>Nuevo estado *<select id="nuevoEstadoMaquina" required><option value="">Seleccionar estado</option><option value="Dañada">Dañada</option><option value="En reparación">En reparación</option><option value="Operativa">Operativa</option><option value="De respaldo">De respaldo</option></select></label>
<label>Motivo *<input id="motivoEstadoMaquina" required maxlength="250" placeholder="Ej.: Equipo dañado / Reparación finalizada"></label>
</div>
<label style="margin-top:14px">Observaciones<textarea id="observacionesEstadoMaquina" maxlength="1000"></textarea></label>
<div class="actions"><button class="secondary" id="btnCambiarEstadoMaquina" type="submit">Actualizar estado</button></div>
<p id="mensajeEstadoMaquina" class="message" role="status" aria-live="polite"></p>
</form>
</section>
</div>
<div class="machine-panel" id="panel-maquinas-historial" hidden>
<section class="card">
<h2>Historial de movimientos</h2><p class="description">Consulta los movimientos por código de máquina o por nombre de lavador. Puedes dejar ambos filtros vacíos para ver todo el historial.</p>
<form id="formHistorialMaquinas">
<div class="form-grid">
<label>Código de máquina<select id="filtroCodigoHistorialMaquinas"><option value="">Todas las máquinas</option></select></label>
<label>Nombre del lavador<select id="filtroLavadorHistorialMaquinas"><option value="">Todos los lavadores</option></select></label>
</div>
<div class="actions"><button class="primary" id="btnBuscarHistorialMaquinas" type="submit">Consultar historial</button></div>
</form>
<p id="mensajeHistorialMaquinas" class="message" role="status" aria-live="polite"></p>
<div class="table-wrap"><table><thead><tr><th>Fecha y hora</th><th>Código</th><th>Movimiento</th><th>Lavador</th><th>Estado anterior</th><th>Estado nuevo</th><th>Motivo</th><th>Observaciones</th></tr></thead><tbody id="tablaHistorialMaquinas"><tr><td colspan="8">Aplica un filtro o consulta todo el historial.</td></tr></tbody></table></div>
</section>
</div>
</section>
<section class="view-section" id="view-configuracion" hidden>
<section class="card"><h2>Configuración</h2><p class="description">En esta etapa, las categorías se escriben al registrar cada producto. Más adelante podemos crear aquí un administrador de categorías y otros ajustes.</p></section>
</section>
<section class="view-section" id="view-detalleDocumentos" hidden>
<section class="card">
<h2 id="tituloDetalleDocumentos">Detalle de documentación</h2>
<p id="descripcionDetalleDocumentos" class="description">Consulta el estado de los documentos del personal.</p>
<div class="actions"><button class="secondary" type="button" data-view="inicio">Volver al resumen</button><button class="primary" type="button" id="btnActualizarDetalleDocumentos">Actualizar lista</button></div>
<p id="mensajeDetalleDocumentos" class="message" role="status" aria-live="polite"></p>
<div class="table-wrap"><table><thead><tr><th>Lavador</th><th>Documento</th><th>Estado</th><th>Fecha de vencimiento</th><th>Acción</th></tr></thead><tbody id="tablaDetalleDocumentos"><tr><td colspan="5">Selecciona una tarjeta del resumen.</td></tr></tbody></table></div>
</section>
</section>
</main>
<footer>Chechare Wash · Sistema interno de gestión</footer>
</div>
</div>
<script>
let productosCache = [];
let solicitudesCache = [];
let tipoAlertaDocumentosActual = 'faltantes';
let lavadorParaSeleccionar = '';
const titulosVista = {
inicio:['Inicio','Resumen general del inventario y las solicitudes.'],
productos:['Registrar producto','Da de alta un producto y define su stock mínimo.'],
consulta:['Consultar productos','Revisa existencias y alertas de reposición.'],
movimientos:['Movimientos de inventario','Registra entradas o consumos y consulta el historial de movimientos.'],
entrada:['Registrar entrada','Suma existencias al inventario.'],
consumo:['Registrar consumo','Descuenta existencias del inventario.'],
personal:['Personal / Lavadores','Administra el personal, los incentivos y los bonos semestrales.'],
maquinas:['Máquinas','Registra equipos, asignaciones, devoluciones y estados.'],
enviarNota:['Enviar nota','Envía una incidencia o solicitud al administrador.'],
notas:['Bandeja de notas','Consulta las notas pendientes y atendidas.'],
configuracion:['Configuración','Ajustes generales del sistema.'],
detalleDocumentos:['Detalle de documentación','Consulta los documentos faltantes y los vencimientos del personal.']
};
function mostrarError(error) {
return error && error.message ? error.message : 'Ocurrió un error desconocido.';
}
function mostrarVista(nombre) {
const vista = titulosVista[nombre] ? nombre : 'inicio';
document.querySelectorAll('.view-section').forEach(function(seccion) {
seccion.hidden = seccion.id !== 'view-' + vista;
});
document.querySelectorAll('[data-view]').forEach(function(boton) {
boton.classList.toggle('active', boton.dataset.view === vista);
});
document.getElementById('toggleMaquinas').classList.toggle('active', vista === 'maquinas');
document.getElementById('togglePersonal').classList.toggle('active', vista === 'personal');
const info = titulosVista[vista];
document.getElementById('pageTitle').textContent = info[0];
document.getElementById('pageSubtitle').textContent = info[1];
if (['productos','consulta','movimientos','entrada','consumo'].includes(vista)) {
document.getElementById('submenuInventario').hidden = false;
document.getElementById('toggleInventario').setAttribute('aria-expanded','true');
document.getElementById('chevronInventario').textContent = '⌄';
}
if (vista === 'consulta') cargarInventario();
if (vista === 'movimientos') { cargarInventario(); cargarHistorialMovimientos(); }
if (vista === 'personal') {
document.getElementById('submenuPersonal').hidden = false;
document.getElementById('togglePersonal').setAttribute('aria-expanded','true');
document.getElementById('chevronPersonal').textContent = '⌄';
abrirPanelPersonal('registrar');
cargarLavadores();
}
if (vista === 'maquinas') {
document.getElementById('submenuMaquinas').hidden = false;
document.getElementById('toggleMaquinas').setAttribute('aria-expanded','true');
document.getElementById('chevronMaquinas').textContent = '⌄';
abrirPanelMaquinas('consulta');
cargarModuloMaquinas();
}
if (vista === 'inicio') cargarResumenMaquinas();
if (vista === 'inicio') cargarResumenPersonal();
if (vista === 'notas') cargarSolicitudes();
if (vista === 'detalleDocumentos') cargarDetalleDocumentos(tipoAlertaDocumentosActual);
if (vista === 'entrada') prepararTipoMovimiento('Entrada');
if (vista === 'consumo') prepararTipoMovimiento('Consumo');
}
document.querySelectorAll('[data-view]').forEach(function(boton) {
boton.addEventListener('click', function() { mostrarVista(boton.dataset.view); });
});
document.querySelectorAll('[data-alerta]').forEach(function(boton) {
boton.addEventListener('click', function() { abrirDetalleDocumentos(boton.dataset.alerta); });
});
document.getElementById('btnActualizarDetalleDocumentos').addEventListener('click', function() {
cargarDetalleDocumentos(tipoAlertaDocumentosActual);
});
document.getElementById('toggleInventario').addEventListener('click', function() {
const submenu = document.getElementById('submenuInventario');
submenu.hidden = !submenu.hidden;
this.setAttribute('aria-expanded', String(!submenu.hidden));
document.getElementById('chevronInventario').textContent = submenu.hidden ? '›' : '⌄';
});
document.getElementById('toggleMaquinas').addEventListener('click', function() {
const submenu = document.getElementById('submenuMaquinas');
submenu.hidden = !submenu.hidden;
this.setAttribute('aria-expanded', String(!submenu.hidden));
document.getElementById('chevronMaquinas').textContent = submenu.hidden ? '›' : '⌄';
if (!submenu.hidden && !document.querySelector('#view-maquinas:not([hidden])')) mostrarVista('maquinas');
});
document.getElementById('togglePersonal').addEventListener('click', function() {
const submenu = document.getElementById('submenuPersonal');
submenu.hidden = !submenu.hidden;
this.setAttribute('aria-expanded', String(!submenu.hidden));
document.getElementById('chevronPersonal').textContent = submenu.hidden ? '›' : '⌄';
if (!submenu.hidden && document.getElementById('view-personal').hidden) mostrarVista('personal');
});
document.querySelectorAll('[data-personal-panel]').forEach(function(boton) {
boton.addEventListener('click', function() {
if (document.getElementById('view-personal').hidden) mostrarVista('personal');
abrirPanelPersonal(boton.dataset.personalPanel);
});
});
function abrirPanelPersonal(nombre) {
const panel = ['registrar','consultar','documentos','estadoDocumentacion','semanal','incentivo','bonos','historial'].includes(nombre) ? nombre : 'registrar';
document.querySelectorAll('.personal-panel').forEach(function(elemento) {
elemento.hidden = elemento.id !== 'panel-personal-' + panel;
});
document.querySelectorAll('[data-personal-panel]').forEach(function(boton) {
boton.classList.toggle('active', boton.dataset.personalPanel === panel);
});
const titulos = {
registrar:['Registrar lavador','Registra los datos personales y la fecha real de ingreso laboral.'],
consultar:['Consultar lavadores','Consulta el personal registrado y su antigüedad.'],
documentos:['Documentación','Carga y consulta los documentos obligatorios de cada lavador.'],
estadoDocumentacion:['Estado de documentación','Consulta y filtra los documentos faltantes, vencidos y completos de cada lavador.'],
semanal:['Registro semanal','Registra el resultado Sí o No de cada semana completa.'],
incentivo:['Estado de incentivos','Consulta quién tiene o no tiene incentivo durante una semana.'],
bonos:['Bonos semestrales','Consulta la evaluación y registra los pagos individuales.'],
historial:['Historial individual','Consulta los incentivos semanales y movimientos de bonos por persona.']
};
document.getElementById('pageTitle').textContent = titulos[panel][0];
document.getElementById('pageSubtitle').textContent = titulos[panel][1];
if (panel === 'consultar') cargarTablaPersonal();
if (panel === 'estadoDocumentacion') cargarEstadoDocumentacion();
if (panel === 'semanal' && document.getElementById('fechaSemanaIncentivo').value) cargarSemanaIncentivos();
if (panel === 'incentivo') prepararConsultaEstadoIncentivo();
if (panel === 'bonos') cargarBonosSemestrales();
if (panel === 'historial') cargarSelectorHistorialPersonal();
}
document.querySelectorAll('[data-maquina-panel]').forEach(function(boton) {
boton.addEventListener('click', function() {
if (document.getElementById('view-maquinas').hidden) mostrarVista('maquinas');
abrirPanelMaquinas(boton.dataset.maquinaPanel);
});
});
function abrirPanelMaquinas(nombre) {
const panel = ['consulta','registrar','asignar','devolver','estado','historial'].includes(nombre) ? nombre : 'consulta';
document.querySelectorAll('.machine-panel').forEach(function(elemento) {
elemento.hidden = elemento.id !== 'panel-maquinas-' + panel;
});
document.querySelectorAll('[data-maquina-panel]').forEach(function(boton) {
boton.classList.toggle('active', boton.dataset.maquinaPanel === panel);
});
const titulos = {
consulta:['Consultar máquinas','Consulta el estado actual y quién tiene cada equipo asignado.'],
registrar:['Registrar máquina','Registra un equipo nuevo en el inventario de máquinas.'],
asignar:['Asignar máquina','Asigna una máquina disponible a un lavador activo.'],
devolver:['Registrar devolución','Registra la devolución de una máquina asignada.'],
estado:['Cambiar estado','Registra daños, reparaciones y cambios de disponibilidad.'],
historial:['Historial de movimientos','Consulta el historial por máquina o por lavador.']
};
document.getElementById('pageTitle').textContent = titulos[panel][0];
document.getElementById('pageSubtitle').textContent = titulos[panel][1];
}
function abrirMovimiento(tipo) {
prepararTipoMovimiento(tipo);
mostrarVista('movimientos');
}
function prepararTipoMovimiento(tipo) {
document.getElementById('tipoMovimiento').value = tipo;
actualizarTipoMovimiento();
}
function cargarInventario() {
const tabla = document.getElementById('tablaProductos');
const estado = document.getElementById('estadoInventario');
const boton = document.getElementById('btnActualizar');
if (tabla) {
tabla.replaceChildren();
const fila = document.createElement('tr');
const celda = document.createElement('td');
celda.colSpan = 7;
celda.textContent = 'Cargando inventario...';
fila.appendChild(celda);
tabla.appendChild(fila);
}
if (boton) boton.disabled = true;
if (estado) estado.textContent = '';
google.script.run.withSuccessHandler(function(productos) {
productosCache = (productos || []).filter(function(p) {
return String(p.activo).toLowerCase() !== 'false';
});
if (boton) boton.disabled = false;
const bajos = productosCache.filter(function(p) {
return (Number(p.stockActual) || 0) <= (Number(p.stockMinimo) || 0);
}).length;
document.getElementById('totalProductos').textContent = productosCache.length;
document.getElementById('totalBajos').textContent = bajos;
if (estado) estado.textContent = 'Productos activos: ' + productosCache.length;
renderizarInventario();
actualizarSelectorProductos();
}).withFailureHandler(function(error) {
if (boton) boton.disabled = false;
if (tabla) {
tabla.replaceChildren();
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 7;
td.textContent = 'Error al cargar: ' + mostrarError(error);
tr.appendChild(td);
tabla.appendChild(tr);
}
if (estado) estado.textContent = 'No se pudo consultar el inventario.';
}).obtenerProductos();
}
function renderizarInventario() {
const tabla = document.getElementById('tablaProductos');
if (!tabla) return;
tabla.replaceChildren();
if (productosCache.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 7;
td.textContent = 'No hay productos activos registrados.';
tr.appendChild(td);
tabla.appendChild(tr);
return;
}
const busqueda = normalizarBusquedaInventario(document.getElementById('buscarProductoConsulta').value);
const productosFiltrados = productosCache.filter(function(p) {
const texto = normalizarBusquedaInventario([p.codigo, p.producto, p.categoria, p.unidad].join(' '));
return texto.includes(busqueda);
});
if (!productosFiltrados.length) {
const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 7;
td.textContent = 'No se encontraron productos que coincidan con la búsqueda.'; tr.appendChild(td); tabla.appendChild(tr); return;
}
const campo = document.getElementById('ordenProductos').value;
const direccion = document.getElementById('direccionOrdenProductos').value;
const factor = direccion === 'desc' ? -1 : 1;
const productosOrdenados = productosFiltrados.slice().sort(function(a, b) {
let valorA;
let valorB;
if (campo === 'stockActual' || campo === 'stockMinimo') {
valorA = Number(a[campo]) || 0;
valorB = Number(b[campo]) || 0;
return (valorA - valorB) * factor;
}
if (campo === 'estado') {
valorA = (Number(a.stockActual) || 0) <= (Number(a.stockMinimo) || 0) ? 'Reponer' : 'Disponible';
valorB = (Number(b.stockActual) || 0) <= (Number(b.stockMinimo) || 0) ? 'Reponer' : 'Disponible';
} else {
valorA = String(a[campo] || '');
valorB = String(b[campo] || '');
}
return valorA.localeCompare(valorB, 'es', { numeric: true, sensitivity: 'base' }) * factor;
});
productosOrdenados.forEach(function(p) {
const stock = Number(p.stockActual) || 0;
const minimo = Number(p.stockMinimo) || 0;
const bajo = stock <= minimo;
const tr = document.createElement('tr');
[p.codigo, p.producto, p.categoria, p.unidad, p.stockActual, p.stockMinimo, bajo ? 'Reponer' : 'Disponible'].forEach(function(valor, indice) {
const td = document.createElement('td');
td.textContent = valor == null ? '' : String(valor);
if (indice === 6) td.className = bajo ? 'stock-low' : 'stock-ok';
tr.appendChild(td);
});
tabla.appendChild(tr);
});
}
function normalizarBusquedaInventario(valor) {
return String(valor || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
}
function actualizarSelectorProductos() {
const selector = document.getElementById('productoMovimiento');
const buscador = document.getElementById('buscarProductoMovimiento');
const anterior = selector.value;
const termino = normalizarBusquedaInventario(buscador ? buscador.value : '');
const coincidencias = productosCache.filter(function(p) {
const texto = normalizarBusquedaInventario([p.codigo, p.producto, p.categoria, p.unidad].join(' '));
return !termino || texto.includes(termino);
});
selector.replaceChildren();
const inicial = document.createElement('option');
inicial.value = '';
inicial.textContent = coincidencias.length
? 'Selecciona un producto (' + coincidencias.length + ' coincidencia(s))'
: 'No se encontraron productos';
selector.appendChild(inicial);
coincidencias.forEach(function(p) {
const opcion = document.createElement('option');
opcion.value = p.codigo;
opcion.textContent = p.codigo + ' — ' + p.producto + ' (' + p.stockActual + ' ' + p.unidad + ')';
selector.appendChild(opcion);
});
if (coincidencias.some(p => p.codigo === anterior)) selector.value = anterior;
else if (coincidencias.length === 1) selector.value = coincidencias[0].codigo;
actualizarAyudaProducto();
}
function actualizarAyudaProducto() {
const codigo = document.getElementById('productoMovimiento').value;
const p = productosCache.find(item => item.codigo === codigo);
const ayuda = document.getElementById('ayudaUnidad');
const cantidad = document.getElementById('cantidadMovimiento');
if (!p) { ayuda.textContent = 'Selecciona un producto para ver su unidad y stock actual.'; cantidad.removeAttribute('max'); return; }
ayuda.textContent = 'Unidad: ' + p.unidad + '. Stock actual: ' + p.stockActual + ' ' + p.unidad + '.';
actualizarTipoMovimiento();
}
function actualizarTipoMovimiento() {
const tipo = document.getElementById('tipoMovimiento').value;
document.getElementById('etiquetaCantidad').firstChild.textContent =
tipo === 'Entrada' ? 'Cantidad que entra *' : 'Cantidad que consumes *';
const boton = document.getElementById('btnMovimiento');
boton.textContent = tipo === 'Entrada' ? 'Registrar entrada' : 'Registrar consumo';
boton.className = tipo === 'Entrada' ? 'entry' : 'consume';
actualizarAyudaProductoSinRecursion();
}
function actualizarAyudaProductoSinRecursion() {
const codigo = document.getElementById('productoMovimiento').value;
const p = productosCache.find(item => item.codigo === codigo);
const cantidad = document.getElementById('cantidadMovimiento');
if (p && document.getElementById('tipoMovimiento').value === 'Consumo') cantidad.max = Math.max(0, Number(p.stockActual) || 0);
else cantidad.removeAttribute('max');
}
document.getElementById('buscarProductoConsulta').addEventListener('input', renderizarInventario);
document.getElementById('buscarProductoMovimiento').addEventListener('input', actualizarSelectorProductos);
document.getElementById('productoMovimiento').addEventListener('change', actualizarAyudaProducto);
document.getElementById('tipoMovimiento').addEventListener('change', actualizarTipoMovimiento);
document.getElementById('unidadProducto').addEventListener('change', function() {
const otra = this.value === 'Otro';
document.getElementById('otraUnidadContenedor').hidden = !otra;
document.getElementById('otraUnidad').required = otra;
});
document.getElementById('formProducto').addEventListener('submit', function(event) {
event.preventDefault();
const formulario = event.currentTarget;
const boton = document.getElementById('btnRegistrar');
const mensaje = document.getElementById('mensajeProducto');
const datos = Object.fromEntries(new FormData(formulario).entries());
if (datos.unidad === 'Otro') datos.unidad = document.getElementById('otraUnidad').value.trim();
datos.stockInicial = Number(datos.stockInicial); datos.stockMinimo = Number(datos.stockMinimo);
boton.disabled = true; mensaje.textContent = 'Registrando producto...';
google.script.run.withSuccessHandler(function(resultado) {
mensaje.textContent = resultado.mensaje || 'Producto registrado correctamente.';
formulario.reset(); document.getElementById('otraUnidadContenedor').hidden = true;
document.getElementById('otraUnidad').required = false; boton.disabled = false; cargarInventario();
}).withFailureHandler(function(error) {
mensaje.textContent = 'No se pudo registrar: ' + mostrarError(error); boton.disabled = false;
}).registrarProducto(datos);
});
document.getElementById('formMovimiento').addEventListener('submit', function(event) {
event.preventDefault();
const codigo = document.getElementById('productoMovimiento').value;
const cantidad = Number(document.getElementById('cantidadMovimiento').value);
const tipo = document.getElementById('tipoMovimiento').value;
const motivo = document.getElementById('motivoMovimiento').value.trim();
const boton = document.getElementById('btnMovimiento');
const mensaje = document.getElementById('mensajeMovimiento');
if (!codigo || !Number.isFinite(cantidad) || cantidad <= 0 || !motivo) {
mensaje.textContent = 'Selecciona un producto e indica una cantidad y un motivo válidos.'; return;
}
boton.disabled = true; mensaje.textContent = 'Registrando movimiento...';
const runner = google.script.run.withSuccessHandler(function(resultado) {
mensaje.textContent = (resultado.mensaje || 'Movimiento registrado.') + ' Stock resultante: ' + resultado.stockResultante + ' ' + resultado.unidad + '.';
document.getElementById('cantidadMovimiento').value = ''; document.getElementById('motivoMovimiento').value = '';
boton.disabled = false; cargarInventario(); cargarHistorialMovimientos();
}).withFailureHandler(function(error) {
mensaje.textContent = 'No se pudo registrar: ' + mostrarError(error); boton.disabled = false;
});
if (tipo === 'Entrada') runner.registrarEntrada(codigo,cantidad,motivo);
else runner.registrarConsumo(codigo,cantidad,motivo);
});
document.getElementById('formSolicitud').addEventListener('submit', function(event) {
event.preventDefault();
const boton = document.getElementById('btnSolicitud');
const mensaje = document.getElementById('mensajeSolicitud');
const nombre = document.getElementById('nombreSolicitud').value.trim();
const nota = document.getElementById('textoSolicitud').value.trim();
const categoria = document.getElementById('categoriaSolicitud').value;
if (!categoria) { mensaje.textContent = 'Selecciona una categoría.'; return; }
boton.disabled = true; mensaje.textContent = 'Enviando nota...';
google.script.run.withSuccessHandler(function(resultado) {
mensaje.textContent = resultado.mensaje || 'Nota enviada.';
document.getElementById('textoSolicitud').value = ''; document.getElementById('categoriaSolicitud').value = ''; boton.disabled = false; cargarSolicitudes();
}).withFailureHandler(function(error) {
mensaje.textContent = 'No se pudo enviar: ' + mostrarError(error); boton.disabled = false;
}).registrarSolicitud(nota,nombre,categoria);
});
function cargarSolicitudes() {
const tabla = document.getElementById('tablaSolicitudes');
const boton = document.getElementById('btnActualizarNotas');
const mensaje = document.getElementById('mensajeBandeja');
if (boton) boton.disabled = true;
if (mensaje) mensaje.textContent = 'Cargando notas...';
google.script.run.withSuccessHandler(function(notas) {
solicitudesCache = Array.isArray(notas) ? notas : [];
if (boton) boton.disabled = false;
if (mensaje) mensaje.textContent = '';
renderizarSolicitudes();
}).withFailureHandler(function(error) {
if (boton) boton.disabled = false;
if (mensaje) mensaje.textContent = 'No se pudieron cargar las notas: ' + mostrarError(error);
tabla.replaceChildren(); const tr = document.createElement('tr'); const td = document.createElement('td');
td.colSpan = 6; td.textContent = 'Error al consultar la bandeja.'; tr.appendChild(td); tabla.appendChild(tr);
}).obtenerSolicitudes();
}
function renderizarSolicitudes() {
const tabla = document.getElementById('tablaSolicitudes');
const filtro = document.getElementById('filtroNotas').value;
tabla.replaceChildren();
const pendientes = solicitudesCache.filter(nota => nota.estado !== 'Atendida').length;
const atendidas = solicitudesCache.filter(nota => nota.estado === 'Atendida').length;
document.getElementById('totalNotasPendientes').textContent = pendientes;
document.getElementById('totalNotasAtendidas').textContent = atendidas;
document.getElementById('totalNotasDashboard').textContent = pendientes;
const notas = solicitudesCache.filter(nota => filtro === 'Todas' || nota.estado === filtro);
if (notas.length === 0) {
const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 6;
td.textContent = solicitudesCache.length === 0 ? 'Todavía no hay notas registradas.' : 'No hay notas para el filtro seleccionado.';
tr.appendChild(td); tabla.appendChild(tr); return;
}
notas.forEach(function(nota) {
const tr = document.createElement('tr');
const tdFecha = document.createElement('td'); tdFecha.textContent = nota.fecha || ''; tr.appendChild(tdFecha);
const tdPersona = document.createElement('td'); tdPersona.textContent = nota.persona || 'Sin identificar'; tr.appendChild(tdPersona);
const tdCategoria = document.createElement('td'); tdCategoria.textContent = nota.categoria || '—'; tr.appendChild(tdCategoria);
const tdNota = document.createElement('td'); tdNota.className = 'note-content'; tdNota.textContent = nota.nota || ''; tr.appendChild(tdNota);
const tdEstado = document.createElement('td'); const etiqueta = document.createElement('span');
const estaAtendida = nota.estado === 'Atendida'; etiqueta.className = 'state-label ' + (estaAtendida ? 'done' : 'pending');
etiqueta.textContent = estaAtendida ? 'Atendida' : 'Pendiente'; tdEstado.appendChild(etiqueta); tr.appendChild(tdEstado);
const tdAcciones = document.createElement('td'); tdAcciones.className = 'note-actions';
const boton = document.createElement('button'); boton.type = 'button'; boton.className = estaAtendida ? 'pending-btn' : 'attended';
boton.textContent = estaAtendida ? 'Marcar pendiente' : 'Marcar atendida';
boton.addEventListener('click', function() { cambiarEstadoSolicitud(nota.fila,estaAtendida?'Pendiente':'Atendida',boton); });
tdAcciones.appendChild(boton); tr.appendChild(tdAcciones); tabla.appendChild(tr);
});
}
function cambiarEstadoSolicitud(fila,nuevoEstado,boton) {
const mensaje = document.getElementById('mensajeBandeja');
boton.disabled = true; mensaje.textContent = 'Actualizando estado de la nota...';
google.script.run.withSuccessHandler(function(resultado) {
mensaje.textContent = resultado.mensaje || 'Estado actualizado.'; cargarSolicitudes();
}).withFailureHandler(function(error) {
boton.disabled = false; mensaje.textContent = 'No se pudo actualizar el estado: ' + mostrarError(error);
}).actualizarEstadoSolicitud(fila,nuevoEstado);
}
document.getElementById('filtroNotas').addEventListener('change',renderizarSolicitudes);
document.getElementById('ordenProductos').addEventListener('change', renderizarInventario);
document.getElementById('direccionOrdenProductos').addEventListener('change', renderizarInventario);
let maquinasCache = [];
let lavadoresMaquinasCache = [];
function claseEstadoMaquina(estado) {
if (estado === 'Operativa') return 'operativa';
if (estado === 'De respaldo') return 'respaldo';
if (estado === 'Dañada') return 'danada';
if (estado === 'En reparación') return 'reparacion';
return '';
}
function actualizarIndicadoresMaquinas(lista, sufijo) {
const operativas = lista.filter(m => m.estado === 'Operativa').length;
const respaldo = lista.filter(m => m.estado === 'De respaldo').length;
const problemas = lista.filter(m => ['Dañada','En reparación'].includes(m.estado)).length;
const ids = {
total: ['totalMaquinasDashboard','totalMaquinasModulo'],
operativas: ['maquinasOperativasDashboard','maquinasOperativasModulo'],
respaldo: ['maquinasRespaldoDashboard','maquinasRespaldoModulo'],
problemas: ['maquinasProblemasDashboard','maquinasProblemasModulo']
};
const valores = {total:lista.length,operativas:operativas,respaldo:respaldo,problemas:problemas};
Object.keys(ids).forEach(function(tipo) {
ids[tipo].forEach(function(id) {
const elemento = document.getElementById(id);
if (elemento) elemento.textContent = valores[tipo];
});
});
}
function agregarFilaVacia(tabla, columnas, mensaje) {
tabla.replaceChildren();
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = columnas;
td.textContent = mensaje;
tr.appendChild(td);
tabla.appendChild(tr);
}
function renderizarResumenMaquinasDashboard(lista) {
const tabla = document.getElementById('tablaResumenMaquinasDashboard');
if (!tabla) return;
tabla.replaceChildren();
if (!lista.length) {
agregarFilaVacia(tabla,4,'Todavía no hay máquinas registradas.');
return;
}
lista.forEach(function(maquina) {
const tr = document.createElement('tr');
const valores = [maquina.codigo,maquina.tipo,maquina.estado,maquina.asignacionActiva ? maquina.asignacionActiva.nombreLavador : 'Sin asignar'];
valores.forEach(function(valor,index) {
const td = document.createElement('td');
if (index === 2) {
const etiqueta = document.createElement('span');
etiqueta.className = 'machine-status ' + claseEstadoMaquina(maquina.estado);
etiqueta.textContent = maquina.estado || 'Sin estado';
td.appendChild(etiqueta);
} else td.textContent = valor || '—';
tr.appendChild(td);
});
tabla.appendChild(tr);
});
}
function cargarResumenMaquinas() {
const mensaje = document.getElementById('mensajeResumenMaquinasDashboard');
google.script.run.withSuccessHandler(function(lista) {
maquinasCache = Array.isArray(lista) ? lista : [];
actualizarIndicadoresMaquinas(maquinasCache);
renderizarResumenMaquinasDashboard(maquinasCache);
if (mensaje) mensaje.textContent = '';
}).withFailureHandler(function(error) {
if (mensaje) mensaje.textContent = 'No se pudo cargar el resumen de máquinas: ' + mostrarError(error);
const tabla = document.getElementById('tablaResumenMaquinasDashboard');
if (tabla) agregarFilaVacia(tabla,4,'No se pudo consultar el equipamiento.');
}).obtenerResumenMaquinas();
}
function renderizarTablaMaquinas(lista) {
const tabla = document.getElementById('tablaMaquinas');
if (!tabla) return;
tabla.replaceChildren();
if (!lista.length) {
agregarFilaVacia(tabla,5,'Todavía no hay máquinas registradas.');
return;
}
lista.forEach(function(maquina) {
const tr = document.createElement('tr');
const valores = [maquina.codigo,maquina.tipo,maquina.estado,maquina.asignacionActiva ? maquina.asignacionActiva.nombreLavador : 'Sin asignar',maquina.fechaRegistro];
valores.forEach(function(valor,index) {
const td = document.createElement('td');
if (index === 2) {
const etiqueta = document.createElement('span');
etiqueta.className = 'machine-status ' + claseEstadoMaquina(maquina.estado);
etiqueta.textContent = maquina.estado || 'Sin estado';
td.appendChild(etiqueta);
} else td.textContent = valor || '—';
tr.appendChild(td);
});
tabla.appendChild(tr);
});
}
function llenarSelectorMaquinas(id, lista, filtro) {
const selector = document.getElementById(id);
if (!selector) return;
const anterior = selector.value;
selector.replaceChildren();
const inicial = document.createElement('option');
inicial.value = '';
inicial.textContent = 'Seleccionar máquina';
selector.appendChild(inicial);
lista.filter(filtro).forEach(function(maquina) {
const opcion = document.createElement('option');
opcion.value = maquina.codigo;
opcion.textContent = maquina.codigo + ' — ' + maquina.tipo + ' (' + maquina.estado + ')';
selector.appendChild(opcion);
});
if (lista.some(m => m.codigo === anterior && filtro(m))) selector.value = anterior;
}
function actualizarSelectoresModuloMaquinas() {
llenarSelectorMaquinas('maquinaParaAsignar',maquinasCache,m => !m.asignacionActiva && !m.lavadorAsignado && !['Dañada','En reparación'].includes(m.estado));
llenarSelectorMaquinas('maquinaParaDevolver',maquinasCache,m => Boolean(m.asignacionActiva || m.lavadorAsignado));
llenarSelectorMaquinas('maquinaParaEstado',maquinasCache,() => true);
const selectorLavador = document.getElementById('lavadorParaAsignar');
if (selectorLavador) {
const anterior = selectorLavador.value;
selectorLavador.replaceChildren();
const inicial = document.createElement('option'); inicial.value = ''; inicial.textContent = 'Seleccionar lavador activo'; selectorLavador.appendChild(inicial);
lavadoresMaquinasCache.filter(l => l.activo).forEach(function(lavador) {
const opcion = document.createElement('option'); opcion.value = lavador.id; opcion.textContent = lavador.nombre; selectorLavador.appendChild(opcion);
});
if (lavadoresMaquinasCache.some(l => l.id === anterior && l.activo)) selectorLavador.value = anterior;
}
// Filtro de historial: códigos de todas las máquinas registradas.
const filtroCodigo = document.getElementById('filtroCodigoHistorialMaquinas');
if (filtroCodigo) {
const anterior = filtroCodigo.value;
filtroCodigo.replaceChildren();
const todas = document.createElement('option');
todas.value = '';
todas.textContent = 'Todas las máquinas';
filtroCodigo.appendChild(todas);
maquinasCache.forEach(function(maquina) {
const opcion = document.createElement('option');
opcion.value = maquina.codigo;
opcion.textContent = maquina.codigo + ' — ' + maquina.tipo;
filtroCodigo.appendChild(opcion);
});
if (maquinasCache.some(m => m.codigo === anterior)) filtroCodigo.value = anterior;
}
// Filtro de historial: lavadores activos e inactivos para incluir movimientos antiguos.
const filtroLavador = document.getElementById('filtroLavadorHistorialMaquinas');
if (filtroLavador) {
const anterior = filtroLavador.value;
filtroLavador.replaceChildren();
const todos = document.createElement('option');
todos.value = '';
todos.textContent = 'Todos los lavadores';
filtroLavador.appendChild(todos);
const nombres = [...new Set(lavadoresMaquinasCache.map(l => l.nombre).filter(Boolean))]
.sort((a, b) => a.localeCompare(b));
nombres.forEach(function(nombre) {
const opcion = document.createElement('option');
opcion.value = nombre;
opcion.textContent = nombre;
filtroLavador.appendChild(opcion);
});
if (nombres.includes(anterior)) filtroLavador.value = anterior;
}
}
function cargarModuloMaquinas() {
const mensaje = document.getElementById('mensajeListaMaquinas');
if (mensaje) mensaje.textContent = 'Cargando máquinas y lavadores...';
google.script.run.withSuccessHandler(function(lista) {
maquinasCache = Array.isArray(lista) ? lista : [];
actualizarIndicadoresMaquinas(maquinasCache);
renderizarTablaMaquinas(maquinasCache);
actualizarSelectoresModuloMaquinas();
if (mensaje) mensaje.textContent = maquinasCache.length + ' máquina(s) registradas.';
}).withFailureHandler(function(error) {
if (mensaje) mensaje.textContent = 'No se pudo cargar la lista: ' + mostrarError(error);
agregarFilaVacia(document.getElementById('tablaMaquinas'),5,'Error al consultar las máquinas.');
}).obtenerResumenMaquinas();
google.script.run.withSuccessHandler(function(lavadores) {
lavadoresMaquinasCache = Array.isArray(lavadores) ? lavadores : [];
actualizarSelectoresModuloMaquinas();
}).withFailureHandler(function(error) {
const aviso = document.getElementById('mensajeAsignarMaquina');
if (aviso) aviso.textContent = 'No se pudo cargar la lista de lavadores: ' + mostrarError(error);
}).obtenerLavadores();
}
function ejecutarAccionMaquina(config) {
const boton = document.getElementById(config.boton);
const mensaje = document.getElementById(config.mensaje);
boton.disabled = true;
mensaje.textContent = config.cargando;
config.ejecutar(
function(resultado) {
mensaje.textContent = resultado.mensaje || config.exito;
boton.disabled = false;
if (config.formulario) document.getElementById(config.formulario).reset();
cargarModuloMaquinas();
cargarResumenMaquinas();
},
function(error) {
mensaje.textContent = 'No se pudo completar la operación: ' + mostrarError(error);
boton.disabled = false;
}
);
}
document.getElementById('btnActualizarMaquinas').addEventListener('click',function() {
cargarModuloMaquinas();
cargarResumenMaquinas();
});
document.getElementById('formRegistrarMaquina').addEventListener('submit',function(event) {
event.preventDefault();
const datos = Object.fromEntries(new FormData(event.currentTarget).entries());
ejecutarAccionMaquina({boton:'btnRegistrarMaquina',mensaje:'mensajeRegistrarMaquina',cargando:'Registrando máquina...',exito:'Máquina registrada.',formulario:'formRegistrarMaquina',ejecutar:function(ok,fail) {
google.script.run.withSuccessHandler(ok).withFailureHandler(fail).registrarMaquina(datos);
}});
});
document.getElementById('formAsignarMaquina').addEventListener('submit',function(event) {
event.preventDefault();
const codigo = document.getElementById('maquinaParaAsignar').value;
const idLavador = document.getElementById('lavadorParaAsignar').value;
const observaciones = document.getElementById('observacionesAsignacion').value.trim();
ejecutarAccionMaquina({boton:'btnAsignarMaquina',mensaje:'mensajeAsignarMaquina',cargando:'Registrando asignación...',exito:'Máquina asignada.',formulario:'formAsignarMaquina',ejecutar:function(ok,fail) {
google.script.run.withSuccessHandler(ok).withFailureHandler(fail).asignarMaquina(codigo,idLavador,observaciones);
}});
});
document.getElementById('formDevolverMaquina').addEventListener('submit',function(event) {
event.preventDefault();
const codigo = document.getElementById('maquinaParaDevolver').value;
const motivo = document.getElementById('motivoDevolucionMaquina').value.trim();
const observaciones = document.getElementById('observacionesDevolucionMaquina').value.trim();
ejecutarAccionMaquina({boton:'btnDevolverMaquina',mensaje:'mensajeDevolverMaquina',cargando:'Registrando devolución...',exito:'Devolución registrada.',formulario:'formDevolverMaquina',ejecutar:function(ok,fail) {
google.script.run.withSuccessHandler(ok).withFailureHandler(fail).devolverMaquina(codigo,motivo,observaciones);
}});
});
document.getElementById('formEstadoMaquina').addEventListener('submit',function(event) {
event.preventDefault();
const codigo = document.getElementById('maquinaParaEstado').value;
const estado = document.getElementById('nuevoEstadoMaquina').value;
const motivo = document.getElementById('motivoEstadoMaquina').value.trim();
const observaciones = document.getElementById('observacionesEstadoMaquina').value.trim();
ejecutarAccionMaquina({boton:'btnCambiarEstadoMaquina',mensaje:'mensajeEstadoMaquina',cargando:'Actualizando estado...',exito:'Estado actualizado.',formulario:'formEstadoMaquina',ejecutar:function(ok,fail) {
google.script.run.withSuccessHandler(ok).withFailureHandler(fail).cambiarEstadoMaquina(codigo,estado,motivo,observaciones);
}});
});
document.getElementById('formHistorialMaquinas').addEventListener('submit',function(event) {
event.preventDefault();
const codigo = document.getElementById('filtroCodigoHistorialMaquinas').value;
const lavador = document.getElementById('filtroLavadorHistorialMaquinas').value;
const boton = document.getElementById('btnBuscarHistorialMaquinas');
const mensaje = document.getElementById('mensajeHistorialMaquinas');
const tabla = document.getElementById('tablaHistorialMaquinas');
boton.disabled = true; mensaje.textContent = 'Consultando historial...';
google.script.run.withSuccessHandler(function(lista) {
boton.disabled = false; tabla.replaceChildren();
const movimientos = Array.isArray(lista) ? lista : [];
if (!movimientos.length) {
agregarFilaVacia(tabla,8,'No se encontraron movimientos con esos filtros.');
mensaje.textContent = '0 movimientos encontrados.'; return;
}
movimientos.forEach(function(item) {
const tr = document.createElement('tr');
[item.fecha,item.codigo,item.tipoMovimiento,item.lavador,item.estadoAnterior,item.estadoNuevo,item.motivo,item.observaciones].forEach(function(valor) {
const td = document.createElement('td'); td.textContent = valor || '—'; tr.appendChild(td);
});
tabla.appendChild(tr);
});
mensaje.textContent = movimientos.length + ' movimiento(s) encontrados.';
}).withFailureHandler(function(error) {
boton.disabled = false; mensaje.textContent = 'No se pudo consultar el historial: ' + mostrarError(error);
agregarFilaVacia(tabla,8,'Error al consultar el historial.');
}).obtenerHistorialMaquinas(codigo,lavador);
});
let lavadoresPersonalCache = [];
function cargarTablaPersonal() {
const tabla = document.getElementById('tablaPersonal');
tabla.innerHTML = '<tr><td colspan="8">Cargando personal...</td></tr>';
google.script.run.withSuccessHandler(function(lista) {
lavadoresPersonalCache = Array.isArray(lista) ? lista : [];
tabla.replaceChildren();
if (!lavadoresPersonalCache.length) { tabla.innerHTML = '<tr><td colspan="8">No hay lavadores registrados.</td></tr>'; return; }
lavadoresPersonalCache.forEach(function(l) {
const tr = document.createElement('tr');
[l.nombre, l.telefono || '—', l.fechaIngreso || 'Sin fecha', l.antiguedad || '—', l.activo ? 'Activo' : 'Inactivo'].forEach(function(v) {
const td = document.createElement('td'); td.textContent = v; tr.appendChild(td);
});
const celdaAplica = document.createElement('td'), selectorAplica = document.createElement('select');
[['true','Sí aplica'],['false','No aplica']].forEach(function(par){const op=document.createElement('option');op.value=par[0];op.textContent=par[1];selectorAplica.appendChild(op);});
selectorAplica.value = l.aplicaIncentivo === false ? 'false' : 'true'; celdaAplica.appendChild(selectorAplica); tr.appendChild(celdaAplica);
const celdaMotivo = document.createElement('td'), selectorMotivo = document.createElement('select');
[['','Seleccionar'],['Jefe','Jefe'],['Encargado','Encargado'],['Otro','Otro']].forEach(function(par){const op=document.createElement('option');op.value=par[0];op.textContent=par[1];selectorMotivo.appendChild(op);});
selectorMotivo.value = l.motivoNoAplica || ''; selectorMotivo.disabled = selectorAplica.value === 'true';
selectorAplica.addEventListener('change',function(){selectorMotivo.disabled=selectorAplica.value==='true';if(selectorAplica.value==='true')selectorMotivo.value='';});
celdaMotivo.appendChild(selectorMotivo); tr.appendChild(celdaMotivo);
const accion = document.createElement('td');
const guardar = document.createElement('button'); guardar.className='primary'; guardar.textContent='Guardar evaluación';
guardar.addEventListener('click',function(){
guardar.disabled=true;
google.script.run.withSuccessHandler(function(r){document.getElementById('mensajeListaPersonal').textContent=r.mensaje || 'Configuración actualizada.';guardar.disabled=false;cargarTablaPersonal();})
.withFailureHandler(function(e){document.getElementById('mensajeListaPersonal').textContent='No se pudo actualizar: '+mostrarError(e);guardar.disabled=false;})
.actualizarAplicabilidadIncentivoLavador(l.id,selectorAplica.value,selectorMotivo.value);
});
const editar = document.createElement('button'); editar.className='secondary'; editar.textContent='Editar ingreso'; editar.style.marginTop='6px'; editar.addEventListener('click',function(){
const fecha = prompt('Fecha real de ingreso laboral de ' + l.nombre + ' (AAAA-MM-DD):', l.fechaIngreso || '');
if (!fecha) return;
google.script.run.withSuccessHandler(function(r){document.getElementById('mensajeListaPersonal').textContent=r.mensaje || 'Fecha actualizada.';cargarTablaPersonal();})
.withFailureHandler(function(e){document.getElementById('mensajeListaPersonal').textContent='No se pudo actualizar: '+mostrarError(e);})
.actualizarFechaIngresoLavador(l.id,fecha);
});
accion.append(guardar,editar); tr.appendChild(accion); tabla.appendChild(tr);
});
document.getElementById('mensajeListaPersonal').textContent = lavadoresPersonalCache.length + ' lavador(es) encontrado(s).';
}).withFailureHandler(function(error) {
tabla.innerHTML = '<tr><td colspan="8">No se pudo cargar el personal.</td></tr>';
document.getElementById('mensajeListaPersonal').textContent = mostrarError(error);
}).obtenerLavadores();
}
document.getElementById('btnActualizarListaPersonal').addEventListener('click', cargarTablaPersonal);
function fechaLocalISO(fecha) {
const y = fecha.getFullYear(), m = String(fecha.getMonth()+1).padStart(2,'0'), d = String(fecha.getDate()).padStart(2,'0');
return y + '-' + m + '-' + d;
}
function ajustarFechaLunes(valor) {
if (!valor) return '';
const f = new Date(valor + 'T12:00:00');
f.setDate(f.getDate() - ((f.getDay()+6)%7));
return fechaLocalISO(f);
}
function seleccionarResultadoIncentivo(fila, resultado) {
const botones = fila.querySelectorAll('button[data-resultado-incentivo]');
botones.forEach(function(boton) {
const activo = boton.dataset.resultadoIncentivo === resultado;
boton.classList.toggle('is-si', activo && resultado === 'Sí');
boton.classList.toggle('is-no', activo && resultado === 'No');
boton.setAttribute('aria-pressed', activo ? 'true' : 'false');
});
fila.dataset.incentivo = resultado;
}
function cargarSemanaIncentivos() {
const input = document.getElementById('fechaSemanaIncentivo');
if (!input.value) return;
const lunes = ajustarFechaLunes(input.value);
input.value = lunes;
const domingo = new Date(lunes + 'T12:00:00'); domingo.setDate(domingo.getDate()+6);
document.getElementById('rangoSemanaIncentivo').textContent = 'Semana seleccionada: ' + lunes + ' al ' + fechaLocalISO(domingo) + ' (lunes a domingo).';
const tabla = document.getElementById('tablaIncentivosSemana');
tabla.innerHTML = '<tr><td colspan="2">Cargando semana...</td></tr>';
google.script.run.withSuccessHandler(function(r) {
tabla.replaceChildren();
(r.registros || []).forEach(function(item) {
const tr = document.createElement('tr'), nombre = document.createElement('td'), celda = document.createElement('td');
nombre.textContent = item.nombre;
tr.dataset.idLavador = item.idLavador;
if (item.incentivo === 'No aplica') {
const estado = document.createElement('span');
estado.className = 'incentive-na';
estado.textContent = 'No aplica' + (item.motivoNoAplica ? ' — ' + item.motivoNoAplica : '');
celda.appendChild(estado);
tr.dataset.incentivo = 'No aplica';
} else {
const grupo = document.createElement('div');
grupo.className = 'incentive-actions';
[['Sí','Sí'],['No','No']].forEach(function(par) {
const boton = document.createElement('button');
boton.type = 'button';
boton.className = 'incentive-choice';
boton.textContent = par[1];
boton.dataset.resultadoIncentivo = par[0];
boton.setAttribute('aria-pressed', 'false');
boton.addEventListener('click', function() {
seleccionarResultadoIncentivo(tr, par[0]);
});
grupo.appendChild(boton);
});
celda.appendChild(grupo);
if (item.incentivo === 'Sí' || item.incentivo === 'No') seleccionarResultadoIncentivo(tr, item.incentivo);
else tr.dataset.incentivo = '';
}
tr.append(nombre,celda); tabla.appendChild(tr);
});
if (!r.registros || !r.registros.length) tabla.innerHTML = '<tr><td colspan="2">No hay lavadores activos.</td></tr>';
}).withFailureHandler(function(error) {
tabla.innerHTML = '<tr><td colspan="2">No se pudo cargar la semana: ' + mostrarError(error) + '</td></tr>';
}).obtenerIncentivosSemana(lunes);
}
function marcarTodosIncentivos(resultado) {
document.querySelectorAll('#tablaIncentivosSemana tr[data-id-lavador]').forEach(function(fila) {
if (fila.dataset.incentivo !== 'No aplica' && fila.querySelector('button[data-resultado-incentivo]')) {
seleccionarResultadoIncentivo(fila, resultado);
}
});
document.getElementById('mensajeIncentivos').textContent = 'Se marcaron los lavadores correspondientes como "' + resultado + '". Puedes modificar cualquier resultado antes de guardar.';
}
document.getElementById('btnTodosIncentivoSi').addEventListener('click', function() { marcarTodosIncentivos('Sí'); });
document.getElementById('btnTodosIncentivoNo').addEventListener('click', function() { marcarTodosIncentivos('No'); });
document.getElementById('fechaSemanaIncentivo').addEventListener('change', cargarSemanaIncentivos);
document.getElementById('btnGuardarIncentivos').addEventListener('click', function() {
const boton = this, mensaje = document.getElementById('mensajeIncentivos'), fecha = document.getElementById('fechaSemanaIncentivo').value;
const registros = Array.from(document.querySelectorAll('#tablaIncentivosSemana tr[data-id-lavador]')).map(fila => ({idLavador:fila.dataset.idLavador,incentivo:fila.dataset.incentivo || ''}));
if (!fecha) { mensaje.textContent = 'Selecciona una semana.'; return; }
if (registros.some(r => !r.incentivo)) { mensaje.textContent = 'Selecciona Sí o No para todos los lavadores que corresponda evaluar.'; return; }
boton.disabled = true; mensaje.textContent = 'Guardando resultados...';
google.script.run.withSuccessHandler(function(r) { mensaje.textContent = r.mensaje || 'Semana guardada.'; boton.disabled = false; })
.withFailureHandler(function(error) { mensaje.textContent = 'No se pudo guardar: ' + mostrarError(error); boton.disabled = false; })
.guardarIncentivosSemana(fecha, registros);
});
let estadoIncentivosCache = [];
let semanaEstadoIncentivoCargada = '';
function prepararConsultaEstadoIncentivo() {
const input = document.getElementById('fechaConsultaIncentivo');
if (!input.value) input.value = ajustarFechaLunes(fechaLocalISO(new Date()));
if (input.value !== semanaEstadoIncentivoCargada) cargarEstadoIncentivo();
}
let historialMovimientosCache = [];
function renderizarHistorialMovimientos() {
const tabla = document.getElementById('tablaHistorialMovimientos');
if (!tabla) return;
const tipo = document.getElementById('filtroTipoMovimiento').value;
const busqueda = normalizarTextoBusqueda(document.getElementById('buscarHistorialMovimiento').value);
const filtrados = historialMovimientosCache.filter(function(movimiento) {
const coincideTipo = tipo === 'Todos' || movimiento.tipo === tipo;
const texto = normalizarTextoBusqueda([movimiento.codigo, movimiento.producto, movimiento.motivo].join(' '));
return coincideTipo && (!busqueda || texto.includes(busqueda));
});
tabla.replaceChildren();
if (!filtrados.length) {
const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 8;
td.textContent = historialMovimientosCache.length ? 'No hay movimientos que coincidan con los filtros.' : 'Todavía no hay movimientos registrados.';
tr.appendChild(td); tabla.appendChild(tr); return;
}
filtrados.forEach(function(movimiento) {
const tr = document.createElement('tr');
[movimiento.fechaHora,movimiento.tipo,movimiento.codigo,movimiento.producto,movimiento.cantidad,movimiento.stockAnterior,movimiento.stockResultante,movimiento.motivo].forEach(function(valor,indice) {
const td=document.createElement('td'); td.textContent=valor == null ? '' : String(valor);
if (indice===1) td.className = movimiento.tipo==='Entrada' || movimiento.tipo==='Stock inicial' ? 'stock-ok' : 'stock-low';
if (indice===7) td.className='note-content';
tr.appendChild(td);
});
tabla.appendChild(tr);
});
}
function cargarHistorialMovimientos() {
const tabla=document.getElementById('tablaHistorialMovimientos');
const mensaje=document.getElementById('mensajeHistorialMovimientos');
const boton=document.getElementById('btnActualizarHistorialMovimientos');
if (!tabla) return;
if (boton) boton.disabled=true;
if (mensaje) mensaje.textContent='Cargando historial...';
google.script.run.withSuccessHandler(function(movimientos) {
historialMovimientosCache=Array.isArray(movimientos)?movimientos:[];
if (boton) boton.disabled=false;
if (mensaje) mensaje.textContent=historialMovimientosCache.length+' movimiento(s) registrado(s).';
renderizarHistorialMovimientos();
}).withFailureHandler(function(error) {
if (boton) boton.disabled=false;
if (mensaje) mensaje.textContent='No se pudo cargar el historial: '+mostrarError(error);
tabla.replaceChildren(); const tr=document.createElement('tr'); const td=document.createElement('td'); td.colSpan=8; td.textContent='Error al consultar el historial.'; tr.appendChild(td); tabla.appendChild(tr);
}).obtenerHistorialMovimientos();
}
document.getElementById('filtroTipoMovimiento').addEventListener('change',renderizarHistorialMovimientos);
document.getElementById('buscarHistorialMovimiento').addEventListener('input',renderizarHistorialMovimientos);
document.getElementById('btnActualizarHistorialMovimientos').addEventListener('click',cargarHistorialMovimientos);
function normalizarTextoBusqueda(valor) {
return String(valor || '').toLocaleLowerCase('es').normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function cargarEstadoIncentivo() {
const input = document.getElementById('fechaConsultaIncentivo');
if (!input.value) return;
const lunesObjetivo = ajustarFechaLunes(input.value);
input.value = lunesObjetivo;
const lunesResultado = new Date(lunesObjetivo + 'T12:00:00');
lunesResultado.setDate(lunesResultado.getDate() - 7);
const fechaResultado = fechaLocalISO(lunesResultado);
const domingoResultado = new Date(lunesResultado); domingoResultado.setDate(domingoResultado.getDate() + 6);
const domingoObjetivo = new Date(lunesObjetivo + 'T12:00:00'); domingoObjetivo.setDate(domingoObjetivo.getDate() + 6);
document.getElementById('rangoConsultaIncentivo').textContent = 'Incentivo aplicable del ' + lunesObjetivo + ' al ' + fechaLocalISO(domingoObjetivo) + '. Se toma el resultado de la semana anterior: ' + fechaResultado + ' al ' + fechaLocalISO(domingoResultado) + '.';
const tabla = document.getElementById('tablaEstadoIncentivo');
tabla.innerHTML = '<tr><td colspan="3">Consultando incentivos...</td></tr>';
document.getElementById('mensajeConsultaIncentivo').textContent = '';
semanaEstadoIncentivoCargada = lunesObjetivo;
google.script.run.withSuccessHandler(function(r) {
estadoIncentivosCache = (r && r.registros ? r.registros : []).map(function(item) {
return { idLavador:item.idLavador, nombre:item.nombre, resultado:item.incentivo || '', motivoNoAplica:item.motivoNoAplica || '' };
});
renderizarEstadoIncentivo();
}).withFailureHandler(function(error) {
estadoIncentivosCache = [];
tabla.innerHTML = '<tr><td colspan="3">No se pudo consultar: ' + mostrarError(error) + '</td></tr>';
document.getElementById('resumenConsultaIncentivo').textContent = '';
}).obtenerIncentivosSemana(fechaResultado);
}
function renderizarEstadoIncentivo() {
const tabla = document.getElementById('tablaEstadoIncentivo');
const busqueda = normalizarTextoBusqueda(document.getElementById('buscarLavadorIncentivo').value.trim());
const filtro = document.getElementById('filtroEstadoIncentivo').value;
const orden = { 'Sí':0, 'No':1, 'No aplica':2, '':3 };
const filtrados = estadoIncentivosCache.filter(function(item) {
const coincideNombre = normalizarTextoBusqueda(item.nombre).includes(busqueda);
const estado = item.resultado || 'Sin registrar';
const coincideEstado = filtro === 'Todos' || estado === filtro;
return coincideNombre && coincideEstado;
}).sort(function(a,b) {
return (orden[a.resultado] - orden[b.resultado]) || a.nombre.localeCompare(b.nombre, 'es');
});
tabla.replaceChildren();
filtrados.forEach(function(item) {
const tr = document.createElement('tr');
const nombre = document.createElement('td'); nombre.textContent = item.nombre;
const estado = document.createElement('td');
const resultado = document.createElement('td'); resultado.textContent = item.resultado || 'Sin registrar';
if (item.resultado === 'Sí') estado.textContent = 'Tiene incentivo — 60 %';
else if (item.resultado === 'No') estado.textContent = 'No tiene incentivo — 50 %';
else if (item.resultado === 'No aplica') estado.textContent = 'No aplica' + (item.motivoNoAplica ? ' — ' + item.motivoNoAplica : '');
else estado.textContent = 'Pendiente de registrar';
tr.append(nombre, estado, resultado); tabla.appendChild(tr);
});
if (!filtrados.length) tabla.innerHTML = '<tr><td colspan="3">No se encontraron lavadores con esos criterios.</td></tr>';
const si = estadoIncentivosCache.filter(x => x.resultado === 'Sí').length;
const no = estadoIncentivosCache.filter(x => x.resultado === 'No').length;
const pendientes = estadoIncentivosCache.filter(x => !x.resultado).length;
const noAplica = estadoIncentivosCache.filter(x => x.resultado === 'No aplica').length;
document.getElementById('resumenConsultaIncentivo').textContent = 'Total: ' + estadoIncentivosCache.length + ' | Con incentivo: ' + si + ' | Sin incentivo: ' + no + ' | No aplica: ' + noAplica + ' | Sin resultado: ' + pendientes + '. Mostrando ' + filtrados.length + '.';
}
document.getElementById('fechaConsultaIncentivo').addEventListener('change', cargarEstadoIncentivo);
document.getElementById('buscarLavadorIncentivo').addEventListener('input', renderizarEstadoIncentivo);
document.getElementById('filtroEstadoIncentivo').addEventListener('change', renderizarEstadoIncentivo);
function cargarBonosSemestrales() {
const tabla = document.getElementById('tablaBonos'); tabla.innerHTML = '<tr><td colspan="8">Cargando evaluación...</td></tr>';
google.script.run.withSuccessHandler(function(lista) {
tabla.replaceChildren();
(lista || []).forEach(function(b) {
const tr = document.createElement('tr');
[b.nombre,b.antiguedad || '—',b.fechaInicio || '—',b.fechaEvaluacion || '—',b.ultimoPago || 'Nunca',String(b.faltas)].forEach(v => { const td=document.createElement('td'); td.textContent=v; tr.appendChild(td); });
const estado=document.createElement('td'); estado.textContent=b.estado; tr.appendChild(estado);
const accion=document.createElement('td');
if (b.estado === 'Bono pendiente') { const btn=document.createElement('button'); btn.className='entry'; btn.textContent='Registrar pago'; btn.addEventListener('click',()=>registrarPagoBonoUI(b)); accion.appendChild(btn); }
else if (b.estado === 'No corresponde') { const btn=document.createElement('button'); btn.className='secondary'; btn.textContent='Modificar con autorización'; btn.addEventListener('click',()=>desbloquearBonoUI(b)); accion.appendChild(btn); }
else accion.textContent = b.estado === 'Falta fecha de ingreso' ? 'Completar fecha laboral' : '—';
tr.appendChild(accion); tabla.appendChild(tr);
});
if (!lista || !lista.length) tabla.innerHTML='<tr><td colspan="8">No hay lavadores registrados.</td></tr>';
}).withFailureHandler(function(error) { tabla.innerHTML='<tr><td colspan="8">Error: '+mostrarError(error)+'</td></tr>'; }).obtenerResumenBonosSemestrales();
}
document.getElementById('btnActualizarBonos').addEventListener('click', cargarBonosSemestrales);
function registrarPagoBonoUI(b) {
const fecha = prompt('Fecha real de pago del bono (AAAA-MM-DD):', fechaLocalISO(new Date()));
if (!fecha) return;
const obs = prompt('Observaciones (opcional):','') || '';
if (!confirm('¿Confirmas el pago del bono a ' + b.nombre + ' con fecha ' + fecha + '?')) return;
google.script.run.withSuccessHandler(function(r) { document.getElementById('mensajeBonos').textContent=r.mensaje; cargarBonosSemestrales(); })
.withFailureHandler(function(e) { document.getElementById('mensajeBonos').textContent='No se pudo registrar el pago: '+mostrarError(e); })
.registrarPagoBonoSemestral(b.idLavador,fecha,obs);
}
function desbloquearBonoUI(b) {
const codigo=prompt('Código de autorización para modificar el estado de ' + b.nombre + ':'); if (!codigo) return;
const nuevo=prompt('Escribe el nuevo estado exactamente:\nBono pendiente\nEn evaluación'); if (!nuevo) return;
const obs=prompt('Motivo de la modificación (obligatorio):',''); if (!obs) return;
google.script.run.withSuccessHandler(function(r) { document.getElementById('mensajeBonos').textContent=r.mensaje; cargarBonosSemestrales(); })
.withFailureHandler(function(e) { document.getElementById('mensajeBonos').textContent='No se pudo modificar: '+mostrarError(e); })
.desbloquearEstadoBono(b.idLavador,nuevo,codigo,obs);
}
function cargarSelectorHistorialPersonal() {
const selector=document.getElementById('selectorHistorialPersonal');
google.script.run.withSuccessHandler(function(lista) {
lavadoresPersonalCache=lista || []; selector.replaceChildren();
const op=document.createElement('option'); op.value=''; op.textContent='Seleccionar lavador'; selector.appendChild(op);
lavadoresPersonalCache.forEach(l=>{const o=document.createElement('option');o.value=l.id;o.textContent=l.nombre;selector.appendChild(o);});
}).withFailureHandler(function(e){document.getElementById('mensajeHistorialPersonal').textContent=mostrarError(e);}).obtenerLavadores();
}
document.getElementById('selectorHistorialPersonal').addEventListener('change',function(){
const id=this.value, ti=document.getElementById('tablaHistorialIncentivos'), tb=document.getElementById('tablaHistorialBonos');
if(!id){ti.innerHTML='<tr><td colspan="4">Selecciona un lavador.</td></tr>';tb.innerHTML='<tr><td colspan="6">Selecciona un lavador.</td></tr>';return;}
ti.innerHTML='<tr><td colspan="4">Cargando incentivos...</td></tr>'; tb.innerHTML='<tr><td colspan="6">Cargando movimientos...</td></tr>';
google.script.run.withSuccessHandler(function(r){
ti.replaceChildren(); (r.incentivos || []).forEach(function(x){const tr=document.createElement('tr');[x.semanaDesde,x.semanaHasta,x.incentivo,x.fechaRegistro].forEach(v=>{const td=document.createElement('td');td.textContent=v||'—';tr.appendChild(td);});ti.appendChild(tr);});
if(!r.incentivos || !r.incentivos.length) ti.innerHTML='<tr><td colspan="4">Sin registros semanales.</td></tr>';
tb.replaceChildren(); (r.bonos || []).forEach(function(x){const tr=document.createElement('tr');[x.movimiento,x.fecha,x.periodo,x.faltas,x.estado,x.observaciones].forEach(v=>{const td=document.createElement('td');td.textContent=v||'—';tr.appendChild(td);});tb.appendChild(tr);});
if(!r.bonos || !r.bonos.length) tb.innerHTML='<tr><td colspan="6">Sin movimientos de bonos.</td></tr>';
document.getElementById('mensajeHistorialPersonal').textContent='Historial actualizado.';
}).withFailureHandler(function(e){document.getElementById('mensajeHistorialPersonal').textContent='No se pudo cargar el historial: '+mostrarError(e);})
.obtenerHistorialIndividualLavador(id);
});
let estadoDocumentacionCache = [];
function renderizarEstadoDocumentacion() {
const tabla=document.getElementById('tablaEstadoDocumentacion');
if (!tabla) return;
const termino=normalizarTextoBusqueda(document.getElementById('buscarEstadoDocumentacion').value);
const documentoSeleccionado=document.getElementById('filtroDocumentoEstado').value;
const estadoSeleccionado=document.getElementById('filtroEstadoDocumento').value;
const lista=estadoDocumentacionCache.filter(function(item) {
if (!normalizarTextoBusqueda(item.nombre).includes(termino)) return false;
const documentos=Array.isArray(item.documentos)?item.documentos:[];
const documentosFiltrados=documentoSeleccionado==='Todos'
? documentos
: documentos.filter(function(doc){ return doc.tipo===documentoSeleccionado; });
if (estadoSeleccionado==='Todos') return true;
if (!documentosFiltrados.length) return estadoSeleccionado==='Sin datos';
return documentosFiltrados.some(function(doc) {
const estado=doc && doc.estado ? doc.estado : 'Sin datos';
return estado===estadoSeleccionado;
});
});
tabla.replaceChildren();
if (!lista.length) {
const tr=document.createElement('tr'); const td=document.createElement('td'); td.colSpan=6;
td.textContent=estadoDocumentacionCache.length?'No hay lavadores que coincidan con los filtros seleccionados.':'No hay lavadores activos.';
tr.appendChild(td); tabla.appendChild(tr); return;
}
lista.forEach(function(item) {
const tr=document.createElement('tr');
const nombre=document.createElement('td'); nombre.textContent=item.nombre; tr.appendChild(nombre);
// El backend entrega los documentos en el orden de DOCUMENTOS_OBLIGATORIOS,
// pero la tabla tiene su propio orden de columnas. Asociarlos por tipo evita
// que los estados aparezcan bajo el encabezado equivocado.
const documentosPorTipo = new Map(
(Array.isArray(item.documentos) ? item.documentos : []).map(function(doc) {
return [doc.tipo, doc];
})
);
const ordenColumnasDocumentos = [
'Carnet de lavador',
'Licencia de conducir',
'Seguro',
'Certificado de antecedentes'
];
ordenColumnasDocumentos.forEach(function(tipo) {
const doc = documentosPorTipo.get(tipo);
const td=document.createElement('td');
const estado=document.createElement('span');
const valorEstado = doc && doc.estado ? doc.estado : 'Sin datos';
estado.className='document-status '+(valorEstado==='Faltante'?'faltante':valorEstado==='Vencido'?'vencido':valorEstado==='No aplica'?'no-aplica':'');
estado.textContent=valorEstado;
td.appendChild(estado);
if (doc && doc.fechaVencimiento) { const detalle=document.createElement('div'); detalle.className='help'; detalle.textContent=doc.fechaVencimiento; td.appendChild(detalle); }
tr.appendChild(td);
});
const tdGeneral=document.createElement('td');
const badge=document.createElement('span');
badge.className='document-status '+(item.estadoGeneral==='Faltante'?'faltante':item.estadoGeneral==='Vencido'?'vencido':'');
badge.textContent=item.estadoGeneral; tdGeneral.appendChild(badge); tr.appendChild(tdGeneral);
tabla.appendChild(tr);
});
const mensaje=document.getElementById('mensajeEstadoDocumentacion');
if (mensaje) mensaje.textContent='Lavadores activos: '+estadoDocumentacionCache.length+'. Mostrando: '+lista.length+'.';
}
function cargarEstadoDocumentacion() {
const tabla=document.getElementById('tablaEstadoDocumentacion');
const mensaje=document.getElementById('mensajeEstadoDocumentacion');
if (!tabla) return;
if (mensaje) mensaje.textContent='Consultando documentación...';
tabla.innerHTML='<tr><td colspan="6">Cargando estado de documentación...</td></tr>';
google.script.run.withSuccessHandler(function(lista) {
estadoDocumentacionCache=Array.isArray(lista)?lista:[];
renderizarEstadoDocumentacion();
}).withFailureHandler(function(error) {
estadoDocumentacionCache=[];
tabla.innerHTML='<tr><td colspan="6">No se pudo consultar: '+mostrarError(error)+'</td></tr>';
if (mensaje) mensaje.textContent='Error al consultar el estado de documentación.';
}).obtenerEstadoDocumentacionLavadores('');
}
document.getElementById('buscarEstadoDocumentacion').addEventListener('input',renderizarEstadoDocumentacion);
document.getElementById('filtroDocumentoEstado').addEventListener('change',renderizarEstadoDocumentacion);
document.getElementById('filtroEstadoDocumento').addEventListener('change',renderizarEstadoDocumentacion);
document.getElementById('btnActualizarEstadoDocumentacion').addEventListener('click',cargarEstadoDocumentacion);
function cargarLavadores() {
const selector = document.getElementById('selectorLavador');
const boton = document.getElementById('btnActualizarLavadores');
if (boton) boton.disabled = true;
google.script.run.withSuccessHandler(function(lavadores) {
const lista = (lavadores || []).filter(l => l.activo);
const anterior = selector.value;
selector.replaceChildren();
const inicial = document.createElement('option'); inicial.value = ''; inicial.textContent = 'Seleccionar lavador'; selector.appendChild(inicial);
lista.forEach(function(l) { const op = document.createElement('option'); op.value = l.id; op.textContent = l.nombre + (l.telefono ? ' — ' + l.telefono : ''); selector.appendChild(op); });
if (lavadorParaSeleccionar && lista.some(l => l.id === lavadorParaSeleccionar)) {
selector.value = lavadorParaSeleccionar;
lavadorParaSeleccionar = '';
} else if (lista.some(l => l.id === anterior)) selector.value = anterior;
if (boton) boton.disabled = false;
cargarDocumentosLavador();
}).withFailureHandler(function(error) {
if (boton) boton.disabled = false;
document.getElementById('mensajeLavador').textContent = 'No se pudo cargar la lista: ' + mostrarError(error);
}).obtenerLavadores();
}
document.getElementById('formLavador').addEventListener('submit', function(event) {
event.preventDefault();
const boton = document.getElementById('btnRegistrarLavador'), mensaje = document.getElementById('mensajeLavador');
boton.disabled = true; mensaje.textContent = 'Registrando lavador...';
google.script.run.withSuccessHandler(function(r) {
mensaje.textContent = r.mensaje || 'Lavador registrado.';
document.getElementById('formLavador').reset(); boton.disabled = false; cargarLavadores();
setTimeout(function(){ document.getElementById('selectorLavador').value = r.id; cargarDocumentosLavador(); }, 500);
}).withFailureHandler(function(error) { mensaje.textContent = 'No se pudo registrar: ' + mostrarError(error); boton.disabled = false; })
.registrarLavador({nombre:document.getElementById('nombreLavador').value.trim(), telefono:document.getElementById('telefonoLavador').value.trim(), fechaIngreso:document.getElementById('fechaIngresoLavador').value});
});
document.getElementById('btnActualizarLavadores').addEventListener('click', cargarLavadores);
document.getElementById('selectorLavador').addEventListener('change', cargarDocumentosLavador);
const btnEliminarLavador = document.getElementById('btnEliminarLavador');
if (btnEliminarLavador) btnEliminarLavador.addEventListener('click', function() {
const selector = document.getElementById('selectorLavador');
const idLavador = selector.value;
const nombreLavador = selector.options[selector.selectedIndex]?.text || '';
const boton = this;
const mensaje = document.getElementById('mensajeLavador');
if (!idLavador) {
mensaje.textContent = 'Selecciona un lavador para eliminar.';
return;
}
if (!confirm('¿Seguro que deseas desactivar a "' + nombreLavador + '"? Dejará de aparecer en la lista activa, pero se conservará su historial.')) {
return;
}
boton.disabled = true;
mensaje.textContent = 'Eliminando lavador...';
google.script.run
.withSuccessHandler(function(resultado) {
mensaje.textContent = resultado.mensaje || 'Lavador desactivado.';
document.getElementById('panelDocumentosLavador').hidden = true;
document.getElementById('selectorLavador').value = '';
cargarLavadores();
boton.disabled = false;
})
.withFailureHandler(function(error) {
mensaje.textContent = 'No se pudo eliminar: ' + mostrarError(error);
boton.disabled = false;
})
.eliminarLavador(idLavador);
});
const btnEliminarLavadorDefinitivo = document.getElementById('btnEliminarLavadorDefinitivo');
if (btnEliminarLavadorDefinitivo) btnEliminarLavadorDefinitivo.addEventListener('click', function() {
const selector = document.getElementById('selectorLavador');
const idLavador = selector.value;
const nombreLavador = selector.options[selector.selectedIndex]?.text || '';
const boton = this;
const mensaje = document.getElementById('mensajeLavador');
if (!idLavador) {
mensaje.textContent = 'Selecciona un lavador para eliminar definitivamente.';
return;
}
const confirmacion = confirm(
'ATENCIÓN: vas a eliminar definitivamente a "' + nombreLavador + '". ' +
'Se borrará su registro y sus documentos de la hoja de cálculo; los archivos asociados se enviarán a la papelera de Google Drive. ' +
'Esta acción no se puede deshacer desde la aplicación. ¿Deseas continuar?'
);
if (!confirmacion) return;
const segundaConfirmacion = prompt('Para confirmar, escribe ELIMINAR');
if (segundaConfirmacion !== 'ELIMINAR') {
mensaje.textContent = 'Eliminación cancelada. Debes escribir ELIMINAR exactamente para continuar.';
return;
}
boton.disabled = true;
mensaje.textContent = 'Eliminando lavador y enviando sus archivos a la papelera...';
google.script.run
.withSuccessHandler(function(resultado) {
mensaje.textContent = resultado.mensaje || 'Lavador eliminado definitivamente.';
document.getElementById('panelDocumentosLavador').hidden = true;
selector.value = '';
cargarLavadores();
boton.disabled = false;
})
.withFailureHandler(function(error) {
mensaje.textContent = 'No se pudo eliminar definitivamente: ' + mostrarError(error);
boton.disabled = false;
})
.eliminarLavadorDefinitivo(idLavador);
});
function cargarDocumentosLavador() {
const id = document.getElementById('selectorLavador').value, panel = document.getElementById('panelDocumentosLavador'), tabla = document.getElementById('tablaDocumentosLavador');
tabla.replaceChildren(); panel.hidden = !id; if (!id) return;
const trCarga = document.createElement('tr'), tdCarga = document.createElement('td'); tdCarga.colSpan = 6; tdCarga.textContent = 'Cargando documentos...'; trCarga.appendChild(tdCarga); tabla.appendChild(trCarga);
google.script.run.withSuccessHandler(function(docs) {
tabla.replaceChildren();
(docs || []).forEach(function(doc) {
const tr = document.createElement('tr');
const tdTipo = document.createElement('td'); tdTipo.textContent = doc.tipo; tr.appendChild(tdTipo);
const tdRequisito = document.createElement('td');
const selector = document.createElement('select');
const opObligatorio = document.createElement('option'); opObligatorio.value = 'true'; opObligatorio.textContent = 'Obligatorio'; selector.appendChild(opObligatorio);
const opNoAplica = document.createElement('option'); opNoAplica.value = 'false'; opNoAplica.textContent = 'No aplica'; selector.appendChild(opNoAplica);
selector.value = doc.requerido === false ? 'false' : 'true';
selector.addEventListener('change', function() {
selector.disabled = true;
google.script.run.withSuccessHandler(function(r) {
cargarDocumentosLavador();
cargarResumenPersonal();
cargarEstadoDocumentacion();
const aviso = document.getElementById('mensajeDocumentoLavador'); if (aviso) aviso.textContent = r.mensaje;
}).withFailureHandler(function(error) {
selector.disabled = false; selector.value = doc.requerido === false ? 'false' : 'true';
const aviso = document.getElementById('mensajeDocumentoLavador'); if (aviso) aviso.textContent = 'No se pudo actualizar el requisito: ' + mostrarError(error);
}).guardarAplicabilidadDocumento(id, doc.tipo, selector.value === 'true');
});
tdRequisito.appendChild(selector); tr.appendChild(tdRequisito);
const estado = doc.requerido === false ? 'No aplica' : doc.estado;
[estado, doc.fechaEmision || '—', doc.noVence ? 'No vence' : (doc.fechaVencimiento || '—')].forEach(function(valor) { const td = document.createElement('td'); td.textContent = valor; tr.appendChild(td); });
const tdArchivo = document.createElement('td');
if (doc.url) { const a = document.createElement('a'); a.href = doc.url; a.target = '_blank'; a.rel = 'noopener'; a.textContent = doc.nombreArchivo || 'Abrir archivo'; tdArchivo.appendChild(a); } else tdArchivo.textContent = 'Sin archivo';
tr.appendChild(tdArchivo); tabla.appendChild(tr);
});
}).withFailureHandler(function(error) { tabla.replaceChildren(); const tr = document.createElement('tr'), td = document.createElement('td'); td.colSpan = 6; td.textContent = 'No se pudieron cargar los documentos: ' + mostrarError(error); tr.appendChild(td); tabla.appendChild(tr); }).obtenerDocumentosLavador(id);
}
document.getElementById('documentoNoVence').addEventListener('change', function() {
const campo = document.getElementById('fechaVencimientoDocumento'); campo.disabled = this.checked; if (this.checked) campo.value = '';
});
document.getElementById('formDocumentoLavador').addEventListener('submit', function(event) {
event.preventDefault();
const idLavador = document.getElementById('selectorLavador').value, tipo = document.getElementById('tipoDocumentoLavador').value;
const fechaEmision = document.getElementById('fechaEmisionDocumento').value, noVence = document.getElementById('documentoNoVence').checked;
const fechaVencimiento = document.getElementById('fechaVencimientoDocumento').value, input = document.getElementById('archivoDocumentoLavador'), archivo = input.files && input.files[0];
const boton = document.getElementById('btnGuardarDocumentoLavador'), mensaje = document.getElementById('mensajeDocumentoLavador');
if (!idLavador) { mensaje.textContent = 'Selecciona un lavador.'; return; }
if (!archivo) { mensaje.textContent = 'Selecciona el archivo del documento.'; return; }
if (archivo.size > 6 * 1024 * 1024) { mensaje.textContent = 'El archivo supera los 6 MB. Reduce su tamaño antes de subirlo.'; return; }
if (!noVence && !fechaVencimiento) { mensaje.textContent = 'Indica la fecha de vencimiento o marca «No vence».'; return; }
boton.disabled = true; mensaje.textContent = 'Subiendo documento a Google Drive...';
const reader = new FileReader();
reader.onload = function() {
const base64 = String(reader.result).split(',')[1];
google.script.run.withSuccessHandler(function(r) {
mensaje.textContent = r.mensaje || 'Documento guardado.';
document.getElementById('formDocumentoLavador').reset(); document.getElementById('fechaVencimientoDocumento').disabled = false;
boton.disabled = false; cargarDocumentosLavador(); cargarEstadoDocumentacion();
}).withFailureHandler(function(error) { mensaje.textContent = 'No se pudo guardar el documento: ' + mostrarError(error); boton.disabled = false; })
.guardarDocumentoLavador({idLavador:idLavador, tipo:tipo, fechaEmision:fechaEmision, fechaVencimiento:fechaVencimiento, noVence:noVence,
archivo:{nombre:archivo.name,mimeType:archivo.type || 'application/octet-stream',base64:base64}});
};
reader.onerror = function() { mensaje.textContent = 'No se pudo leer el archivo.'; boton.disabled = false; };
reader.readAsDataURL(archivo);
});
document.getElementById('btnProbarAvisoPersonal').addEventListener('click', function() {
const boton = this, mensaje = document.getElementById('mensajeAvisoPersonal'); boton.disabled = true; mensaje.textContent = 'Revisando documentación...';
google.script.run.withSuccessHandler(function(r) { mensaje.textContent = r.mensaje || 'Revisión completada.'; boton.disabled = false; })
.withFailureHandler(function(error) { mensaje.textContent = 'Falló la revisión: ' + mostrarError(error); boton.disabled = false; }).probarAvisoPersonal();
});
function abrirDetalleDocumentos(tipo) {
tipoAlertaDocumentosActual = tipo || 'faltantes';
mostrarVista('detalleDocumentos');
}
function cargarDetalleDocumentos(tipo) {
tipoAlertaDocumentosActual = tipo || tipoAlertaDocumentosActual;
const nombres = {
faltantes: ['Documentos faltantes', 'Lavadores activos que todavía no tienen cargado el archivo obligatorio.'],
porVencer: ['Documentos por vencer en 30 días', 'Documentos con vencimiento desde hoy hasta los próximos 30 días.'],
vencidos: ['Documentos vencidos', 'Documentos cuya fecha de vencimiento ya pasó.']
};
const info = nombres[tipoAlertaDocumentosActual] || nombres.faltantes;
document.getElementById('tituloDetalleDocumentos').textContent = info[0];
document.getElementById('descripcionDetalleDocumentos').textContent = info[1];
const tabla = document.getElementById('tablaDetalleDocumentos');
const mensaje = document.getElementById('mensajeDetalleDocumentos');
tabla.replaceChildren(); mensaje.textContent = 'Cargando documentación...';
const carga = document.createElement('tr'), celda = document.createElement('td');
celda.colSpan = 5; celda.textContent = 'Cargando lista...'; carga.appendChild(celda); tabla.appendChild(carga);
google.script.run.withSuccessHandler(function(items) {
tabla.replaceChildren(); mensaje.textContent = (items || []).length + ' registro(s).';
if (!(items || []).length) {
const tr = document.createElement('tr'), td = document.createElement('td');
td.colSpan = 5; td.textContent = 'No hay documentos en esta categoría.'; tr.appendChild(td); tabla.appendChild(tr); return;
}
items.forEach(function(item) {
const tr = document.createElement('tr');
[item.nombreLavador, item.tipo, item.estado, item.fechaVencimiento || '—'].forEach(function(valor) {
const td = document.createElement('td'); td.textContent = valor; tr.appendChild(td);
});
const tdAccion = document.createElement('td');
if (item.url) {
const a = document.createElement('a'); a.href = item.url; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Abrir archivo'; tdAccion.appendChild(a);
tdAccion.appendChild(document.createTextNode(' · '));
}
const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'secondary'; btn.textContent = 'Abrir ficha';
btn.addEventListener('click', function() { abrirFichaLavador(item.idLavador); });
tdAccion.appendChild(btn); tr.appendChild(tdAccion); tabla.appendChild(tr);
});
}).withFailureHandler(function(error) {
tabla.replaceChildren(); mensaje.textContent = 'No se pudo cargar el detalle: ' + mostrarError(error);
const tr = document.createElement('tr'), td = document.createElement('td'); td.colSpan = 5; td.textContent = 'Error al consultar los documentos.'; tr.appendChild(td); tabla.appendChild(tr);
}).obtenerDetalleDocumentos(tipoAlertaDocumentosActual);
}
function abrirFichaLavador(idLavador) {
lavadorParaSeleccionar = idLavador;
mostrarVista('personal');
}
function cargarResumenPersonal() {
const tabla = document.getElementById('tablaResumenDocumentos');
const mensaje = document.getElementById('mensajeResumenPersonal');
if (mensaje) mensaje.textContent = '';
google.script.run.withSuccessHandler(function(resumen) {
document.getElementById('totalLavadoresDashboard').textContent = resumen.totalLavadores;
document.getElementById('totalFaltantesDashboard').textContent = resumen.totalFaltantes;
document.getElementById('totalPorVencerDashboard').textContent = resumen.totalPorVencer;
document.getElementById('totalVencidosDashboard').textContent = resumen.totalVencidos;
tabla.replaceChildren();
(resumen.porTipo || []).forEach(function(item) {
const tr = document.createElement('tr');
const tdTipo = document.createElement('td'); tdTipo.textContent = item.tipo; tr.appendChild(tdTipo);
const tdFaltantes = document.createElement('td'); tdFaltantes.textContent = item.faltantes; tr.appendChild(tdFaltantes);
tabla.appendChild(tr);
});
if (!(resumen.porTipo || []).length) {
const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 2; td.textContent = 'No hay documentos obligatorios configurados.'; tr.appendChild(td); tabla.appendChild(tr);
}
}).withFailureHandler(function(error) {
tabla.replaceChildren(); const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 2; td.textContent = 'No se pudo cargar el resumen de lavadores.'; tr.appendChild(td); tabla.appendChild(tr);
if (mensaje) mensaje.textContent = mostrarError(error);
}).obtenerResumenPersonal();
}
window.addEventListener('load',function() {
mostrarVista('inicio');
cargarInventario();
cargarSolicitudes();
cargarResumenPersonal();
});
</script>
</body>
</html>
131,582 chars · codepastes.com