Files
MarineMaintenance/templates/work_order_detail.html
T
alro65 67a0e674ca Initial commit — MarineMaintenance v1.0
Marine maintenance management: work orders with photos, ISM/SWP procedures,
MSDS, inventory, RFQ/purchases, vessel history, bilingual PDF reports.

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

1000 lines
49 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.
{% extends 'base.html' %}
{% block title %}{{ order.order_number }}{% endblock %}
{% block page_title %}{{ order.order_number }}{% endblock %}
{% block topbar_actions %}
<div class="topbar-actions-desktop">
{% if order.status != 'completed' %}
<a href="{{ url_for('work_order_edit', woid=order.id) }}" class="btn btn-secondary btn-sm">✏️ Editar</a>
<button onclick="setStatus('in_progress')" class="btn btn-warning btn-sm">▶ En Progreso</button>
<button onclick="setStatus('completed')" class="btn btn-success btn-sm">✅ Completar</button>
<button onclick="deleteThisWO()" class="btn btn-danger btn-sm">🗑️</button>
{% else %}
<button onclick="confirmReopen()" class="btn btn-warning btn-sm">🔓 Reabrir WO</button>
{% endif %}
<button onclick="openAssignModal()" class="btn btn-secondary btn-sm">👤 Asignar</button>
<a href="{{ url_for('work_order_pdf', woid=order.id) }}?lang=es" target="_blank" class="btn btn-primary btn-sm">📄 ES</a>
<a href="{{ url_for('work_order_pdf', woid=order.id) }}?lang=en" target="_blank" class="btn btn-secondary btn-sm">📄 EN</a>
<button onclick="openShareModal()" class="btn btn-secondary btn-sm">📤 Enviar</button>
<a href="{{ url_for('work_orders') }}" class="btn btn-secondary btn-sm">← Volver</a>
</div>
{% endblock %}
{% block content %}
<!-- BARRA DE ACCIONES MÓVIL -->
<style>
.mobile-action-bar { display:none; }
.topbar-actions-desktop { display:flex; gap:8px; flex-wrap:wrap; }
@media (max-width:768px) {
.topbar-actions-desktop { display:none !important; }
.mobile-action-bar {
display:flex;position:fixed;bottom:0;left:0;right:0;
background:var(--navy2);border-top:2px solid rgba(0,180,216,0.4);
padding:8px 10px;gap:6px;z-index:950;
}
.mobile-action-bar .mab-btn {
flex:1;text-align:center;font-size:11px;padding:8px 4px;
border-radius:8px;border:none;cursor:pointer;min-width:0;
display:flex;flex-direction:column;align-items:center;gap:2px;
font-weight:600;
}
.mobile-action-bar .mab-btn .icon { font-size:18px;line-height:1; }
.mobile-action-bar .mab-btn .label { font-size:10px;line-height:1; }
.content { padding-bottom:80px !important; }
}
</style>
<div class="mobile-action-bar">
{% if order.status != 'completed' %}
<button onclick="setStatus('in_progress')" class="mab-btn btn-warning"
style="background:rgba(244,162,97,0.2);color:var(--warning)">
<span class="icon"></span><span class="label">Progreso</span>
</button>
<button onclick="setStatus('completed')" class="mab-btn btn-success"
style="background:rgba(46,196,182,0.2);color:var(--success)">
<span class="icon"></span><span class="label">Completar</span>
</button>
<a href="{{ url_for('work_order_edit', woid=order.id) }}" class="mab-btn"
style="background:rgba(255,255,255,0.08);color:var(--white);text-decoration:none">
<span class="icon">✏️</span><span class="label">Editar</span>
</a>
{% else %}
<button onclick="confirmReopen()" class="mab-btn"
style="background:rgba(244,162,97,0.2);color:var(--warning)">
<span class="icon">🔓</span><span class="label">Reabrir</span>
</button>
{% endif %}
<button onclick="openShareModal()" class="mab-btn"
style="background:rgba(255,255,255,0.08);color:var(--white)">
<span class="icon">📤</span><span class="label">Enviar</span>
</button>
<a href="{{ url_for('work_order_pdf', woid=order.id) }}?lang=es" target="_blank"
class="mab-btn" style="background:rgba(0,180,216,0.2);color:var(--cyan);text-decoration:none">
<span class="icon">📄</span><span class="label">PDF</span>
</a>
<a href="{{ url_for('work_orders') }}" class="mab-btn"
style="background:rgba(255,255,255,0.08);color:var(--white);text-decoration:none">
<span class="icon"></span><span class="label">Volver</span>
</a>
</div>
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header">📋 Detalles</div>
<table style="font-size:13px">
<tr><td style="color:var(--gray);padding:6px 0;width:140px">Embarcación</td><td><a href="{{ url_for('vessel_history', vid=order.vessel_id) }}" class="text-cyan">{{ order.vessel_name }}</a></td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Sistema</td><td><span class="badge badge-open">{{ order.system_name or '—' }}</span></td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Scope</td><td><strong>{{ order.scope or '—' }}</strong></td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Tipo</td><td>{{ order.work_type or '—' }}</td></tr>
<tr>
<td style="color:var(--gray);padding:6px 0">Facturación</td>
<td>
{% if order.billing_type == 'lump_sum' %}
<span class="badge" style="background:rgba(244,162,97,0.15);color:var(--warning)">💰 A todo costo</span>
{% elif order.billing_type == 'labor_only' %}
<span class="badge" style="background:rgba(0,180,216,0.15);color:var(--cyan)">🔧 Solo M.O.</span>
{% else %}
<span class="badge" style="background:rgba(46,196,182,0.15);color:var(--success)">📋 M.O. + Materiales</span>
{% endif %}
</td>
</tr>
<tr><td style="color:var(--gray);padding:6px 0">Técnico</td><td>{{ order.technician or '—' }}</td></tr>
{% if order.assigned_to %}
<tr>
<td style="color:var(--gray);padding:6px 0">Asignado a</td>
<td>
<span style="color:var(--cyan);font-size:12px">📨 {{ order.assigned_to }}</span>
{% if order.assigned_by %}<br><span style="font-size:11px;color:var(--gray)">por {{ order.assigned_by }}</span>{% endif %}
</td>
</tr>
{% endif %}
<tr><td style="color:var(--gray);padding:6px 0">Fecha inicio</td><td>{{ order.start_date or '—' }}</td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Fecha fin</td><td>{{ order.end_date or '—' }}</td></tr>
<tr><td style="color:var(--gray);padding:6px 0">H. Motor inicio</td><td>{{ order.engine_hours_start or '—' }}</td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Estado</td><td><span class="badge badge-{{ order.status }}">{{ order.status.replace('_',' ') }}</span></td></tr>
</table>
<div style="margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.05);font-size:13px">
<div style="color:var(--gray);font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Descripción</div>
{{ order.description }}
</div>
</div>
<div class="card">
<div class="card-header">💰 Costos</div>
<table style="font-size:13px">
<tr><td style="color:var(--gray);padding:6px 0;width:160px">Horas Trabajadas</td><td>{{ order.labor_hours or 0 }} h</td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Tarifa M.O.</td><td>${{ order.labor_rate or 0 }}/h</td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Costo M.O.</td><td>${{ "%.2f"|format(order.total_labor_cost or 0) }}</td></tr>
<tr><td style="color:var(--gray);padding:6px 0">Costo Repuestos</td><td>${{ "%.2f"|format(order.total_parts_cost or 0) }}</td></tr>
<tr style="font-weight:600"><td style="color:var(--cyan);padding:8px 0">TOTAL</td><td class="text-cyan">${{ "%.2f"|format((order.total_labor_cost or 0) + (order.total_parts_cost or 0)) }}</td></tr>
</table>
<div class="mt-4" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<label style="font-size:12px;color:var(--gray)">Horas trabajadas:</label>
<input type="number" id="labor_hours" step="0.01" value="{{ order.labor_hours or 0 }}" style="width:80px;padding:6px">
<label style="font-size:12px;color:var(--gray)">Tarifa $/h:</label>
<input type="number" id="labor_rate" step="0.01" value="{{ order.labor_rate or 0 }}" style="width:80px;padding:6px">
<button onclick="updateHours()" class="btn btn-sm btn-primary">💾 Guardar Horas</button>
<span id="hoursStatus" style="font-size:12px;color:var(--success)"></span>
</div>
</div>
</div>
<!-- EQUIPOS TRABAJADOS EN ESTE WO -->
<div class="card mb-4">
<div class="card-header flex justify-between">
<span>⚙️ Equipos Trabajados</span>
{% if order.status != 'completed' %}
<button onclick="openEquipModal()" class="btn btn-sm btn-primary">+ Agregar Equipo</button>
{% endif %}
</div>
{% if wo_equipment %}
<div class="table-wrap">
<table>
<thead><tr><th>Equipo</th><th>S/N</th><th>Trabajo Realizado</th><th>Notas</th><th>Horas</th><th>Costo M.O.</th>{% if order.status != 'completed' %}<th></th>{% endif %}</tr></thead>
<tbody>
{% for e in wo_equipment %}
<tr>
<td><strong>{{ e.equip_name or '—' }}</strong><br><span class="text-gray" style="font-size:11px">{{ e.make or '' }} {{ e.model or '' }}</span></td>
<td class="text-gray" style="font-family:monospace;font-size:11px">{{ e.serial_number or '—' }}</td>
<td>{{ e.description or '—' }}</td>
<td class="text-gray" style="font-size:11px">{{ e.notes or '—' }}</td>
<td>{{ e.labor_hours or 0 }} h</td>
<td class="text-cyan">${{ "%.2f"|format((e.labor_hours or 0) * (e.labor_rate or 0)) }}</td>
{% if order.status != 'completed' %}
<td class="flex gap-2">
<button onclick="editWoEquip({{ e.id }}, '{{ e.equip_name or '' }}', {{ e.labor_hours or 0 }}, {{ e.labor_rate or 0 }}, '{{ e.description|replace("'","\\'")|replace('"','\\"') or '' }}', '{{ e.notes|replace("'","\\'")|replace('"','\\"') or '' }}')" class="btn btn-sm btn-secondary">✏️</button>
<button onclick="removeWoEquip({{ e.id }}, this.closest('tr'))" class="btn btn-sm btn-danger">🗑️</button>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr style="background:rgba(0,180,216,0.06);font-weight:600">
<td colspan="4" style="padding:10px 14px;color:var(--gray)">TOTAL MANO DE OBRA</td>
<td style="padding:10px 14px;color:var(--cyan)">{{ wo_equipment|sum(attribute='labor_hours') or 0 }} h</td>
<td style="padding:10px 14px;color:var(--cyan)">${{ "%.2f"|format(wo_equipment|sum(attribute='labor_cost') or 0) }}</td>
{% if order.status != 'completed' %}<td></td>{% endif %}
</tr>
</tfoot>
</table>
</div>
{% else %}
<p class="text-gray" style="font-size:13px">Sin equipos agregados.</p>
{% endif %}
</div>
<!-- MODAL AGREGAR/EDITAR EQUIPO WO -->
<div id="woEquipModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:1000;align-items:center;justify-content:center">
<div class="card" style="width:500px;max-width:95vw;position:relative">
<button onclick="closeEquipModal()"
style="position:absolute;top:10px;right:12px;background:none;border:none;
color:var(--gray);font-size:20px;cursor:pointer">×</button>
<div class="card-header" id="equipModalTitle">⚙️ Agregar Equipo a esta Orden</div>
<input type="hidden" id="woEquipEditId" value="">
<div class="form-group mb-3">
<label>Equipo de la Embarcación</label>
<select id="woEquipSelect">
<option value="">— Sin equipo registrado (manual) —</option>
{% for e in vessel_equipment %}
<option value="{{ e.id }}">{{ e.name }} {% if e.serial_number %}· S/N: {{ e.serial_number }}{% endif %}</option>
{% endfor %}
</select>
</div>
<div class="form-group mb-3">
<label>Trabajo Realizado en este Equipo *</label>
<textarea id="woEquipDesc" rows="3" placeholder="Ej: Cambio de filtros de aceite y combustible..."></textarea>
</div>
<div class="form-grid mb-3">
<div class="form-group">
<label>Horas Trabajadas</label>
<input type="number" id="woEquipHours" step="0.01" value="0" min="0">
</div>
<div class="form-group">
<label>Tarifa $/h</label>
<input type="number" id="woEquipRate" step="0.01" value="{{ order.labor_rate or 0 }}" min="0">
</div>
</div>
<div class="form-group mb-3">
<label>Notas adicionales</label>
<input type="text" id="woEquipNotes" placeholder="Observaciones, proximo servicio, etc.">
</div>
<div class="flex gap-3">
<button onclick="saveWoEquip()" class="btn btn-primary" id="btnSaveEquip">+ Agregar</button>
<button onclick="closeEquipModal()" class="btn btn-secondary">Cancelar</button>
</div>
<div id="woEquipStatus" style="margin-top:10px;font-size:13px"></div>
</div>
</div>
<!-- CAUSA TÉCNICA Y REPARACIONES -->
<div class="card mb-4">
<div class="card-header">⚠️ Causa Técnica y Reparaciones</div>
<div class="form-grid">
<div class="form-group full">
<label>Causa técnica de la falla</label>
<textarea id="root_cause" rows="3" placeholder="Describe la causa raíz del problema...">{{ order.root_cause or '' }}</textarea>
</div>
<div class="form-group full">
<label>Reparaciones realizadas</label>
<textarea id="repairs_done" rows="3" placeholder="Describe detalladamente las reparaciones efectuadas...">{{ order.repairs_done or '' }}</textarea>
</div>
</div>
<button onclick="saveTechFields()" class="btn btn-secondary btn-sm mt-4">💾 Guardar</button>
<span id="saveStatus" style="font-size:12px;color:var(--success);margin-left:10px"></span>
</div>
<!-- FOTOS -->
<div class="card mb-4">
<div class="card-header flex justify-between">
<span>📸 Evidencia Fotográfica</span>
{% if order.status != 'cancelled' %}
<button onclick="document.getElementById('photoModal').style.display='flex'" class="btn btn-sm btn-primary">+ Agregar Foto</button>
{% endif %}
</div>
{% set before_photos = photos|selectattr('photo_type','eq','before')|list %}
{% set after_photos = photos|selectattr('photo_type','eq','after')|list %}
{% if before_photos %}
<div style="margin-bottom:16px">
<div style="font-size:12px;color:var(--warn);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:10px;font-weight:600">⬅ Antes</div>
<div class="photo-grid">
{% for p in before_photos %}
<div class="photo-card before">
<img src="/static/uploads/photos/{{ p.filename }}" onclick="openPhoto(this.src)" style="cursor:pointer">
<div class="photo-label" style="display:flex;align-items:center;gap:4px">
<span style="font-size:11px;color:var(--gray)">Antes</span>
<input type="text" value="{{ p.caption or '' }}"
onchange="updateCaption({{ p.id }}, this.value)"
placeholder="Agregar descripción..."
style="flex:1;font-size:11px;background:transparent;border:none;
border-bottom:1px dashed rgba(255,255,255,0.2);color:var(--white);
padding:2px 4px;outline:none;min-width:0">
</div>
<button class="photo-del" onclick="deletePhoto({{ p.id }}, this.closest('.photo-card'))">×</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if after_photos %}
<div>
<div style="font-size:12px;color:var(--success);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:10px;font-weight:600">➡ Después</div>
<div class="photo-grid">
{% for p in after_photos %}
<div class="photo-card after">
<img src="/static/uploads/photos/{{ p.filename }}" onclick="openPhoto(this.src)" style="cursor:pointer">
<div class="photo-label" style="display:flex;align-items:center;gap:4px">
<span style="font-size:11px;color:var(--gray)">Después</span>
<input type="text" value="{{ p.caption or '' }}"
onchange="updateCaption({{ p.id }}, this.value)"
placeholder="Agregar descripción..."
style="flex:1;font-size:11px;background:transparent;border:none;
border-bottom:1px dashed rgba(255,255,255,0.2);color:var(--white);
padding:2px 4px;outline:none;min-width:0">
</div>
<button class="photo-del" onclick="deletePhoto({{ p.id }}, this.closest('.photo-card'))">×</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if not photos %}
<p class="text-gray" style="font-size:13px">Sin fotos aún. Agrega evidencia fotográfica del trabajo.</p>
{% endif %}
</div>
<!-- REPUESTOS USADOS -->
<div class="card">
<div class="card-header flex justify-between">
<span>🔩 Repuestos Utilizados</span>
{% if order.status != 'cancelled' %}
<button onclick="document.getElementById('partModal').style.display='flex'" class="btn btn-sm btn-primary">+ Agregar Repuesto</button>
{% endif %}
</div>
{% if parts_used %}
<div class="table-wrap">
<table>
<thead><tr><th>Repuesto</th><th>Descripción</th><th>Cantidad</th><th>Precio Unit.</th><th>Total</th></tr></thead>
<tbody>
{% for p in parts_used %}
<tr>
<td>{{ p.part_name or '—' }}{% if p.part_number %} <span class="text-gray">({{ p.part_number }})</span>{% endif %}</td>
<td class="text-gray">{{ p.description or '' }}</td>
<td>{{ p.quantity }}</td>
<td>${{ "%.2f"|format(p.unit_cost) }}</td>
<td class="text-cyan">${{ "%.2f"|format(p.total_cost) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray" style="font-size:13px">Sin repuestos registrados.</p>
{% endif %}
</div>
<!-- MODAL FOTO -->
<div id="photoModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:1000;align-items:center;justify-content:center">
<div class="card" style="width:420px;max-width:95vw">
<div class="card-header">📸 Agregar Foto</div>
<div class="form-group mb-4">
<label>Tipo</label>
<select id="photoType">
<option value="before">Antes</option>
<option value="after">Después</option>
</select>
</div>
<div class="form-group mb-4">
<label>Foto (cámara o galería)</label>
<input type="file" id="photoFile" accept="image/*" capture="environment" style="padding:8px">
</div>
<div class="form-group mb-4">
<label>Descripción (opcional)</label>
<input type="text" id="photoCaption" placeholder="Ej: Válvula hidráulica antes de cambio">
</div>
<div class="flex gap-3">
<button onclick="uploadPhoto()" class="btn btn-primary">📤 Subir</button>
<button onclick="document.getElementById('photoModal').style.display='none'" class="btn btn-secondary">Cancelar</button>
</div>
<div id="uploadStatus" style="margin-top:10px;font-size:13px"></div>
</div>
</div>
<!-- MODAL REPUESTO -->
<div id="partModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:1000;align-items:center;justify-content:center">
<div class="card" style="width:460px;max-width:95vw">
<div class="card-header">🔩 Agregar Repuesto</div>
<div class="form-group mb-4">
<label>Repuesto del Inventario</label>
<select id="partSelect" onchange="fillPartInfo()">
<option value="">-- Sin inventario (manual) --</option>
{% for p in available_parts %}
<option value="{{ p.id }}" data-price="{{ p.sale_price }}" data-stock="{{ p.quantity }}">
{{ p.name }} {% if p.part_number %}({{ p.part_number }}){% endif %} — Stock: {{ p.quantity }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group mb-4">
<label>Descripción (si es manual)</label>
<input type="text" id="partDesc" placeholder="Descripción del material">
</div>
<div class="form-grid mb-4">
<div class="form-group">
<label>Cantidad</label>
<input type="number" id="partQty" value="1" min="0.01" step="0.01">
</div>
<div class="form-group">
<label>Precio Unitario ($)</label>
<input type="number" id="partCost" value="0" step="0.01">
</div>
</div>
<div class="flex gap-3">
<button onclick="addPart()" class="btn btn-primary"> Agregar</button>
<button onclick="document.getElementById('partModal').style.display='none'" class="btn btn-secondary">Cancelar</button>
</div>
</div>
</div>
<!-- EMAIL LOG -->
{% if email_log %}
<div class="card mb-4">
<div class="card-header">📧 Historial de Envíos</div>
<div class="table-wrap">
<table>
<thead><tr><th>Fecha</th><th>Destinatario</th><th>Idioma</th><th>Estado</th><th>Enviado por</th><th>PDF</th></tr></thead>
<tbody>
{% for e in email_log %}
<tr>
<td class="text-gray" style="font-size:12px;white-space:nowrap">{{ e.sent_at[:16] }}</td>
<td>
<div style="font-weight:600;font-size:13px">{{ e.to_name or '—' }}</div>
<div style="font-size:11px;color:var(--gray)">{{ e.to_email }}</div>
</td>
<td><span class="badge badge-open" style="font-size:10px">{{ e.lang.upper() }}</span></td>
<td>
{% if e.status == 'sent' %}
<span class="badge badge-completed">✅ Enviado</span>
{% else %}
<span class="badge badge-danger" style="cursor:help"
title="{{ e.error_msg or 'Error desconocido' }}">❌ Fallido</span>
{% if e.error_msg %}
<div style="font-size:10px;color:var(--danger);max-width:200px;margin-top:2px">
{{ e.error_msg[:80] }}{% if e.error_msg|length > 80 %}...{% endif %}
</div>
{% endif %}
{% endif %}
</td>
<td class="text-gray" style="font-size:12px">{{ e.sent_by or '—' }}</td>
<td>
{% if e.pdf_filename %}
<a href="/static/uploads/pdfs/{{ e.pdf_filename }}" target="_blank" class="btn btn-sm btn-secondary">📄</a>
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- FIRMAS -->
<div class="card mt-4">
<div class="card-header">✍️ Firmas</div>
<div class="grid-2">
<!-- Firma Técnico -->
<div>
<div style="font-size:11px;color:var(--gray);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">
Técnico: {{ order.technician or '—' }}
</div>
{% if order.signature_tech %}
<img src="/static/uploads/signatures/{{ order.signature_tech }}"
style="background:white;border-radius:8px;border:1px solid rgba(0,180,216,0.2);max-width:100%;height:120px;object-fit:contain;display:block">
<button onclick="clearSignature('tech')" class="btn btn-sm btn-secondary" style="margin-top:8px">🗑 Borrar</button>
{% else %}
<canvas id="sigTech" width="340" height="120"
style="background:white;border-radius:8px;border:2px solid rgba(0,180,216,0.3);
touch-action:none;cursor:crosshair;display:block;max-width:100%"></canvas>
<div class="flex gap-2" style="margin-top:8px">
<button onclick="saveSignature('tech')" class="btn btn-sm btn-primary">💾 Guardar</button>
<button onclick="clearCanvas('sigTech')" class="btn btn-sm btn-secondary">🗑 Limpiar</button>
</div>
{% endif %}
</div>
<!-- Firma Cliente/Capitán -->
<div>
<div style="font-size:11px;color:var(--gray);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">
Capitán / Cliente
</div>
{% if order.signature_client %}
<img src="/static/uploads/signatures/{{ order.signature_client }}"
style="background:white;border-radius:8px;border:1px solid rgba(0,180,216,0.2);max-width:100%;height:120px;object-fit:contain;display:block">
<button onclick="clearSignature('client')" class="btn btn-sm btn-secondary" style="margin-top:8px">🗑 Borrar</button>
{% else %}
<canvas id="sigClient" width="340" height="120"
style="background:white;border-radius:8px;border:2px solid rgba(0,180,216,0.3);
touch-action:none;cursor:crosshair;display:block;max-width:100%"></canvas>
<div class="flex gap-2" style="margin-top:8px">
<button onclick="saveSignature('client')" class="btn btn-sm btn-primary">💾 Guardar</button>
<button onclick="clearCanvas('sigClient')" class="btn btn-sm btn-secondary">🗑 Limpiar</button>
</div>
{% endif %}
</div>
</div>
</div>
<!-- SHARE MODAL -->
<div id="shareModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:1000;align-items:center;justify-content:center">
<div class="card" style="width:480px;max-width:95vw">
<div class="card-header">📤 Enviar Reporte</div>
<!-- Tabs -->
<div style="display:flex;gap:4px;margin-bottom:16px;background:rgba(0,0,0,0.2);border-radius:8px;padding:4px">
<button onclick="showTab('email')" id="tabEmail" class="btn btn-sm btn-primary" style="flex:1">✉️ Email</button>
<button onclick="showTab('share')" id="tabShare" class="btn btn-sm btn-secondary" style="flex:1">📱 WhatsApp / SMS</button>
</div>
<!-- EMAIL TAB -->
<div id="tabEmailContent">
<div class="form-group mb-3">
<label>Para (nombre)</label>
<input type="text" id="sendToName" value="{{ vessel_captain_name or vessel_owner_name or '' }}"
placeholder="Nombre del destinatario">
</div>
<div class="form-group mb-3">
<label>Email *</label>
<input type="email" id="sendToEmail"
value="{{ vessel_captain_email or vessel_owner_email or '' }}"
placeholder="email@ejemplo.com">
{% if vessel_captain_email or vessel_owner_email %}
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">
{% if vessel_captain_email %}
<button onclick="document.getElementById('sendToEmail').value='{{ vessel_captain_email }}';document.getElementById('sendToName').value='{{ vessel_captain_name }}'"
class="btn btn-sm btn-secondary" style="font-size:11px">⚓ {{ vessel_captain_name or 'Capitán' }}</button>
{% endif %}
{% if vessel_owner_email %}
<button onclick="document.getElementById('sendToEmail').value='{{ vessel_owner_email }}';document.getElementById('sendToName').value='{{ vessel_owner_name }}'"
class="btn btn-sm btn-secondary" style="font-size:11px">👤 {{ vessel_owner_name or 'Propietario' }}</button>
{% endif %}
</div>
{% endif %}
</div>
<div class="form-group mb-3">
<label>Idioma del Reporte</label>
<select id="sendLang" style="width:100%;padding:8px;border-radius:6px;background:rgba(255,255,255,0.06);border:1px solid rgba(0,180,216,0.2);color:var(--white)">
<option value="es">Español</option>
<option value="en">English</option>
</select>
</div>
<div class="form-group mb-4">
<label>Asunto</label>
<input type="text" id="sendSubject"
value="Reporte de Mantenimiento — {{ order.order_number }}">
</div>
<div class="flex gap-3">
<button onclick="sendEmail()" class="btn btn-primary">📧 Enviar Email</button>
<button onclick="closeShareModal()" class="btn btn-secondary">Cancelar</button>
</div>
<div id="emailStatus" style="margin-top:10px;font-size:13px"></div>
</div>
<!-- SHARE TAB -->
<div id="tabShareContent" style="display:none">
<p style="font-size:13px;color:var(--gray);margin-bottom:16px">
Primero guardamos el PDF en el servidor para generar un link compartible.
</p>
<div id="shareLinks" style="display:none">
<div class="form-group mb-3">
<label>Link del PDF</label>
<input type="text" id="pdfLink" readonly
style="font-size:12px;cursor:pointer"
onclick="this.select();document.execCommand('copy')">
<div style="font-size:11px;color:var(--gray);margin-top:3px">Toca para copiar</div>
</div>
<div class="flex gap-3" style="flex-wrap:wrap">
<a id="waLink" href="#" target="_blank"
style="background:#25D366;color:white;padding:10px 20px;border-radius:8px;
text-decoration:none;font-weight:600;font-size:14px;display:flex;align-items:center;gap:8px">
<span style="font-size:20px">💬</span> WhatsApp
</a>
<a id="smsLink" href="#"
style="background:#0a84ff;color:white;padding:10px 20px;border-radius:8px;
text-decoration:none;font-weight:600;font-size:14px;display:flex;align-items:center;gap:8px">
<span style="font-size:20px">💬</span> SMS
</a>
</div>
</div>
<div id="shareLoading" style="font-size:13px;color:var(--gray)"></div>
<div class="flex gap-3 mt-4">
<button onclick="generateShareLinks()" class="btn btn-primary">🔗 Generar Links</button>
<button onclick="closeShareModal()" class="btn btn-secondary">Cancelar</button>
</div>
</div>
</div>
</div>
<!-- LIGHTBOX -->
<div id="lightbox" onclick="this.style.display='none'" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.95);z-index:2000;align-items:center;justify-content:center;cursor:zoom-out">
<img id="lightboxImg" style="max-width:95vw;max-height:95vh;border-radius:8px">
</div>
{% endblock %}
{% block scripts %}
<script>
const WO_ID = {{ order.id }};
function saveTechFields() {
fetch(`/work-orders/${WO_ID}/update-fields`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
root_cause: document.getElementById('root_cause').value,
repairs_done: document.getElementById('repairs_done').value
})
}).then(() => {
document.getElementById('saveStatus').textContent = '✓ Guardado';
setTimeout(() => document.getElementById('saveStatus').textContent = '', 2000);
});
}
function deleteThisWO() {
if (!confirm('¿Eliminar esta orden de trabajo? Esta acción no se puede deshacer.')) return;
fetch('/work-orders/' + WO_ID + '/delete', {method:'DELETE'})
.then(r => r.json())
.then(d => {
if (d.ok) window.location.href = '/work-orders';
else alert('Error: ' + d.error);
});
}
function updateHours() {
const h = parseFloat(document.getElementById('labor_hours').value) || 0;
const r = parseFloat(document.getElementById('labor_rate').value) || 0;
const st = document.getElementById('hoursStatus');
st.textContent = 'Guardando...';
st.style.color = 'var(--gray)';
fetch(`/api/update-labor/${WO_ID}`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({labor_hours: h, labor_rate: r})
})
.then(r => r.json())
.then(d => {
if (d.ok) {
st.textContent = 'Guardado';
st.style.color = 'var(--success)';
setTimeout(() => location.reload(), 600);
} else {
st.textContent = 'Error';
st.style.color = 'var(--danger)';
}
})
.catch(e => { st.textContent = 'Error: ' + e; st.style.color = 'var(--danger)'; });
}
function uploadPhoto() {
const file = document.getElementById('photoFile').files[0];
if (!file) { alert('Selecciona una foto'); return; }
const fd = new FormData();
fd.append('photo', file);
fd.append('photo_type', document.getElementById('photoType').value);
fd.append('caption', document.getElementById('photoCaption').value);
document.getElementById('uploadStatus').textContent = 'Subiendo...';
fetch(`/work-orders/${WO_ID}/upload-photo`, { method:'POST', body: fd })
.then(r => r.json()).then(d => {
if (d.ok) location.reload();
else document.getElementById('uploadStatus').textContent = 'Error: ' + d.error;
});
}
function deletePhoto(id, el) {
if (!confirm('¿Eliminar esta foto?')) return;
fetch(`/api/delete-photo/${id}`, {method:'DELETE'})
.then(() => el.remove());
}
function updateCaption(id, caption) {
fetch(`/api/update-photo-caption/${id}`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({caption})
});
}
function fillPartInfo() {
const sel = document.getElementById('partSelect');
const opt = sel.options[sel.selectedIndex];
if (opt.value) {
document.getElementById('partCost').value = opt.dataset.price || 0;
}
}
function addPart() {
const data = {
part_id: document.getElementById('partSelect').value || null,
description: document.getElementById('partDesc').value,
quantity: document.getElementById('partQty').value,
unit_cost: document.getElementById('partCost').value
};
fetch(`/work-orders/${WO_ID}/add-part`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(data)
}).then(() => location.reload());
}
function openEquipModal() {
document.getElementById('woEquipEditId').value = '';
document.getElementById('woEquipSelect').value = '';
document.getElementById('woEquipDesc').value = '';
document.getElementById('woEquipHours').value = '0';
document.getElementById('woEquipNotes').value = '';
document.getElementById('equipModalTitle').textContent = '⚙️ Agregar Equipo a esta Orden';
document.getElementById('btnSaveEquip').textContent = '+ Agregar';
document.getElementById('woEquipStatus').textContent = '';
document.getElementById('woEquipModal').style.display = 'flex';
}
function closeEquipModal() {
document.getElementById('woEquipModal').style.display = 'none';
}
function editWoEquip(id, name, hours, rate, desc, notes) {
document.getElementById('woEquipEditId').value = id;
document.getElementById('woEquipDesc').value = desc;
document.getElementById('woEquipHours').value = hours;
document.getElementById('woEquipRate').value = rate;
document.getElementById('woEquipNotes').value = notes;
document.getElementById('equipModalTitle').textContent = '✏️ Editar Equipo: ' + name;
document.getElementById('btnSaveEquip').textContent = '💾 Guardar';
document.getElementById('woEquipStatus').textContent = '';
document.getElementById('woEquipModal').style.display = 'flex';
}
function saveWoEquip() {
const editId = document.getElementById('woEquipEditId').value;
const desc = document.getElementById('woEquipDesc').value.trim();
const statusEl = document.getElementById('woEquipStatus');
if (!desc) { alert('Describe el trabajo realizado'); return; }
const payload = {
equipment_id: document.getElementById('woEquipSelect').value || null,
description: desc,
notes: document.getElementById('woEquipNotes').value,
labor_hours: parseFloat(document.getElementById('woEquipHours').value) || 0,
labor_rate: parseFloat(document.getElementById('woEquipRate').value) || 0
};
statusEl.textContent = 'Guardando...';
statusEl.style.color = 'var(--gray)';
const url = editId
? '/work-orders/' + WO_ID + '/update-equipment/' + editId
: '/work-orders/' + WO_ID + '/add-equipment';
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(d => {
if (d.ok) { statusEl.textContent = '✓'; setTimeout(() => location.reload(), 400); }
else { statusEl.textContent = 'Error: ' + (d.error || 'desconocido'); statusEl.style.color = 'var(--danger)'; }
})
.catch(e => { statusEl.textContent = 'Error: ' + e; statusEl.style.color = 'var(--danger)'; });
}
function removeWoEquip(id, row) {
if (!confirm('Quitar este equipo de la orden?')) return;
fetch(`/work-orders/${WO_ID}/remove-equipment/${id}`, {method:'DELETE'})
.then(r => r.json())
.then(d => { if (d.ok) row.remove(); })
.catch(e => console.error(e));
}
// ── SHARE & EMAIL ─────────────────────────────────────────────────────────────
function openShareModal() {
document.getElementById('shareModal').style.display = 'flex';
showTab('email');
}
function closeShareModal() {
document.getElementById('shareModal').style.display = 'none';
}
function showTab(tab) {
document.getElementById('tabEmailContent').style.display = tab==='email' ? '' : 'none';
document.getElementById('tabShareContent').style.display = tab==='share' ? '' : 'none';
document.getElementById('tabEmail').className = 'btn btn-sm ' + (tab==='email' ? 'btn-primary' : 'btn-secondary');
document.getElementById('tabShare').className = 'btn btn-sm ' + (tab==='share' ? 'btn-primary' : 'btn-secondary');
document.getElementById('tabEmail').style.flex = '1';
document.getElementById('tabShare').style.flex = '1';
}
function sendEmail() {
const to = document.getElementById('sendToEmail').value.trim();
const name = document.getElementById('sendToName').value.trim();
const subj = document.getElementById('sendSubject').value.trim();
const lang = document.getElementById('sendLang').value;
const st = document.getElementById('emailStatus');
if (!to) { st.textContent = 'Ingresa un email.'; st.style.color='var(--danger)'; return; }
st.textContent = 'Generando PDF y enviando...'; st.style.color = 'var(--gray)';
fetch(`/work-orders/${WO_ID}/send`, {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body: new URLSearchParams({to_email:to, to_name:name, subject:subj, lang:lang})
}).then(r=>r.json()).then(d=>{
if (d.ok) {
st.textContent = '✅ Email enviado correctamente.';
st.style.color = 'var(--success)';
} else {
st.textContent = '❌ ' + (d.error || 'Error desconocido');
st.style.color = 'var(--danger)';
}
});
}
function generateShareLinks() {
const loading = document.getElementById('shareLoading');
loading.textContent = 'Guardando PDF...';
// First save PDF
fetch(`/work-orders/${WO_ID}/save-pdf`, {method:'POST'})
.then(r=>r.json()).then(saved => {
loading.textContent = 'Generando links...';
return fetch(`/work-orders/${WO_ID}/share`);
})
.then(r=>r.json()).then(d=>{
if (!d.ok) { loading.textContent = 'Error generando links.'; return; }
document.getElementById('pdfLink').value = d.pdf_url;
document.getElementById('waLink').href = d.wa_link;
document.getElementById('smsLink').href = d.sms_link;
document.getElementById('shareLinks').style.display = '';
loading.textContent = '';
});
}
openPhoto = function(src) {
document.getElementById('lightboxImg').src = src;
document.getElementById('lightbox').style.display = 'flex';
}
function initSignaturePad(canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Scale for retina
const rect = canvas.getBoundingClientRect();
let drawing = false;
let lastX = 0, lastY = 0;
function getPos(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return [(src.clientX - r.left) * (canvas.width / r.width),
(src.clientY - r.top) * (canvas.height / r.height)];
}
function start(e) { e.preventDefault(); drawing = true; [lastX, lastY] = getPos(e); }
function draw(e) {
if (!drawing) return; e.preventDefault();
const [x, y] = getPos(e);
ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(x, y);
ctx.strokeStyle = '#0a1628'; ctx.lineWidth = 2.5;
ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke();
[lastX, lastY] = [x, y];
}
function stop() { drawing = false; }
canvas.addEventListener('mousedown', start);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stop);
canvas.addEventListener('mouseleave', stop);
canvas.addEventListener('touchstart', start, {passive:false});
canvas.addEventListener('touchmove', draw, {passive:false});
canvas.addEventListener('touchend', stop);
}
function clearCanvas(canvasId) {
const canvas = document.getElementById(canvasId);
if (canvas) canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
}
function saveSignature(who) {
const canvasId = who === 'tech' ? 'sigTech' : 'sigClient';
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const dataUrl = canvas.toDataURL('image/png');
fetch(`/work-orders/${WO_ID}/save-signature`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({who, dataUrl})
}).then(r => r.json()).then(d => {
if (d.ok) location.reload();
else alert('Error al guardar firma');
});
}
function clearSignature(who) {
if (!confirm('¿Borrar esta firma?')) return;
fetch(`/work-orders/${WO_ID}/clear-signature`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({who})
}).then(() => location.reload());
}
initSignaturePad('sigTech');
initSignaturePad('sigClient');
function openPhoto(src) {
document.getElementById('lightboxImg').src = src;
document.getElementById('lightbox').style.display = 'flex';
}
function addEmail(email) {
const input = document.getElementById('assignEmails');
const current = input.value.trim();
if (current && !current.includes(email)) {
input.value = current + '; ' + email;
} else if (!current) {
input.value = email;
}
}
// ── ASSIGN MODAL ─────────────────────────────────────────────────────────────
function openAssignModal() {
document.getElementById('assignModal').style.display = 'flex';
}
function closeAssignModal() {
document.getElementById('assignModal').style.display = 'none';
}
function sendAssignment() {
const emails = document.getElementById('assignEmails').value.trim();
const message = document.getElementById('assignMessage').value.trim();
if (!emails) { alert('Ingresa al menos un email'); return; }
const btn = document.getElementById('assignBtn');
btn.disabled = true; btn.textContent = 'Enviando...';
fetch('/work-orders/{{ order.id }}/assign', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({to_emails: emails, message: message})
})
.then(r => r.json())
.then(d => {
btn.disabled = false; btn.textContent = '📨 Asignar y Enviar';
if (d.ok) {
closeAssignModal();
alert('✅ WO asignada y enviada con PDF adjunto.');
location.reload();
} else {
alert('Error al enviar: ' + (d.error || 'desconocido'));
}
});
}
// Auto-notify on status change
function setStatus(newStatus) {
fetch(`/work-orders/${WO_ID}/update-status`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({status: newStatus})
}).then(() => {
{% if order.assigned_to %}
fetch('/work-orders/{{ order.id }}/notify-status', {method:'POST'});
{% endif %}
location.reload();
});
}
function confirmReopen() {
if (!confirm('¿Reabrir esta orden de trabajo? El estado volverá a "En Progreso" y podrás editarla nuevamente.')) return;
setStatus('in_progress');
}
</script>
<!-- ASSIGN MODAL -->
<div id="assignModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);
z-index:1000;align-items:center;justify-content:center">
<div style="background:var(--navy2);border:1px solid rgba(0,180,216,0.3);border-radius:12px;
padding:24px;width:100%;max-width:500px;margin:20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0;color:var(--white)">👤 Asignar Orden de Trabajo</h3>
<button onclick="closeAssignModal()" style="background:none;border:none;color:var(--gray);font-size:20px;cursor:pointer"></button>
</div>
<div style="margin-bottom:8px;font-size:12px;color:var(--cyan)">{{ order.order_number }} — {{ order.vessel_name }}</div>
{% if order.assigned_to %}
<div style="background:rgba(0,180,216,0.08);border:1px solid rgba(0,180,216,0.2);border-radius:6px;padding:8px 12px;margin-bottom:12px;font-size:12px;color:var(--gray)">
Actualmente asignado a: <strong style="color:var(--cyan)">{{ order.assigned_to }}</strong>
</div>
{% endif %}
<div class="form-group" style="margin-bottom:12px">
<label>Emails (separados por coma o punto y coma)</label>
<input type="text" id="assignEmails"
placeholder="tecnico@empresa.com; supervisor@empresa.com"
style="width:100%;padding:8px 12px;border-radius:6px;
background:rgba(255,255,255,0.06);border:1px solid rgba(0,180,216,0.25);
color:var(--white);font-size:13px">
{% if users %}
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">
{% for u in users %}
<span onclick="addEmail('{{ u.email }}')"
style="background:rgba(0,180,216,0.1);border:1px solid rgba(0,180,216,0.2);
color:var(--cyan);padding:3px 8px;border-radius:12px;font-size:11px;
cursor:pointer" title="{{ u.full_name }}">
+ {{ u.full_name or u.username }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group" style="margin-bottom:16px">
<label>Mensaje adicional (opcional)</label>
<textarea id="assignMessage" rows="3"
style="width:100%;padding:8px 12px;border-radius:6px;
background:rgba(255,255,255,0.06);border:1px solid rgba(0,180,216,0.25);
color:var(--white);font-size:13px"
placeholder="Instrucciones especiales, prioridad, materiales necesarios..."></textarea>
</div>
<div style="font-size:11px;color:var(--gray);margin-bottom:12px">
📎 Se enviará el PDF de la WO adjunto al email
</div>
<div style="display:flex;gap:10px">
<button id="assignBtn" onclick="sendAssignment()" class="btn btn-primary" style="flex:1">📨 Asignar y Enviar</button>
<button onclick="closeAssignModal()" class="btn btn-secondary">Cancelar</button>
</div>
</div>
</div>
{% endblock %}