67a0e674ca
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>
1000 lines
49 KiB
HTML
1000 lines
49 KiB
HTML
{% 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 %}
|