Files
fleet-management/app/templates/admin/dashboard.html
T
alro65 5b7b41aa50 Initial commit: Fleet Management app with security hardening and background launcher
- Flask app with SQLAlchemy, Flask-Login, Flask-Mail
- Admin/owner roles, vessel management, charters, work orders
- Background launcher (Iniciar.vbs) runs server without terminal window
- Root redirect fixed: / → /login
- debug=False, use_reloader=False for pythonw.exe compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 02:54:10 -04:00

1540 lines
92 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<title>Fleet Management - Admin</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; }
.header { background: #0a2a3a; color: white; padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; border-bottom: 3px solid #c4a747; }
.header h1 { font-size: 22px; }
.header h1 span { color: #c4a747; }
.logout-btn { background: #c4a747; color: #0a2a3a; padding: 8px 18px; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 14px; }
.logout-btn:hover { background: #d4b757; }
.nav-tabs { background: white; border-bottom: 2px solid #e0e0e0; padding: 0 30px; display: flex; flex-wrap: wrap; gap: 2px; }
.tab-btn { padding: 13px 22px; background: none; border: none; cursor: pointer; font-size: 14px; font-weight: 600; color: #666; border-bottom: 3px solid transparent; transition: all 0.2s; }
.tab-btn:hover { color: #0a2a3a; background: #f5f5f5; }
.tab-btn.active { color: #c4a747; border-bottom-color: #c4a747; background: #fffbf0; }
.container { padding: 25px 30px; max-width: 1500px; margin: 0 auto; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.card { background: white; border-radius: 10px; padding: 25px; margin-bottom: 20px; box-shadow: 0 1px 6px rgba(0,0,0,0.07); border-left: 4px solid #c4a747; }
.card h3 { color: #0a2a3a; margin-bottom: 18px; border-bottom: 2px solid #f0e8cc; padding-bottom: 10px; font-size: 16px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 11px 12px; text-align: left; border-bottom: 1px solid #f0f0f0; }
th { background: #0a2a3a; color: white; font-weight: 600; }
tr:hover td { background: #fafaf8; }
.btn { background: #0a2a3a; color: white; padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: 600; margin: 3px; transition: background 0.2s; }
.btn:hover { background: #c4a747; color: #0a2a3a; }
.btn-sm { padding: 5px 11px; font-size: 12px; }
.btn-gold { background: #c4a747; color: #0a2a3a; }
.btn-gold:hover { background: #d4b757; }
.btn-success { background: #27ae60; color: white; }
.btn-success:hover { background: #2ecc71; color: white; }
.btn-warning { background: #e67e22; color: white; }
.btn-warning:hover { background: #d35400; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn-danger:hover { background: #c0392b; color: white; }
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 18px; margin-bottom: 20px; }
.stat-card { background: white; border-radius: 10px; padding: 20px 15px; text-align: center; border-top: 4px solid #c4a747; box-shadow: 0 1px 6px rgba(0,0,0,0.07); }
.stat-card h4 { color: #666; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.stat-card .value { font-size: 30px; font-weight: bold; color: #0a2a3a; }
.stat-card .sub { font-size: 11px; color: #999; margin-top: 4px; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; margin-bottom: 5px; color: #0a2a3a; font-weight: 600; font-size: 13px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 9px 12px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #c4a747; box-shadow: 0 0 0 2px rgba(196,167,71,0.15); }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 15px; }
.form-group textarea { min-height: 80px; resize: vertical; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.55); justify-content: center; align-items: flex-start; z-index: 1000; padding-top: 40px; overflow-y: auto; }
.modal-content { background: white; padding: 30px; border-radius: 10px; width: 600px; max-width: 95%; margin-bottom: 40px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 2px solid #f0e8cc; padding-bottom: 12px; }
.modal-header h3 { color: #0a2a3a; font-size: 18px; }
.close { cursor: pointer; font-size: 26px; color: #999; line-height: 1; }
.close:hover { color: #0a2a3a; }
.owner-card { background: white; border-radius: 10px; margin-bottom: 18px; overflow: hidden; box-shadow: 0 1px 6px rgba(0,0,0,0.07); }
.owner-header { background: #0a2a3a; color: white; padding: 14px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
.owner-header:hover { background: #0d3448; }
.owner-meta { padding: 12px 20px; background: #f7f9fa; display: flex; gap: 25px; flex-wrap: wrap; font-size: 13px; border-bottom: 1px solid #eee; }
.owner-vessels { padding: 15px 20px 20px 20px; display: none; }
.owner-vessels.show { display: block; }
.vessel-detail { background: #fafafa; border-radius: 8px; margin-bottom: 14px; padding: 15px; border: 1px solid #e8e8e8; }
.vessel-name { font-size: 17px; font-weight: bold; color: #0a2a3a; margin-bottom: 10px; }
.vessel-specs { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; margin-bottom: 12px; }
.spec-item { background: white; padding: 7px 10px; border-radius: 5px; border: 1px solid #eee; font-size: 13px; }
.spec-label { font-weight: bold; color: #0a2a3a; display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.3px; color: #888; }
.spec-value { color: #0a2a3a; font-weight: 600; }
.history-item { background: #f9f9f9; padding: 13px 15px; margin: 8px 0; border-left: 3px solid #c4a747; border-radius: 0 5px 5px 0; font-size: 13px; }
.history-item strong { color: #0a2a3a; }
.message-bar { padding: 10px 15px; margin: 0 0 15px 0; border-radius: 5px; display: none; font-size: 14px; font-weight: 600; }
.message-bar.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.message-bar.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 12px; font-size: 11px; font-weight: 600; }
.badge-draft { background: #e9ecef; color: #495057; }
.badge-signed { background: #cce5ff; color: #004085; }
.badge-completed { background: #d4edda; color: #155724; }
.badge-paid { background: #c4a747; color: #0a2a3a; }
.badge-pending { background: #fff3cd; color: #856404; }
.badge-approved { background: #cce5ff; color: #004085; }
.badge-done { background: #d4edda; color: #155724; }
.badge-rejected { background: #f8d7da; color: #721c24; }
/* Priority badges */
.badge-normal { background: #e9ecef; color: #495057; }
.badge-urgente { background: #ffeeba; color: #856404; border: 1px solid #f39c12; }
.badge-emergencia { background: #f8d7da; color: #721c24; border: 1px solid #e74c3c; animation: pulse-red 1.5s infinite; }
@keyframes pulse-red { 0%,100% { box-shadow: 0 0 0 0 rgba(231,76,60,0.4); } 50% { box-shadow: 0 0 0 4px rgba(231,76,60,0); } }
/* Notification modal */
.notify-modal .modal-content { border-top: 4px solid #25D366; }
.notify-channels { display: flex; gap: 12px; margin-bottom: 18px; }
.btn-wa { background: #25D366; color: white; display: flex; align-items: center; gap: 6px; padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; text-decoration: none; }
.btn-wa:hover { background: #1da851; color: white; }
.btn-email { background: #0a2a3a; color: white; display: flex; align-items: center; gap: 6px; padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; text-decoration: none; }
.btn-email:hover { background: #c4a747; color: #0a2a3a; }
.msg-preview { background: #f7f9fa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; font-size: 13px; white-space: pre-wrap; color: #333; max-height: 260px; overflow-y: auto; line-height: 1.6; }
.priority-emergency-row td { background: #fff5f5 !important; }
.priority-urgent-row td { background: #fffbf0 !important; }
.notified-check { color: #27ae60; font-size: 12px; }
/* Notification bell on charter row */
.btn-notify { background: none; border: 1px solid #25D366; color: #25D366; padding: 4px 8px; border-radius: 5px; cursor: pointer; font-size: 12px; }
.btn-notify:hover { background: #25D366; color: white; }
.total-preview { background: #f7f9fa; border: 1px solid #e0e8cc; border-radius: 5px; padding: 12px 15px; margin-top: 8px; font-size: 13px; }
.total-preview strong { color: #0a2a3a; }
.section-title { color: #0a2a3a; font-size: 14px; font-weight: 700; margin: 18px 0 10px 0; padding-bottom: 6px; border-bottom: 1px dashed #e0d8b0; }
@media (max-width: 900px) {
.stats-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 600px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.form-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<div style="display:flex;flex-direction:column;gap:3px;">
<h1>Fleet <span>Management</span></h1>
<div style="font-size:12px;color:#a0bfcc;letter-spacing:0.3px;">{{ user.company.name if user.company else '' }}</div>
</div>
<div style="display:flex;gap:10px;align-items:center;">
{% if user.is_super_admin %}
<a href="/admin/companies" class="btn btn-sm btn-gold" style="font-size:11px;">&#9776; Gestionar Companias</a>
{% endif %}
<a href="{{ url_for('auth.logout') }}" class="logout-btn">Cerrar Sesión</a>
</div>
</div>
<div class="nav-tabs">
<button class="tab-btn active" onclick="showTab('dashboard', this)">Dashboard</button>
<button class="tab-btn" onclick="showTab('owners', this)">Duenos y Botes</button>
<button class="tab-btn" onclick="showTab('charters', this)">Charters</button>
<button class="tab-btn" onclick="showTab('captains', this)">Capitanes</button>
<button class="tab-btn" onclick="showTab('workorders', this)">Work Orders</button>
<button class="tab-btn" onclick="showTab('history', this)">Historial</button>
<button class="tab-btn" onclick="showTab('accounting', this)">Contabilidad</button>
</div>
<div class="container">
<div id="message" class="message-bar"></div>
<!-- DASHBOARD -->
<div id="dashboard" class="tab-content active">
<div class="stats-grid">
<div class="stat-card"><h4>Duenos</h4><div class="value" id="statOwners">0</div></div>
<div class="stat-card"><h4>Botes</h4><div class="value" id="statVessels">0</div></div>
<div class="stat-card"><h4>Capitanes</h4><div class="value" id="statCaptains">0</div></div>
<div class="stat-card"><h4>Charters</h4><div class="value" id="statCharters">0</div><div class="sub">completados</div></div>
<div class="stat-card"><h4>Ingresos</h4><div class="value" id="statRevenue">$0</div><div class="sub">comision mgmt</div></div>
</div>
<div class="card"><h3>Bienvenido al Sistema de Gestion de Flota</h3><p style="color:#666;font-size:14px;">Use las pestanas para gestionar duenos, embarcaciones, charters, capitanes, mantenimiento y contabilidad.</p></div>
</div>
<!-- DUENOS Y BOTES -->
<div id="owners" class="tab-content">
<div class="card">
<h3>Duenos y sus Embarcaciones</h3>
<div style="margin-bottom:15px;">
<button class="btn" onclick="openModal('ownerModal')">+ Nuevo Dueno</button>
<button class="btn btn-gold" onclick="openModal('vesselModal')">+ Nueva Embarcacion</button>
</div>
<div id="ownersWithVessels"></div>
</div>
</div>
<!-- CHARTERS -->
<div id="charters" class="tab-content">
<div class="card">
<h3>Gestion de Charters</h3>
<button class="btn" onclick="openModal('charterModal')">+ Nuevo Charter</button>
<div style="margin-top:15px; overflow-x:auto;">
<table>
<thead>
<tr>
<th>Fecha/Hora</th>
<th>Bote</th>
<th>Dueno</th>
<th>Cliente</th>
<th>Capitán</th>
<th>Horas</th>
<th>Total</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="chartersList"></tbody>
</table>
</div>
</div>
</div>
<!-- CAPITANES -->
<div id="captains" class="tab-content">
<div class="card">
<h3>Capitanes (Socios)</h3>
<button class="btn" onclick="openModal('captainModal')">+ Nuevo Capitan</button>
<table style="margin-top:15px;">
<thead>
<tr><th>Nombre</th><th>Telefono</th><th>Licencia</th><th>Tarifa/hora</th></tr>
</thead>
<tbody id="captainsList"></tbody>
</table>
</div>
</div>
<!-- WORK ORDERS -->
<div id="workorders" class="tab-content">
<div class="card">
<h3>Work Orders (Mantenimiento)</h3>
<button class="btn" onclick="openModal('workorderModal')">+ Nueva Work Order</button>
<table style="margin-top:15px;">
<thead>
<tr><th>Fecha</th><th>Bote</th><th>Dueno</th><th>Descripcion</th><th>Costo Est.</th><th>Prioridad</th><th>Estado</th><th>Acciones</th></tr>
</thead>
<tbody id="workordersList"></tbody>
</table>
</div>
</div>
<!-- HISTORIAL -->
<div id="history" class="tab-content">
<div class="card">
<h3>Historial por Bote</h3>
<div class="form-group" style="max-width:400px;">
<label>Seleccionar Bote:</label>
<select id="historyVesselSelect" onchange="loadHistory()"></select>
</div>
<div id="historyContent"></div>
</div>
</div>
<!-- CONTABILIDAD -->
<div id="accounting" class="tab-content">
<!-- Resumen global -->
<div class="stats-grid" style="grid-template-columns:repeat(3,1fr);">
<div class="stat-card"><h4>Ingresos Totales</h4><div class="value" id="accRevenue">$0</div></div>
<div class="stat-card"><h4>Gastos Totales</h4><div class="value" id="accExpenses">$0</div></div>
<div class="stat-card"><h4>Utilidad Neta</h4><div class="value" id="accProfit">$0</div></div>
</div>
<!-- P&L consolidado por bote -->
<div class="card">
<h3>P&amp;L Consolidado por Embarcacion</h3>
<div style="margin-bottom:12px;">
<button class="btn btn-sm" onclick="syncAllAccounting()">Sincronizar desde Charters y WOs</button>
</div>
<table>
<thead>
<tr><th>Bote</th><th>Dueno</th><th>Ing. Charters</th><th>Ing. Plan</th><th>Gastos (WO)</th><th>Combustible</th><th>Utilidad</th><th></th></tr>
</thead>
<tbody id="pnlBody"></tbody>
</table>
</div>
<!-- Tarjetas por embarcacion con acceso al diario -->
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:6px;">
<h3 style="margin:0;">Diario Contable por Embarcacion</h3>
<div style="display:flex;gap:8px;">
<button class="btn btn-sm" style="background:#27ae60;color:white;" onclick="openFuelModal()">+ Combustible</button>
<button class="btn btn-sm btn-gold" onclick="openModal('entryModal')">+ Asiento Manual</button>
</div>
</div>
<p style="color:#666;font-size:13px;margin:0 0 18px;">
Cada embarcacion tiene su propio diario contable con ingresos, gastos, facturas de combustible, work orders y mantenimiento.
Todo gasto e ingreso queda enlazado automaticamente.
</p>
<div id="vesselAccountingCards" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:16px;">
<div style="color:#999;text-align:center;padding:40px;grid-column:1/-1;">Cargando embarcaciones...</div>
</div>
</div>
<!-- Combustible reciente (ultimos registros) -->
<div class="card">
<h3>Ultimos Registros de Combustible</h3>
<div style="overflow-x:auto;">
<table>
<thead>
<tr><th>Fecha</th><th>Bote</th><th>Gallons</th><th>$/Gal</th><th>Total</th><th>Proveedor</th><th>Invoice</th><th>Charter</th><th></th></tr>
</thead>
<tbody id="fuelBody">
<tr><td colspan="9" style="color:#999;text-align:center;">Sin registros de combustible</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- =========== MODALES =========== -->
<!-- Modal: Nuevo Dueno -->
<div id="ownerModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Nuevo Dueno</h3>
<span class="close" onclick="closeModal('ownerModal')">&times;</span>
</div>
<form id="ownerForm">
<div class="form-group"><label>Nombre *</label><input type="text" id="ownerName" required></div>
<div class="form-grid">
<div class="form-group"><label>Email</label><input type="email" id="ownerEmail"></div>
<div class="form-group"><label>Telefono</label><input type="text" id="ownerPhone"></div>
</div>
<button type="submit" class="btn btn-gold">Guardar</button>
</form>
</div>
</div>
<!-- Modal: Nueva Embarcacion -->
<div id="vesselModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="vesselModalTitle">Nueva Embarcacion</h3>
<span class="close" onclick="closeModal('vesselModal')">&times;</span>
</div>
<form id="vesselForm">
<input type="hidden" id="vesselId">
<div class="form-group"><label>Nombre del Bote *</label><input type="text" id="vesselName" required></div>
<div class="form-grid">
<div class="form-group"><label>Dueno *</label><select id="vesselOwnerId" required></select></div>
<div class="form-group"><label>Plan de Servicio</label>
<select id="vesselPlan">
<option value="1">Basico ($199/mes) - Solo charters</option>
<option value="2">Estandar ($399/mes) - Charters + mant.</option>
<option value="3">Mantenimiento ($299/mes) - Solo mant.</option>
<option value="4">Plus ($599/mes) - Todo incluido</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group"><label>Marca / Tipo</label><input type="text" id="vesselMake" placeholder="Ej: Sea Ray, Azimut"></div>
<div class="form-group"><label>Modelo</label><input type="text" id="vesselModel" placeholder="Ej: 330 Sundancer"></div>
</div>
<div class="form-group"><label>Motores</label><input type="text" id="vesselEngines" placeholder="Ej: 2 x Mercruiser 350"></div>
<div class="form-grid">
<div class="form-group"><label>Eslora (pies)</label><input type="number" id="vesselLength" step="1" min="0"></div>
<div class="form-group"><label>Consumo Gal/h (a 14 nudos)</label><input type="number" id="vesselFuel" step="1" min="0"></div>
</div>
<div class="form-grid">
<div class="form-group"><label>Tarifa Base 4 horas ($)</label><input type="number" id="vesselRate" step="50" min="0"></div>
<div class="form-group"><label>Tarifa Hora Extra ($)</label><input type="number" id="vesselExtraRate" step="25" min="0"></div>
</div>
<div class="form-grid">
<div class="form-group"><label>Max. Personas (segun doc. oficial del bote, incl. capitan)</label><input type="number" id="vesselMaxPax" step="1" min="1" max="50" value="12"></div>
<div class="form-group"><label>% Comision Management</label><input type="number" id="vesselPct" step="1" min="0" max="100" value="25"></div>
</div>
<button type="submit" class="btn btn-gold">Guardar</button>
</form>
</div>
</div>
<!-- Modal: Nuevo Capitan -->
<div id="captainModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Nuevo Capitan</h3>
<span class="close" onclick="closeModal('captainModal')">&times;</span>
</div>
<form id="captainForm">
<div class="form-group"><label>Nombre *</label><input type="text" id="captainName" required></div>
<div class="form-grid">
<div class="form-group"><label>Telefono</label><input type="text" id="captainPhone"></div>
<div class="form-group"><label>N. Licencia</label><input type="text" id="captainLicense"></div>
</div>
<div class="form-group" style="max-width:200px;"><label>Tarifa/hora ($)</label><input type="number" id="captainRate" step="5" min="0"></div>
<button type="submit" class="btn btn-gold">Guardar</button>
</form>
</div>
</div>
<!-- Modal: Nuevo Charter -->
<div id="charterModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="charterModalTitle">Nuevo Charter</h3>
<span class="close" onclick="closeModal('charterModal')">&times;</span>
</div>
<form id="charterForm">
<div class="form-group"><label>Embarcacion *</label><select id="charterVesselId" required onchange="updateCharterTotal()"></select></div>
<div class="form-group"><label>Cliente *</label><input type="text" id="chartererName" required></div>
<div class="form-grid">
<div class="form-group"><label>Email Cliente</label><input type="email" id="chartererEmail"></div>
<div class="form-group"><label>Telefono Cliente</label><input type="text" id="chartererPhone"></div>
</div>
<div class="form-grid">
<div class="form-group"><label>Fecha y Hora *</label><input type="datetime-local" id="charterStart" required></div>
<div class="form-group"><label>Horas * (minimo 4)</label><input type="number" id="charterHours" step="0.5" min="4" value="4" required onchange="updateCharterTotal()"></div>
</div>
<div class="total-preview" id="charterTotalPreview" style="display:none;">
<strong>Total estimado:</strong> $<span id="charterTotalValue">0</span>
<br><small id="charterTotalBreakdown" style="color:#666;"></small>
</div>
<hr style="margin:16px 0;border-color:#eee;">
<h4 style="color:#0a2a3a;margin:0 0 12px;font-size:14px;">Capitán y Seguro</h4>
<div class="form-group">
<label>Capitán (opcional)</label>
<select id="charterCaptainId">
<option value="">-- Sin capitán asignado --</option>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group">
<label>N° Rider de Seguro</label>
<input type="text" id="charterRiderNumber" placeholder="ej. MRK-2026-00142">
</div>
<div class="form-group">
<label>Aseguradora</label>
<input type="text" id="charterInsurer" placeholder="ej. Markel Marine">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group">
<label>Suma Asegurada ($)</label>
<input type="number" id="charterCoverage" placeholder="500000">
</div>
<div class="form-group">
<label>Damage Waiver ($)</label>
<input type="number" id="charterDamageWaiver" placeholder="0" value="0">
</div>
</div>
<button type="submit" class="btn btn-gold" style="margin-top:15px;">Guardar Charter</button>
</form>
</div>
</div>
<!-- Modal: Completar Charter (Voucher) -->
<div id="completeModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Completar Charter - Generar Voucher</h3>
<span class="close" onclick="closeModal('completeModal')">&times;</span>
</div>
<form id="completeForm">
<input type="hidden" id="completeCharterId">
<p id="completeCharterInfo" style="background:#f7f9fa;padding:12px;border-radius:5px;margin-bottom:15px;font-size:13px;"></p>
<div class="form-grid">
<div class="form-group"><label>Combustible consumido (gallons)</label><input type="number" id="completeFuel" step="1" min="0" value="0"></div>
<div class="form-group"><label>Cargo por desviacion ($)</label><input type="number" id="completeDeviation" step="10" min="0" value="0"></div>
</div>
<div class="form-group" style="max-width:200px;"><label>Propina (% del total)</label>
<select id="completeTipPct">
<option value="15">15%</option>
<option value="18" selected>18%</option>
<option value="20">20%</option>
</select>
</div>
<div class="total-preview" id="voucherPreview"></div>
<button type="submit" class="btn btn-success" style="margin-top:15px;">Confirmar y Generar Voucher</button>
</form>
</div>
</div>
<!-- Modal: Asiento Contable Manual -->
<div id="entryModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Nuevo Asiento Contable</h3>
<span class="close" onclick="closeModal('entryModal')">&times;</span>
</div>
<form id="entryForm">
<div class="form-grid">
<div class="form-group"><label>Embarcacion *</label><select id="entryVesselId" required></select></div>
<div class="form-group"><label>Fecha *</label><input type="date" id="entryDate" required></div>
</div>
<div class="form-grid">
<div class="form-group"><label>Tipo *</label>
<select id="entryType" onchange="updateEntryCategories()" required>
<option value="income">Ingreso</option>
<option value="expense">Gasto</option>
</select>
</div>
<div class="form-group"><label>Categoria *</label>
<select id="entryCategory" required></select>
</div>
</div>
<div class="form-group"><label>Descripcion *</label><input type="text" id="entryDescription" required></div>
<div class="form-grid">
<div class="form-group"><label>Monto ($) *</label><input type="number" id="entryAmount" step="0.01" min="0.01" required></div>
<div class="form-group"><label>N. Invoice / Referencia</label><input type="text" id="entryInvoice" placeholder="Ej: INV-2026-001"></div>
</div>
<div class="form-group"><label>Notas</label><textarea id="entryNotes" rows="2"></textarea></div>
<button type="submit" class="btn btn-gold">Guardar Asiento</button>
</form>
</div>
</div>
<!-- Modal: Registro de Combustible -->
<div id="fuelModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Registro de Combustible</h3>
<span class="close" onclick="closeModal('fuelModal')">&times;</span>
</div>
<form id="fuelForm">
<div class="form-grid">
<div class="form-group"><label>Embarcacion *</label><select id="fuelVesselId" required></select></div>
<div class="form-group"><label>Fecha *</label><input type="date" id="fuelDate" required></div>
</div>
<div class="form-grid">
<div class="form-group"><label>Gallons</label><input type="number" id="fuelLiters" step="0.1" min="0" oninput="calcFuelTotal()"></div>
<div class="form-group"><label>Precio por galon ($)</label><input type="number" id="fuelPpl" step="0.01" min="0" oninput="calcFuelTotal()"></div>
</div>
<div class="form-group" style="max-width:200px;"><label>Total ($) *</label><input type="number" id="fuelTotal" step="0.01" min="0.01" required></div>
<div class="form-grid">
<div class="form-group"><label>Proveedor / Marina</label><input type="text" id="fuelSupplier" placeholder="Ej: Miami Marine Fuel"></div>
<div class="form-group"><label>N. Invoice / Recibo</label><input type="text" id="fuelInvoice" placeholder="Ej: REC-2026-042"></div>
</div>
<div class="form-group"><label>Charter relacionado (opcional)</label><select id="fuelCharterId"><option value="">-- Ninguno --</option></select></div>
<div class="form-group"><label>Notas</label><textarea id="fuelNotes" rows="2"></textarea></div>
<button type="submit" class="btn btn-gold">Guardar Registro</button>
</form>
</div>
</div>
<!-- Modal: Enviar Contratos -->
<div id="sendContractsModal" class="modal">
<div class="modal-content" style="max-width:460px;">
<div class="modal-header">
<h3>Enviar Contratos por Email</h3>
<span class="close" onclick="closeModal('sendContractsModal')">&times;</span>
</div>
<div id="sendContractsInfo" style="background:#f0f4f8;border-radius:8px;padding:12px;margin-bottom:16px;font-size:13px;color:#0a2a3a;"></div>
<p style="font-size:13px;color:#555;margin:0 0 12px;">Documentos a enviar:</p>
<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:18px;">
<label style="display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer;">
<input type="checkbox" id="sendToCharterer" checked> 📄 Contrato de Charter → al Arrendatario
</label>
<label style="display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer;">
<input type="checkbox" id="sendToOwner" checked> 🏠 Copia + Resumen financiero → al Dueno del Bote
</label>
<label style="display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer;">
<input type="checkbox" id="sendToCaptain" checked> 👤 Contrato de Capitan (su alcance) → al Capitan
</label>
</div>
<div id="sendContractsStatus" style="display:none;padding:12px;border-radius:8px;font-size:13px;margin-bottom:14px;"></div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button class="btn btn-sm" onclick="closeModal('sendContractsModal')">Cancelar</button>
<button class="btn btn-sm btn-success" id="sendContractsBtn" onclick="sendContracts()">Enviar Emails</button>
</div>
</div>
</div>
<!-- Modal: Solicitar Rider de Seguro -->
<div id="requestInsuranceModal" class="modal">
<div class="modal-content" style="max-width:460px;">
<div class="modal-header">
<h3>Solicitar Rider de Seguro</h3>
<span class="close" onclick="closeModal('requestInsuranceModal')">&times;</span>
</div>
<div class="form-group">
<label>Aseguradora</label>
<select id="insurerSelect" onchange="updateInsurerEmail()">
<option value="">-- Seleccione --</option>
<option value="Markel Marine|underwriting@markel.com">Markel Marine</option>
<option value="BoatUS Marine|marine@boatus.com">BoatUS / GEICO Marine</option>
<option value="Progressive Marine|marine@progressive.com">Progressive Marine</option>
<option value="Pantaenius|info@pantaenius.com">Pantaenius</option>
<option value="custom|">Otra (ingresar email manualmente)</option>
</select>
</div>
<div class="form-group">
<label>Email de la Aseguradora *</label>
<input type="email" id="insurerEmail" placeholder="agente@aseguradora.com">
</div>
<p style="font-size:12px;color:#888;margin:0 0 14px;">
Se enviara un email con datos completos del bote, arrendatario y periodo exacto de cobertura (por horas), adjuntando el formulario de solicitud de rider en PDF.
</p>
<div id="insuranceRequestStatus" style="display:none;padding:10px;border-radius:6px;font-size:13px;margin-bottom:12px;"></div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button class="btn btn-sm" onclick="closeModal('requestInsuranceModal')">Cancelar</button>
<button class="btn btn-sm btn-gold" onclick="sendInsuranceRequest()">Enviar Solicitud</button>
</div>
</div>
</div>
<!-- Modal: Notificacion -->
<div id="notifyModal" class="modal notify-modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="notifyTitle">Notificar al Dueno</h3>
<span class="close" onclick="closeModal('notifyModal')">&times;</span>
</div>
<div id="notifyPriorityBanner" style="display:none;margin-bottom:15px;padding:10px 14px;border-radius:6px;font-weight:600;font-size:13px;"></div>
<p style="font-size:13px;color:#666;margin-bottom:12px;">
Seleccione el canal para enviar la notificacion a <strong id="notifyOwnerName"></strong>:
</p>
<div class="notify-channels">
<a id="notifyWaBtn" href="#" target="_blank" class="btn-wa" onclick="markNotified()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>
WhatsApp
</a>
<a id="notifyEmailBtn" href="#" class="btn-email" onclick="markNotified()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 7l10 7 10-7"/></svg>
Enviar Email
</a>
</div>
<p style="font-size:12px;color:#888;margin-bottom:8px;">Vista previa del mensaje:</p>
<div class="msg-preview" id="notifyMsgPreview"></div>
<p style="font-size:11px;color:#aaa;margin-top:10px;">* WhatsApp abre la app con el mensaje listo para enviar. Email abre su cliente de correo.</p>
</div>
</div>
<!-- Modal: Nueva Work Order -->
<div id="workorderModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Nueva Work Order</h3>
<span class="close" onclick="closeModal('workorderModal')">&times;</span>
</div>
<form id="workorderForm">
<div class="form-group"><label>Embarcacion *</label><select id="woVesselId" required></select></div>
<div class="form-group"><label>Descripcion del trabajo *</label><textarea id="woDescription" placeholder="Ej: Cambio de aceite, revision de motores..." required></textarea></div>
<div class="form-grid">
<div class="form-group"><label>Costo Estimado ($)</label><input type="number" id="woCost" step="10" min="0"></div>
<div class="form-group"><label>Prioridad</label>
<select id="woPriority" onchange="updateWoPriorityHint()">
<option value="normal">Normal</option>
<option value="urgente">Urgente</option>
<option value="emergencia">Emergencia</option>
</select>
</div>
</div>
<div id="woPriorityHint" style="display:none;padding:10px 14px;border-radius:6px;font-size:13px;margin-bottom:12px;"></div>
<button type="submit" class="btn btn-gold">Crear Work Order</button>
</form>
</div>
</div>
<script>
// ============ NAVIGATION ============
let allVessels = [];
function showTab(tabId, btnEl) {
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
if (btnEl) btnEl.classList.add('active');
if (tabId === 'history') loadVesselSelect();
if (tabId === 'accounting') loadFuelEntries();
}
function goToLedger(vesselId) {
if (vesselId) {
window.location.href = `/admin/vessel/${vesselId}/accounting`;
} else {
// No vesselId: just switch to accounting tab
const btn = document.querySelector('.tab-btn[onclick*="accounting"]');
showTab('accounting', btn);
}
}
function showMessage(msg, type) {
let el = document.getElementById('message');
el.textContent = msg;
el.className = 'message-bar ' + type;
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 4000);
}
// ============ MODALS ============
function openModal(id) {
loadSelects();
document.getElementById(id).style.display = 'flex';
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
}
// Close modal on background click
document.querySelectorAll('.modal').forEach(m => {
m.addEventListener('click', (e) => {
if (e.target === m) m.style.display = 'none';
});
});
// ============ SELECTS ============
async function loadSelects() {
try {
let [vessels, owners, captains] = await Promise.all([
fetch('/api/vessels').then(r => r.json()),
fetch('/api/owners').then(r => r.json()),
fetch('/api/captains').then(r => r.json())
]);
allVessels = vessels;
const vesselOpts = '<option value="">-- Seleccione --</option>' + vessels.map(v => `<option value="${v.id}" data-rate="${v.base_rate_4h}" data-extra="${v.hourly_rate_extra}">${v.name}</option>`).join('');
const ownerOpts = '<option value="">-- Seleccione --</option>' + owners.map(o => `<option value="${o.id}">${o.name}</option>`).join('');
document.getElementById('charterVesselId').innerHTML = vesselOpts;
document.getElementById('woVesselId').innerHTML = vesselOpts;
document.getElementById('vesselOwnerId').innerHTML = ownerOpts;
document.getElementById('charterCaptainId').innerHTML =
'<option value="">-- Sin capitán asignado --</option>' +
captains.map(c => `<option value="${c.id}">${c.name} (${c.license_type || 'privado'})</option>`).join('');
} catch (e) { console.error(e); }
}
async function loadVesselSelect() {
if (allVessels.length === 0) {
allVessels = await fetch('/api/vessels').then(r => r.json());
}
document.getElementById('historyVesselSelect').innerHTML =
'<option value="">-- Seleccione un bote --</option>' +
allVessels.map(v => `<option value="${v.id}">${v.name} (${v.owner_name})</option>`).join('');
}
// ============ CHARTER TOTAL PREVIEW ============
function updateCharterTotal() {
const sel = document.getElementById('charterVesselId');
const opt = sel.options[sel.selectedIndex];
const hours = parseFloat(document.getElementById('charterHours').value) || 4;
if (!opt || !opt.value) {
document.getElementById('charterTotalPreview').style.display = 'none';
return;
}
const base = parseFloat(opt.dataset.rate) || 0;
const extra = parseFloat(opt.dataset.extra) || 0;
let total = base;
let breakdown = `Base 4h: $${base}`;
if (hours > 4) {
const extraCost = (hours - 4) * extra;
total += extraCost;
breakdown += ` + ${hours - 4}h extra x $${extra} = $${extraCost.toFixed(2)}`;
}
document.getElementById('charterTotalValue').textContent = total.toFixed(2);
document.getElementById('charterTotalBreakdown').textContent = breakdown;
document.getElementById('charterTotalPreview').style.display = 'block';
}
// ============ COMPLETE CHARTER ============
function openCompleteCharter(id, vesselName, chartererName, total) {
document.getElementById('completeCharterId').value = id;
document.getElementById('completeCharterInfo').innerHTML =
`<strong>${vesselName}</strong> &mdash; Cliente: ${chartererName} &mdash; Total: <strong>$${total}</strong>`;
updateVoucherPreview(total);
document.getElementById('completeModal').style.display = 'flex';
document.getElementById('completeTipPct').onchange = () => updateVoucherPreview(total);
}
function updateVoucherPreview(total) {
const tipPct = parseFloat(document.getElementById('completeTipPct').value);
const tip = (total * tipPct / 100).toFixed(2);
document.getElementById('voucherPreview').innerHTML =
`<strong>Resumen del voucher:</strong><br>
Charter: $${total} &nbsp;|&nbsp;
Propina sugerida (${tipPct}%): $${tip}<br>
<small style="color:#888;">La propina la paga el cliente directamente al capitan.</small>`;
}
document.getElementById('completeForm').onsubmit = async (e) => {
e.preventDefault();
const id = document.getElementById('completeCharterId').value;
const body = {
fuel_liters: parseFloat(document.getElementById('completeFuel').value) || 0,
deviation_charged: parseFloat(document.getElementById('completeDeviation').value) || 0,
tip_percentage: parseFloat(document.getElementById('completeTipPct').value)
};
const res = await fetch(`/api/charters/${id}/complete`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
const data = await res.json();
if (data.success) {
closeModal('completeModal');
loadData();
showMessage('Charter completado. Voucher #' + data.voucher_id + ' generado.', 'success');
} else {
showMessage('Error al completar el charter', 'error');
}
};
// ============ HISTORY ============
async function loadHistory() {
const id = document.getElementById('historyVesselSelect').value;
if (!id) { document.getElementById('historyContent').innerHTML = ''; return; }
try {
const data = await fetch(`/api/vessels/${id}/history`).then(r => r.json());
let html = `<h4 style="color:#0a2a3a;margin:15px 0 10px;">${data.vessel_name}</h4>`;
if (data.charters && data.charters.length) {
html += `<div class="section-title">Charters (${data.charters.length})</div>`;
data.charters.forEach(ch => {
html += `<div class="history-item">
<strong>${ch.start_datetime}</strong> &mdash; Cliente: ${ch.charterer_name} ${ch.charterer_phone ? '(' + ch.charterer_phone + ')' : ''}
<br>Horas: ${ch.hours} &nbsp;|&nbsp; Total: $${ch.total_base_rate} &nbsp;|&nbsp; Owner recibe: $${ch.owner_earnings || 0}
<br>Estado: <span class="badge badge-${ch.status}">${ch.status}</span>
</div>`;
});
} else {
html += `<p style="color:#999;padding:10px 0;">Sin charters registrados.</p>`;
}
if (data.workorders && data.workorders.length) {
html += `<div class="section-title">Mantenimiento (${data.workorders.length})</div>`;
data.workorders.forEach(wo => {
html += `<div class="history-item">
<strong>${wo.created_at}</strong> &mdash; ${wo.description}
<br>Estimado: $${wo.estimated_cost || 0} ${wo.actual_cost ? '| Real: $' + wo.actual_cost : ''}
<br>Estado: <span class="badge badge-${wo.status}">${wo.status}</span>
</div>`;
});
} else {
html += `<p style="color:#999;padding:10px 0;">Sin work orders registradas.</p>`;
}
document.getElementById('historyContent').innerHTML = html;
} catch (e) { console.error(e); }
}
function goToHistory(vesselId) {
const btn = document.querySelector('.tab-btn[onclick*="history"]');
showTab('history', btn);
loadVesselSelect().then(() => {
document.getElementById('historyVesselSelect').value = vesselId;
loadHistory();
});
}
function programarCharter(vesselId, vesselName) {
const btn = document.querySelector('.tab-btn[onclick*="charters"]');
showTab('charters', btn);
openModal('charterModal');
setTimeout(() => {
document.getElementById('charterModalTitle').textContent = 'Nuevo Charter - ' + vesselName;
document.getElementById('charterVesselId').value = vesselId;
updateCharterTotal();
}, 300);
}
// ============ OWNERS WITH VESSELS ============
async function loadOwnersWithVessels() {
try {
const [owners, vessels] = await Promise.all([
fetch('/api/owners').then(r => r.json()),
fetch('/api/vessels').then(r => r.json())
]);
allVessels = vessels;
let html = '';
for (let owner of owners) {
const ownerVessels = vessels.filter(v => v.owner_id === owner.id);
html += `<div class="owner-card">
<div class="owner-header" onclick="toggleVessels(${owner.id})">
<span>&#128100; ${owner.name}</span>
<span style="font-size:13px;">${ownerVessels.length} embarcacion${ownerVessels.length !== 1 ? 'es' : ''} &#9660;</span>
</div>
<div class="owner-meta">
<span>&#128231; ${owner.email || 'Sin email'}</span>
<span>&#128222; ${owner.phone || 'Sin telefono'}</span>
</div>
<div class="owner-vessels" id="vessels-${owner.id}">`;
if (ownerVessels.length === 0) {
html += `<p style="color:#999;padding:10px 0;font-size:13px;">Sin embarcaciones registradas.</p>`;
} else {
for (let v of ownerVessels) {
html += `<div class="vessel-detail">
<div class="vessel-name">&#128676; ${v.name}</div>
<div class="vessel-specs">
<div class="spec-item"><span class="spec-label">Marca</span><span class="spec-value">${v.make || 'N/A'}</span></div>
<div class="spec-item"><span class="spec-label">Modelo</span><span class="spec-value">${v.model || 'N/A'}</span></div>
<div class="spec-item"><span class="spec-label">Motores</span><span class="spec-value">${v.engines || 'N/A'}</span></div>
<div class="spec-item"><span class="spec-label">Eslora</span><span class="spec-value">${v.length || 'N/A'} ft</span></div>
<div class="spec-item"><span class="spec-label">Consumo 14kn</span><span class="spec-value">${v.fuel_consumption || 'N/A'} Gal/h</span></div>
<div class="spec-item"><span class="spec-label">Tarifa 4h</span><span class="spec-value">$${v.base_rate_4h || 'N/A'}</span></div>
<div class="spec-item"><span class="spec-label">Hora extra</span><span class="spec-value">$${v.hourly_rate_extra || 'N/A'}</span></div>
<div class="spec-item"><span class="spec-label">Max. Personas</span><span class="spec-value" style="font-weight:bold;color:#e74c3c;">${v.max_passengers || 12} personas (incl. capitan)</span></div>
<div class="spec-item"><span class="spec-label">Plan</span><span class="spec-value">${v.plan_name || 'N/A'}</span></div>
</div>
<div>
<button class="btn btn-sm" onclick="goToHistory(${v.id})">Historial</button>
<button class="btn btn-sm btn-gold" onclick="programarCharter(${v.id}, '${v.name.replace(/'/g, "\\'")}')">Programar Charter</button>
<a class="btn btn-sm btn-success" href="/admin/vessel/${v.id}/accounting">Contabilidad</a>
</div>
</div>`;
}
}
html += `</div></div>`;
}
if (!html) html = '<p style="color:#999;">No hay duenos registrados. Haga clic en "+ Nuevo Dueno" para comenzar.</p>';
document.getElementById('ownersWithVessels').innerHTML = html;
} catch (e) { console.error('loadOwnersWithVessels', e); }
}
function toggleVessels(ownerId) {
document.getElementById(`vessels-${ownerId}`).classList.toggle('show');
}
// ============ LOAD ALL DATA ============
async function loadData() {
try {
const [stats, captains, charters, workorders, pnl] = await Promise.all([
fetch('/api/stats').then(r => r.json()),
fetch('/api/captains').then(r => r.json()),
fetch('/api/charters').then(r => r.json()),
fetch('/api/workorders').then(r => r.json()),
fetch('/api/accounting/pnl').then(r => r.json()).catch(() => [])
]);
// Stats
document.getElementById('statOwners').textContent = stats.owners || 0;
document.getElementById('statVessels').textContent = stats.vessels || 0;
document.getElementById('statCaptains').textContent = stats.captains || 0;
document.getElementById('statCharters').textContent = stats.charters || 0;
document.getElementById('statRevenue').textContent = '$' + (stats.revenue || 0).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0});
// Captains
document.getElementById('captainsList').innerHTML = captains.length
? captains.map(c => `<tr><td>${c.name}</td><td>${c.phone || '-'}</td><td>${c.license_number || '-'}</td><td>$${c.hourly_rate}/h</td></tr>`).join('')
: '<tr><td colspan="4" style="color:#999;text-align:center;">Sin capitanes registrados</td></tr>';
// Charters
const statusBadge = s => `<span class="badge badge-${s}">${s}</span>`;
document.getElementById('chartersList').innerHTML = charters.length
? charters.map(ch => `<tr>
<td>${ch.start_datetime}</td>
<td>${ch.vessel_name}</td>
<td>${ch.owner_name}</td>
<td>${ch.charterer_name}<br><small style="color:#999;">${ch.charterer_phone}</small></td>
<td>${ch.captain_name || '—'}</td>
<td>${ch.hours}h</td>
<td>$${ch.total_base_rate}</td>
<td>
${statusBadge(ch.status)}
${(ch.status === 'draft' || ch.status === 'signed')
? ch.insurance_rider_number
? `<span style="background:#27ae60;color:white;padding:2px 7px;border-radius:10px;font-size:10px;font-weight:bold;display:inline-block;margin-top:3px;">✅ Poliza OK</span>`
: `<span style="background:#e74c3c;color:white;padding:2px 7px;border-radius:10px;font-size:10px;font-weight:bold;display:inline-block;margin-top:3px;">🔴 SIN POLIZA</span>`
: ''}
</td>
<td>
<button class="btn-notify" onclick="openNotify(${ch.id},'charter')" title="Notificar dueno del bote">📲</button>
${ch.status !== 'completed' && ch.status !== 'paid'
? ch.insurance_rider_number
? `<button class="btn btn-sm btn-success" onclick="openCompleteCharter(${ch.id},'${ch.vessel_name.replace(/'/g,"\\'")}','${ch.charterer_name.replace(/'/g,"\\'")}',${ch.total_base_rate})">Completar</button>`
: `<button class="btn btn-sm" style="background:#bbb;color:white;cursor:not-allowed;font-size:11px;" disabled title="Requiere poliza de seguro">🔒 Sin Poliza</button>`
: ''}
<a href="/api/charters/${ch.id}/contract" target="_blank" class="btn btn-sm" style="font-size:10px;padding:3px 7px;" title="Contrato Charter">📄</a>
<a href="/api/charters/${ch.id}/captain-contract" target="_blank" class="btn btn-sm" style="font-size:10px;padding:3px 7px;background:#27ae60;color:white;" title="Contrato Capitan">👤</a>
<a href="/api/charters/${ch.id}/insurance-rider" target="_blank" class="btn btn-sm" style="font-size:10px;padding:3px 7px;background:#8e44ad;color:white;" title="Rider Seguro">${ch.insurance_rider_number ? '' : '🚨'}🛡️</a>
<button class="btn btn-sm" style="background:#2980b9;color:white;font-size:10px;padding:3px 7px;" onclick="openSendContracts(${ch.id},'${ch.charterer_name.replace(/'/g,"\\'")}')">✉️</button>
<button class="btn btn-sm" style="background:#e67e22;color:white;font-size:10px;padding:3px 7px;" onclick="openRequestInsurance(${ch.id})">📋</button>
</td>
</tr>`).join('')
: '<tr><td colspan="9" style="color:#999;text-align:center;">Sin charters registrados</td></tr>';
// Work Orders
const woStatusBadge = s => `<span class="badge badge-${s}">${s}</span>`;
const priBadge = p => {
if (p === 'emergencia') return '<span class="badge badge-emergencia">🚨 EMERGENCIA</span>';
if (p === 'urgente') return '<span class="badge badge-urgente">⚠️ Urgente</span>';
return '<span class="badge badge-normal">Normal</span>';
};
document.getElementById('workordersList').innerHTML = workorders.length
? workorders.map(wo => `<tr class="${wo.priority === 'emergencia' ? 'priority-emergency-row' : wo.priority === 'urgente' ? 'priority-urgent-row' : ''}">
<td>${wo.created_at}</td>
<td><strong>${wo.vessel_name}</strong></td>
<td>${wo.owner_name}<br><small style="color:#999;">${wo.owner_phone || ''}</small></td>
<td>${wo.description}</td>
<td>$${wo.estimated_cost || 0}</td>
<td>${priBadge(wo.priority)}</td>
<td>${woStatusBadge(wo.status)}</td>
<td>
${wo.status === 'pending' || wo.status === 'approved' ? `
<button class="btn-notify" onclick="openNotify(${wo.id},'wo')" title="Notificar al dueno">
${wo.notified_at ? '✓ Notificado' : '📲 Notificar'}
</button>` : ''}
${wo.status === 'pending' ? `<button class="btn btn-sm btn-success" onclick="approveWO(${wo.id})">Aprobar</button>` : ''}
${wo.status === 'pending' ? `<button class="btn btn-sm btn-danger" onclick="rejectWO(${wo.id})">Rechazar</button>` : ''}
${wo.status === 'approved' ? `<button class="btn btn-sm btn-warning" onclick="doneWO(${wo.id})">Marcar Hecho</button>` : ''}
</td>
</tr>`).join('')
: '<tr><td colspan="8" style="color:#999;text-align:center;">Sin work orders</td></tr>';
// P&L
let rev = 0, exp = 0;
pnl.forEach(p => { rev += p.revenue; exp += p.expenses; });
document.getElementById('accRevenue').textContent = '$' + rev.toLocaleString();
document.getElementById('accExpenses').textContent = '$' + exp.toLocaleString();
document.getElementById('accProfit').textContent = '$' + (rev - exp).toLocaleString();
document.getElementById('pnlBody').innerHTML = pnl.length
? pnl.map(p => `<tr>
<td>${p.vessel_name}</td>
<td>${p.owner_name}</td>
<td style="color:#27ae60;">$${(p.charter_revenue||0).toLocaleString()}</td>
<td style="color:#27ae60;">$${(p.plan_revenue||0).toLocaleString()}</td>
<td style="color:#e74c3c;">$${(p.expenses||0).toLocaleString()}</td>
<td style="color:#e74c3c;">$${(p.fuel_cost||0).toLocaleString()}</td>
<td style="font-weight:bold;color:${(p.profit||0) >= 0 ? '#27ae60' : '#e74c3c'}">$${(p.profit||0).toLocaleString()}</td>
<td><a href="/admin/vessel/${p.vessel_id}/accounting" class="btn btn-sm" style="font-size:11px;padding:3px 8px;text-decoration:none;">Diario</a></td>
</tr>`).join('')
: '<tr><td colspan="8" style="color:#999;text-align:center;">Sin datos — use "Sincronizar" para generar</td></tr>';
// Render vessel accounting cards
const cardContainer = document.getElementById('vesselAccountingCards');
if (pnl.length) {
cardContainer.innerHTML = pnl.map(p => {
const profit = p.profit || 0;
const borderColor = profit >= 0 ? '#27ae60' : '#e74c3c';
const profitColor = profit >= 0 ? '#27ae60' : '#e74c3c';
return `<div style="background:#fff;border-radius:10px;border:1px solid #e0e0e0;padding:18px;border-top:4px solid ${borderColor};display:flex;flex-direction:column;gap:12px;">
<div>
<div style="font-size:15px;font-weight:700;color:#0a2a3a;">&#128676; ${p.vessel_name}</div>
<div style="font-size:12px;color:#888;margin-top:2px;">Propietario: ${p.owner_name}</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;text-align:center;">
<div style="background:#f0faf4;border-radius:6px;padding:8px;">
<div style="font-size:9px;color:#666;text-transform:uppercase;letter-spacing:.5px;">Ingresos</div>
<div style="font-size:14px;font-weight:700;color:#27ae60;">$${(p.revenue||p.charter_revenue||0).toLocaleString()}</div>
</div>
<div style="background:#fdf5f5;border-radius:6px;padding:8px;">
<div style="font-size:9px;color:#666;text-transform:uppercase;letter-spacing:.5px;">Gastos</div>
<div style="font-size:14px;font-weight:700;color:#e74c3c;">$${(p.expenses||0).toLocaleString()}</div>
</div>
<div style="background:#f7f9fa;border-radius:6px;padding:8px;">
<div style="font-size:9px;color:#666;text-transform:uppercase;letter-spacing:.5px;">Utilidad</div>
<div style="font-size:14px;font-weight:700;color:${profitColor};">$${profit.toLocaleString()}</div>
</div>
</div>
<a href="/admin/vessel/${p.vessel_id}/accounting"
style="display:block;text-align:center;background:#0a2a3a;color:#c4a747;text-decoration:none;padding:9px 14px;border-radius:6px;font-weight:600;font-size:13px;letter-spacing:.3px;">
Ver Diario Contable &#8594;
</a>
</div>`;
}).join('');
} else if (allVessels.length) {
// Vessels exist but no P&L data yet
cardContainer.innerHTML = allVessels.map(v => `
<div style="background:#fff;border-radius:10px;border:1px solid #e0e0e0;padding:18px;border-top:4px solid #c4a747;display:flex;flex-direction:column;gap:12px;">
<div>
<div style="font-size:15px;font-weight:700;color:#0a2a3a;">&#128676; ${v.name}</div>
<div style="font-size:12px;color:#888;margin-top:2px;">${v.owner_name||''}</div>
</div>
<div style="font-size:12px;color:#999;background:#f8f9fa;padding:10px;border-radius:6px;">
Sin datos contables aun. Use "Sincronizar" para importar charters y work orders.
</div>
<a href="/admin/vessel/${v.id}/accounting"
style="display:block;text-align:center;background:#0a2a3a;color:#c4a747;text-decoration:none;padding:9px 14px;border-radius:6px;font-weight:600;font-size:13px;">
Ver Diario Contable &#8594;
</a>
</div>`).join('');
} else {
cardContainer.innerHTML = '<p style="color:#999;text-align:center;grid-column:1/-1;padding:30px;">No hay embarcaciones registradas.</p>';
}
// Populate entry vessel select
document.getElementById('entryVesselId').innerHTML =
'<option value="">-- Seleccione --</option>' +
allVessels.map(v => `<option value="${v.id}">${v.name}</option>`).join('');
// Load recent fuel entries
loadFuelEntries();
// Owners/Vessels section
loadOwnersWithVessels();
} catch (e) { console.error('loadData error', e); }
}
// ============ PRIORITY HINT ============
function updateWoPriorityHint() {
const val = document.getElementById('woPriority').value;
const hint = document.getElementById('woPriorityHint');
const hints = {
normal: { bg: '#e9ecef', color: '#495057', text: 'Normal: trabajo de mantenimiento rutinario. El bote puede seguir chárterando.' },
urgente: { bg: '#fff3cd', color: '#856404', text: '⚠️ Urgente: requiere atención pronta. El propietario debe aprobar lo antes posible.' },
emergencia: { bg: '#f8d7da', color: '#721c24', text: '🚨 Emergencia: el bote NO puede ir a charter por razones de seguridad hasta ser reparado.' }
};
if (val === 'normal') { hint.style.display = 'none'; return; }
const h = hints[val];
hint.style.cssText = `display:block; background:${h.bg}; color:${h.color}; padding:10px 14px; border-radius:6px; font-size:13px; margin-bottom:12px; font-weight:600;`;
hint.textContent = h.text;
}
// ============ NOTIFICATIONS ============
let currentNotifyId = null;
let currentNotifyType = null;
async function openNotify(id, type) {
currentNotifyId = id;
currentNotifyType = type;
try {
const url = type === 'wo'
? `/api/workorders/${id}/notify-message`
: `/api/charters/${id}/notify-message`;
const data = await fetch(url).then(r => r.json());
document.getElementById('notifyTitle').textContent =
type === 'wo' ? 'Notificar al Dueno — Work Order' : 'Notificar al Dueno — Charter';
document.getElementById('notifyOwnerName').textContent = data.owner_name;
document.getElementById('notifyMsgPreview').textContent = data.body;
// Priority banner
const banner = document.getElementById('notifyPriorityBanner');
if (data.priority === 'emergencia') {
banner.style.cssText = 'display:block; background:#f8d7da; color:#721c24; padding:10px 14px; border-radius:6px; font-weight:600; font-size:13px; margin-bottom:15px;';
banner.textContent = '🚨 EMERGENCIA — El bote NO puede ir a charter hasta que se apruebe y ejecute esta work order.';
} else if (data.priority === 'urgente') {
banner.style.cssText = 'display:block; background:#fff3cd; color:#856404; padding:10px 14px; border-radius:6px; font-weight:600; font-size:13px; margin-bottom:15px;';
banner.textContent = '⚠️ URGENTE — Se requiere aprobación inmediata del propietario.';
} else {
banner.style.display = 'none';
}
// WhatsApp link
const waPhone = data.wa_phone || '';
const waText = encodeURIComponent(data.body);
document.getElementById('notifyWaBtn').href =
waPhone ? `https://wa.me/${waPhone}?text=${waText}` : `https://wa.me/?text=${waText}`;
// Email link
const emailBody = encodeURIComponent(data.body);
const emailSubject = encodeURIComponent(data.subject);
document.getElementById('notifyEmailBtn').href =
`mailto:${data.owner_email}?subject=${emailSubject}&body=${emailBody}`;
document.getElementById('notifyModal').style.display = 'flex';
} catch(e) {
showMessage('Error al generar mensaje', 'error');
console.error(e);
}
}
async function markNotified() {
if (currentNotifyType === 'wo' && currentNotifyId) {
await fetch(`/api/workorders/${currentNotifyId}/mark-notified`, {method: 'POST'});
setTimeout(() => { loadData(); }, 800);
}
}
// ============ API ACTIONS ============
async function approveWO(id) {
await fetch(`/api/workorders/${id}/approve`, {method: 'POST'});
loadData();
showMessage('Work Order aprobada', 'success');
}
async function rejectWO(id) {
if (!confirm('Rechazar esta Work Order?')) return;
await fetch(`/api/workorders/${id}/reject`, {method: 'POST'});
loadData();
showMessage('Work Order rechazada', 'success');
}
async function doneWO(id) {
const cost = prompt('Costo real del trabajo ($):');
if (cost === null) return;
await fetch(`/api/workorders/${id}/done`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({actual_cost: parseFloat(cost) || 0})
});
loadData();
showMessage('Work Order marcada como completada', 'success');
}
// ============ FORM SUBMISSIONS ============
document.getElementById('ownerForm').onsubmit = async (e) => {
e.preventDefault();
await fetch('/api/owners', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: document.getElementById('ownerName').value,
email: document.getElementById('ownerEmail').value,
phone: document.getElementById('ownerPhone').value
})
});
closeModal('ownerModal');
document.getElementById('ownerForm').reset();
loadData();
showMessage('Dueno creado exitosamente', 'success');
};
document.getElementById('vesselForm').onsubmit = async (e) => {
e.preventDefault();
const id = document.getElementById('vesselId').value;
const payload = {
name: document.getElementById('vesselName').value,
make: document.getElementById('vesselMake').value,
model: document.getElementById('vesselModel').value,
engines: document.getElementById('vesselEngines').value,
owner_company_id: document.getElementById('vesselOwnerId').value,
length: document.getElementById('vesselLength').value || 0,
fuel_consumption: document.getElementById('vesselFuel').value || 0,
base_rate_4h: document.getElementById('vesselRate').value || 0,
hourly_rate_extra: document.getElementById('vesselExtraRate').value || 0,
max_passengers: document.getElementById('vesselMaxPax').value || 12,
charter_percentage: document.getElementById('vesselPct').value || 25,
plan_id: document.getElementById('vesselPlan').value
};
if (id) {
await fetch(`/api/vessels/${id}`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)});
} else {
await fetch('/api/vessels', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)});
}
closeModal('vesselModal');
document.getElementById('vesselForm').reset();
document.getElementById('vesselId').value = '';
document.getElementById('vesselModalTitle').textContent = 'Nueva Embarcacion';
loadData();
showMessage('Embarcacion guardada exitosamente', 'success');
};
document.getElementById('captainForm').onsubmit = async (e) => {
e.preventDefault();
await fetch('/api/captains', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: document.getElementById('captainName').value,
phone: document.getElementById('captainPhone').value,
license_number: document.getElementById('captainLicense').value,
hourly_rate: document.getElementById('captainRate').value || 0
})
});
closeModal('captainModal');
document.getElementById('captainForm').reset();
loadData();
showMessage('Capitan creado exitosamente', 'success');
};
document.getElementById('charterForm').onsubmit = async (e) => {
e.preventDefault();
const res = await fetch('/api/charters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vessel_id: document.getElementById('charterVesselId').value,
charterer_name: document.getElementById('chartererName').value,
charterer_email: document.getElementById('chartererEmail').value,
charterer_phone: document.getElementById('chartererPhone').value,
start_datetime: document.getElementById('charterStart').value,
hours: document.getElementById('charterHours').value,
captain_id: document.getElementById('charterCaptainId').value || null,
insurance_rider_number: document.getElementById('charterRiderNumber').value,
insurer_name: document.getElementById('charterInsurer').value,
coverage_amount: parseFloat(document.getElementById('charterCoverage').value) || null,
damage_waiver: parseFloat(document.getElementById('charterDamageWaiver').value) || 0
})
});
const data = await res.json();
if (data.success) {
closeModal('charterModal');
document.getElementById('charterForm').reset();
document.getElementById('charterModalTitle').textContent = 'Nuevo Charter';
document.getElementById('charterTotalPreview').style.display = 'none';
document.getElementById('charterCaptainId').value = '';
document.getElementById('charterRiderNumber').value = '';
document.getElementById('charterInsurer').value = '';
document.getElementById('charterCoverage').value = '';
document.getElementById('charterDamageWaiver').value = '0';
loadData();
showMessage(`Charter creado. Total: $${data.total}`, 'success');
} else {
showMessage('Error al crear el charter', 'error');
}
};
document.getElementById('workorderForm').onsubmit = async (e) => {
e.preventDefault();
const priority = document.getElementById('woPriority').value;
const res = await fetch('/api/workorders', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vessel_id: document.getElementById('woVesselId').value,
description: document.getElementById('woDescription').value,
estimated_cost: document.getElementById('woCost').value || 0,
priority: priority
})
});
const data = await res.json();
closeModal('workorderModal');
document.getElementById('workorderForm').reset();
document.getElementById('woPriorityHint').style.display = 'none';
loadData();
const msg = priority === 'emergencia'
? '🚨 Work Order de EMERGENCIA creada. Notifique al dueno de inmediato.'
: priority === 'urgente'
? '⚠️ Work Order URGENTE creada. Recuerde notificar al dueno.'
: 'Work Order creada. Pendiente de aprobacion del dueno.';
showMessage(msg, priority === 'emergencia' ? 'error' : 'success');
// Auto-abrir notificacion si es emergencia
if (priority === 'emergencia' && data.id) {
setTimeout(() => openNotify(data.id, 'wo'), 600);
}
};
// ============ ACCOUNTING ============
const CATEGORIES = {
income: ['charter','plan_subscription','other_income'],
expense: ['work_order','fuel','cleaning','detailing','teak','marina','insurance','storage','other_expense']
};
const CAT_LABELS = {
charter: 'Charter', plan_subscription: 'Suscripcion Plan',
other_income: 'Otro Ingreso', work_order: 'Work Order / Mant.',
fuel: 'Combustible', cleaning: 'Limpieza', detailing: 'Detailing',
teak: 'Limpieza Teca', marina: 'Marina / Muelle',
insurance: 'Seguro', storage: 'Almacenaje', other_expense: 'Otro Gasto'
};
function updateEntryCategories() {
const type = document.getElementById('entryType').value;
const sel = document.getElementById('entryCategory');
sel.innerHTML = CATEGORIES[type].map(c => `<option value="${c}">${CAT_LABELS[c]}</option>`).join('');
}
updateEntryCategories();
function calcFuelTotal() {
const l = parseFloat(document.getElementById('fuelLiters').value) || 0;
const p = parseFloat(document.getElementById('fuelPpl').value) || 0;
if (l > 0 && p > 0) document.getElementById('fuelTotal').value = (l * p).toFixed(2);
}
async function openFuelModal() {
await loadSelects();
// Populate fuel vessel select
const vessels = allVessels;
document.getElementById('fuelVesselId').innerHTML =
'<option value="">-- Seleccione --</option>' +
vessels.map(v => `<option value="${v.id}">${v.name}</option>`).join('');
// Populate charters select
const charters = await fetch('/api/charters').then(r => r.json());
document.getElementById('fuelCharterId').innerHTML =
'<option value="">-- Ninguno --</option>' +
charters.map(c => `<option value="${c.id}">${c.start_datetime} ${c.vessel_name} ${c.charterer_name}</option>`).join('');
// Default date to today
document.getElementById('fuelDate').value = new Date().toISOString().split('T')[0];
document.getElementById('fuelModal').style.display = 'flex';
}
// loadLedger() moved to /admin/vessel/<id>/accounting dedicated page
async function syncAllAccounting() {
const res = await fetch('/api/accounting/sync-all', {method: 'POST'});
const data = await res.json();
showMessage(`Sincronizacion completada: ${data.entries_created} asientos generados`, 'success');
loadData();
}
async function deleteEntry(id) {
if (!confirm('Eliminar este asiento contable?')) return;
await fetch(`/api/accounting/entries/${id}`, {method: 'DELETE'});
loadData();
}
async function loadFuelEntries() {
const data = await fetch('/api/fuel-entries').then(r => r.json());
document.getElementById('fuelBody').innerHTML = data.length
? data.map(f => `<tr>
<td>${f.date}</td>
<td>${f.vessel_name}</td>
<td>${f.liters ? f.liters + ' gal' : '-'}</td>
<td>${f.price_per_liter ? '$'+f.price_per_liter : '-'}</td>
<td style="font-weight:bold;">$${f.total_cost.toLocaleString()}</td>
<td>${f.supplier||'-'}</td>
<td style="font-size:12px;color:#666;">${f.invoice_number||'-'}</td>
<td>${f.charter_id ? 'CHR-'+String(f.charter_id).padStart(4,'0') : '-'}</td>
<td><button class="btn btn-sm btn-danger" style="padding:3px 7px;font-size:11px;" onclick="deleteFuel(${f.id})">✕</button></td>
</tr>`).join('')
: '<tr><td colspan="9" style="color:#999;text-align:center;">Sin registros de combustible</td></tr>';
}
async function deleteFuel(id) {
if (!confirm('Eliminar este registro de combustible?')) return;
await fetch(`/api/fuel-entries/${id}`, {method: 'DELETE'});
loadFuelEntries();
loadData();
}
// Form: asiento manual
document.getElementById('entryForm').onsubmit = async (e) => {
e.preventDefault();
await fetch('/api/accounting/entries', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vessel_id: document.getElementById('entryVesselId').value,
date: document.getElementById('entryDate').value,
entry_type: document.getElementById('entryType').value,
category: document.getElementById('entryCategory').value,
description: document.getElementById('entryDescription').value,
amount: document.getElementById('entryAmount').value,
invoice_number: document.getElementById('entryInvoice').value,
notes: document.getElementById('entryNotes').value
})
});
closeModal('entryModal');
document.getElementById('entryForm').reset();
updateEntryCategories();
loadData();
showMessage('Asiento contable guardado', 'success');
};
// Form: combustible
document.getElementById('fuelForm').onsubmit = async (e) => {
e.preventDefault();
await fetch('/api/fuel-entries', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vessel_id: document.getElementById('fuelVesselId').value,
date: document.getElementById('fuelDate').value,
liters: document.getElementById('fuelLiters').value || 0,
price_per_liter: document.getElementById('fuelPpl').value || 0,
total_cost: document.getElementById('fuelTotal').value,
supplier: document.getElementById('fuelSupplier').value,
invoice_number: document.getElementById('fuelInvoice').value,
charter_id: document.getElementById('fuelCharterId').value || null,
notes: document.getElementById('fuelNotes').value
})
});
closeModal('fuelModal');
document.getElementById('fuelForm').reset();
loadData();
showMessage('Registro de combustible guardado y asiento generado', 'success');
};
// ============ SEND CONTRACTS ============
let _sendCharterIdContracts = null;
function openSendContracts(charterId, chartererName) {
_sendCharterIdContracts = charterId;
document.getElementById('sendContractsInfo').innerHTML =
`<strong>Charter #${String(charterId).padStart(4,'0')}</strong> &mdash; Arrendatario: <strong>${chartererName}</strong>`;
document.getElementById('sendContractsStatus').style.display = 'none';
const btn = document.getElementById('sendContractsBtn');
btn.disabled = false;
btn.textContent = 'Enviar Emails';
document.getElementById('sendContractsModal').style.display = 'flex';
}
async function sendContracts() {
const btn = document.getElementById('sendContractsBtn');
btn.disabled = true;
btn.textContent = 'Enviando...';
const statusEl = document.getElementById('sendContractsStatus');
try {
const res = await fetch(`/api/charters/${_sendCharterIdContracts}/send-contracts`, {method:'POST'});
const data = await res.json();
if (data.success) {
const r = data.results;
const lines = [];
const label = (key, name) => {
if (r[key] === 'sent') lines.push(`&#9989; ${name}: enviado`);
else if (r[key] === 'failed') lines.push(`&#10060; ${name}: error al enviar`);
else if (r[key] === 'no_email') lines.push(`&#9888;&#65039; ${name}: sin email registrado`);
else if (r[key] === 'no_captain') lines.push(`&#8505;&#65039; Capitan: no asignado al charter`);
};
label('charterer', 'Arrendatario');
label('owner', 'Dueno del bote');
label('captain', 'Capitan');
statusEl.innerHTML = lines.join('<br>');
statusEl.style.cssText = 'display:block;background:#f0faf4;border:1px solid #c3e6cb;color:#155724;padding:12px;border-radius:8px;font-size:13px;margin-bottom:14px;';
} else {
statusEl.innerHTML = '&#10060; Error: ' + (data.error || 'Verifique configuracion SMTP en .env');
statusEl.style.cssText = 'display:block;background:#fdf5f5;border:1px solid #f5c6cb;color:#721c24;padding:12px;border-radius:8px;font-size:13px;margin-bottom:14px;';
}
} catch(e) {
statusEl.innerHTML = '&#10060; Error de conexion';
statusEl.style.cssText = 'display:block;background:#fdf5f5;color:#721c24;padding:12px;border-radius:8px;font-size:13px;margin-bottom:14px;';
}
btn.disabled = false;
btn.textContent = 'Enviar Emails';
}
// ============ REQUEST INSURANCE ============
let _requestInsuranceCharterId = null;
function openRequestInsurance(charterId) {
_requestInsuranceCharterId = charterId;
document.getElementById('insurerSelect').value = '';
document.getElementById('insurerEmail').value = '';
document.getElementById('insuranceRequestStatus').style.display = 'none';
document.getElementById('requestInsuranceModal').style.display = 'flex';
}
function updateInsurerEmail() {
const val = document.getElementById('insurerSelect').value;
if (!val || val.startsWith('custom')) {
document.getElementById('insurerEmail').value = '';
document.getElementById('insurerEmail').focus();
return;
}
document.getElementById('insurerEmail').value = val.split('|')[1] || '';
}
async function sendInsuranceRequest() {
const email = document.getElementById('insurerEmail').value.trim();
if (!email) { alert('Ingrese el email de la aseguradora'); return; }
const sel = document.getElementById('insurerSelect').value;
const name = (sel && !sel.startsWith('custom')) ? sel.split('|')[0] : 'Aseguradora';
const statusEl = document.getElementById('insuranceRequestStatus');
statusEl.innerHTML = 'Enviando solicitud...';
statusEl.style.cssText = 'display:block;background:#fff3cd;color:#856404;padding:10px;border-radius:6px;font-size:13px;';
try {
const res = await fetch(`/api/charters/${_requestInsuranceCharterId}/request-insurance`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({insurer_email: email, insurer_name: name})
});
const data = await res.json();
if (data.success) {
statusEl.innerHTML = `&#9989; Solicitud enviada a <strong>${name}</strong>. Espere el numero de rider por respuesta.`;
statusEl.style.cssText = 'display:block;background:#f0faf4;border:1px solid #c3e6cb;color:#155724;padding:10px;border-radius:6px;font-size:13px;';
setTimeout(() => loadData(), 1000);
} else {
statusEl.innerHTML = `&#10060; ${data.message || 'Error al enviar'}<br><small>Configure MAIL_USERNAME y MAIL_PASSWORD en el archivo .env</small>`;
statusEl.style.cssText = 'display:block;background:#fdf5f5;color:#721c24;padding:10px;border-radius:6px;font-size:13px;';
}
} catch(e) {
statusEl.innerHTML = '&#10060; Error de conexion';
statusEl.style.cssText = 'display:block;background:#fdf5f5;color:#721c24;padding:10px;border-radius:6px;font-size:13px;';
}
}
// ============ INIT ============
loadData();
</script>
</body>
</html>