Files
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

334 lines
16 KiB
HTML

{% extends 'base.html' %}
{% block title %}Historial — {{ vessel.name }}{% endblock %}
{% block page_title %}{{ vessel.name }}{% endblock %}
{% block topbar_actions %}
<a href="{{ url_for('work_order_new') }}?vessel={{ vessel.id }}" class="btn btn-primary">+ Nueva Orden</a>
<a href="{{ url_for('vessel_edit', vid=vessel.id) }}" class="btn btn-secondary">✏️ Editar</a>
<a href="{{ url_for('vessels') }}" class="btn btn-secondary">← Volver</a>
{% endblock %}
{% block content %}
<!-- STATS -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px">
<div class="stat-card">
<div class="stat-label">🚢 Tipo</div>
<div style="font-size:15px;font-weight:600;color:var(--white);margin-top:4px">{{ vessel.vessel_type or '—' }}</div>
<div class="stat-sub">{{ vessel.make or '' }} {{ vessel.model or '' }} {{ vessel.year or '' }}</div>
</div>
<div class="stat-card">
<div class="stat-label">⚙️ Horas Motor</div>
<div class="stat-value">{{ vessel.engine_hours or 0 }}</div>
<div class="stat-sub">horas acumuladas</div>
</div>
<div class="stat-card">
<div class="stat-label">📋 Total WOs</div>
<div class="stat-value">{{ orders|length }}</div>
<div class="stat-sub">órdenes registradas</div>
</div>
<div class="stat-card">
<div class="stat-label">💰 Costo Total</div>
<div class="stat-value" style="font-size:22px">${{ "%.0f"|format(total_cost) }}</div>
<div class="stat-sub">histórico acumulado</div>
</div>
</div>
<!-- CONTACTOS -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px">
<div class="card" style="padding:14px 18px">
<div style="font-size:10px;color:var(--gray);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px">👤 Propietario</div>
<div style="font-size:14px;font-weight:600">{{ vessel.owner_name or '—' }}</div>
<div style="font-size:12px;color:var(--gray)">{{ vessel.owner_phone or '' }}{% if vessel.owner_email %} · {{ vessel.owner_email }}{% endif %}</div>
</div>
<div class="card" style="padding:14px 18px">
<div style="font-size:10px;color:var(--cyan);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px">⚓ Capitán</div>
<div style="font-size:14px;font-weight:600">{{ vessel.captain_name or '—' }}</div>
<div style="font-size:12px;color:var(--gray)">{{ vessel.captain_phone or '' }}{% if vessel.captain_email %} · {{ vessel.captain_email }}{% endif %}</div>
</div>
</div>
<!-- ═══ ÓRDENES DE TRABAJO ═══ -->
<div class="card mb-4">
<div class="card-header flex justify-between">
<span>📋 Órdenes de Trabajo</span>
<a href="{{ url_for('work_order_new') }}?vessel={{ vessel.id }}" class="btn btn-sm btn-primary">+ Nueva Orden</a>
</div>
{% if orders %}
<!-- Búsqueda -->
<div style="margin-bottom:12px;display:flex;gap:10px">
<input type="text" id="histSearch"
placeholder="🔍 Buscar por scope, fecha, técnico..."
style="flex:1;padding:8px 12px;font-size:13px;border-radius:6px;
background:rgba(255,255,255,0.06);border:1px solid rgba(0,180,216,0.2);color:var(--white);"
oninput="filterHistory(this.value)">
<select id="statusFilter" onchange="filterHistory(document.getElementById('histSearch').value)"
style="padding:8px 12px;border-radius:6px;background:rgba(255,255,255,0.06);
border:1px solid rgba(0,180,216,0.2);color:var(--white);font-size:13px">
<option value="">Todos</option>
<option value="open">Abiertas</option>
<option value="in_progress">En Progreso</option>
<option value="completed">Completadas</option>
</select>
</div>
<!-- Tabla desktop -->
<div class="table-wrap" id="histTable">
<table>
<thead>
<tr><th>Orden</th><th>Fecha</th><th>Sistema</th><th>Scope</th><th>Técnico</th><th>Horas</th><th>Costo</th><th>Estado</th><th></th></tr>
</thead>
<tbody id="woTableBody">
{% for o in orders %}
<tr class="history-row"
data-search="{{ (o.scope or '' ~ ' ' ~ (o.description or '') ~ ' ' ~ (o.technician or '') ~ ' ' ~ (o.start_date or ''))|lower }}"
data-status="{{ o.status }}">
<td class="text-cyan" style="white-space:nowrap;font-size:12px">{{ o.order_number }}</td>
<td class="text-gray" style="white-space:nowrap">{{ o.start_date or o.created_at[:10] }}</td>
<td class="text-gray" style="font-size:12px">{{ o.system_name or '—' }}</td>
<td style="max-width:280px">
<div style="font-weight:600;font-size:13px">{{ o.scope or '—' }}</div>
{% if o.description and o.scope and o.description != o.scope %}
<div style="font-size:11px;color:var(--gray)">{{ o.description[:80] }}{% if o.description|length > 80 %}...{% endif %}</div>
{% endif %}
</td>
<td style="font-size:12px">{{ o.technician or '—' }}</td>
<td style="font-size:12px">{{ o.labor_hours or 0 }} h</td>
<td class="text-cyan" style="font-size:12px;white-space:nowrap">
${{ "%.0f"|format((o.calc_labor_cost or 0) + (o.total_parts_cost or 0)) }}
</td>
<td><span class="badge badge-{{ o.status }}" style="font-size:10px">{{ o.status.replace('_',' ') }}</span></td>
<td style="white-space:nowrap">
<a href="{{ url_for('work_order_detail', woid=o.id) }}" class="btn btn-sm btn-secondary">Ver</a>
{% if o.status != 'completed' %}
<a href="{{ url_for('work_order_edit', woid=o.id) }}" class="btn btn-sm btn-secondary">✏️</a>
{% endif %}
<a href="{{ url_for('work_order_pdf', woid=o.id) }}" target="_blank" class="btn btn-sm btn-primary">📄</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Tarjetas móvil -->
<div id="histCards">
{% for o in orders %}
<div class="hist-card history-row"
data-search="{{ (o.scope or '' ~ ' ' ~ (o.description or '') ~ ' ' ~ (o.technician or '') ~ ' ' ~ (o.start_date or ''))|lower }}"
data-status="{{ o.status }}"
style="background:var(--navy2);border:1px solid rgba(0,180,216,0.12);border-radius:10px;margin-bottom:10px;overflow:hidden;">
<div style="padding:10px 14px;background:rgba(0,0,0,0.2);border-bottom:1px solid rgba(255,255,255,0.05);display:flex;justify-content:space-between;align-items:center">
<span style="font-size:12px;color:var(--cyan);font-family:monospace;font-weight:600">{{ o.order_number }}</span>
<span class="badge badge-{{ o.status }}" style="font-size:11px">{{ o.status.replace('_',' ') }}</span>
</div>
<div style="padding:10px 14px">
<div style="font-weight:600;font-size:14px;color:var(--white);margin-bottom:4px">{{ o.scope or '—' }}</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--gray);margin-bottom:6px">
{% if o.system_name %}<span>🔩 {{ o.system_name }}</span>{% endif %}
{% if o.technician %}<span>👤 {{ o.technician }}</span>{% endif %}
<span>📅 {{ o.start_date or o.created_at[:10] }}</span>
</div>
<div style="display:flex;gap:16px;font-size:13px">
<span style="color:var(--gray)">{{ o.labor_hours or 0 }} h</span>
<span style="color:var(--cyan);font-weight:600">${{ "%.0f"|format((o.calc_labor_cost or 0) + (o.total_parts_cost or 0)) }}</span>
</div>
</div>
<div style="display:flex;gap:8px;padding:8px 14px;border-top:1px solid rgba(255,255,255,0.05);background:rgba(0,0,0,0.1)">
<a href="{{ url_for('work_order_detail', woid=o.id) }}" class="btn btn-sm btn-primary" style="flex:1;text-align:center">Ver detalle</a>
{% if o.status != 'completed' %}
<a href="{{ url_for('work_order_edit', woid=o.id) }}" class="btn btn-sm btn-secondary">✏️</a>
{% endif %}
<a href="{{ url_for('work_order_pdf', woid=o.id) }}" target="_blank" class="btn btn-sm btn-secondary">📄</a>
</div>
</div>
{% endfor %}
</div>
<div id="noHistResults" style="display:none;text-align:center;padding:20px;color:var(--gray)">
No se encontraron órdenes.
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--gray)">
<div style="font-size:36px;margin-bottom:10px">📋</div>
<div style="margin-bottom:14px">Sin órdenes de trabajo para esta embarcación.</div>
<a href="{{ url_for('work_order_new') }}?vessel={{ vessel.id }}" class="btn btn-primary">+ Crear Primera Orden</a>
</div>
{% endif %}
</div>
<!-- EQUIPOS -->
<div class="card mb-4">
<div class="card-header flex justify-between">
<span>⚙️ Equipos Registrados</span>
<a href="{{ url_for('equipment_new', vid=vessel.id) }}" class="btn btn-sm btn-primary">+ Agregar Equipo</a>
</div>
{% if equipment %}
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px">
{% for e in equipment %}
<div style="background:rgba(0,0,0,0.2);border:1px solid rgba(0,180,216,0.12);border-radius:8px;padding:12px 14px">
<div style="font-size:13px;font-weight:600;color:var(--white);margin-bottom:3px">{{ e.name }}</div>
<div style="font-size:11px;color:var(--gray);margin-bottom:5px">
{{ e.make or '' }} {{ e.model or '' }}
{% if e.position %}· <span style="color:var(--cyan)">{{ e.position }}</span>{% endif %}
</div>
{% if e.serial_number %}
<div style="font-size:11px;background:rgba(0,180,216,0.08);border:1px solid rgba(0,180,216,0.2);
border-radius:4px;padding:3px 8px;color:var(--cyan);font-family:monospace;margin-bottom:5px">
S/N: {{ e.serial_number }}
</div>
{% endif %}
<div style="font-size:11px;color:var(--gray)">{{ e.engine_hours or 0 }} h</div>
<a href="{{ url_for('equipment_edit', vid=vessel.id, eid=e.id) }}"
class="btn btn-sm btn-secondary" style="margin-top:8px;font-size:11px">✏️</a>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray" style="font-size:13px">Sin equipos. Agrega motores, generadores, etc.</p>
{% endif %}
</div>
<!-- DOCUMENTOS -->
<div class="card mb-4">
<div class="card-header flex justify-between">
<span>📁 Documentos y Manuales</span>
<button onclick="document.getElementById('docModal').style.display='flex'" class="btn btn-sm btn-primary">+ Adjuntar</button>
</div>
{% if documents %}
<div class="table-wrap">
<table>
<thead><tr><th>Título</th><th>Tipo</th><th>Equipo</th><th>Tamaño</th><th>Fecha</th><th></th></tr></thead>
<tbody>
{% for d in documents %}
<tr>
<td>
<span style="font-size:15px;margin-right:6px">
{% if d.filename.endswith('.pdf') %}📄
{% elif d.filename.endswith(('.doc','docx')) %}📝
{% elif d.filename.endswith(('.xls','xlsx')) %}📊
{% elif d.filename.endswith(('.jpg','jpeg','png','gif')) %}🖼️
{% else %}📎{% endif %}
</span>
<strong>{{ d.title }}</strong>
{% if d.description %}<br><span class="text-gray" style="font-size:11px">{{ d.description }}</span>{% endif %}
</td>
<td><span class="badge badge-open" style="font-size:10px">{{ d.doc_type }}</span></td>
<td class="text-gray">{{ d.equipment_name or '—' }}</td>
<td class="text-gray" style="font-size:12px">{% if d.file_size %}{{ "%.1f"|format(d.file_size/1024) }} KB{% else %}—{% endif %}</td>
<td class="text-gray" style="font-size:12px">{{ d.created_at[:10] }}</td>
<td class="flex gap-2">
<a href="{{ url_for('document_download', doc_id=d.id) }}" class="btn btn-sm btn-primary">⬇️</a>
<button onclick="deleteDoc({{ d.id }}, this.closest('tr'))" class="btn btn-sm btn-danger">🗑️</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray" style="font-size:13px">Sin documentos adjuntos.</p>
{% endif %}
</div>
<!-- MODAL DOCUMENTO -->
<div id="docModal" 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">📁 Adjuntar Documento</div>
<div class="form-group mb-3">
<label>Título *</label>
<input type="text" id="docTitle" placeholder="Ej: Manual MTU 12V2000">
</div>
<div class="form-grid mb-3">
<div class="form-group">
<label>Tipo</label>
<select id="docType">
<option value="manual">Manual</option>
<option value="certificate">Certificado</option>
<option value="warranty">Garantía</option>
<option value="inspection">Inspección</option>
<option value="drawing">Plano/Diagrama</option>
<option value="other">Otro</option>
</select>
</div>
<div class="form-group">
<label>Equipo (opcional)</label>
<select id="docEquipment">
<option value="">— General —</option>
{% for e in equipment %}
<option value="{{ e.id }}">{{ e.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group mb-3">
<label>Descripción (opcional)</label>
<input type="text" id="docDesc">
</div>
<div class="form-group mb-3">
<label>Archivo</label>
<input type="file" id="docFile" accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.png,.jpg,.jpeg" style="padding:8px">
</div>
<div class="flex gap-3">
<button onclick="uploadDoc()" class="btn btn-primary">📤 Subir</button>
<button onclick="document.getElementById('docModal').style.display='none'" class="btn btn-secondary">Cancelar</button>
</div>
<div id="docStatus" style="margin-top:10px;font-size:13px"></div>
</div>
</div>
{% endblock %}
{% block head %}
<style>
@media (max-width:768px) {
#histTable { display:none; }
#histCards { display:block; }
}
@media (min-width:769px) {
#histTable { display:block; }
#histCards { display:none; }
}
</style>
{% endblock %}
{% block scripts %}
<script>
const VID = {{ vessel.id }};
function filterHistory(q) {
q = (q || '').toLowerCase().trim();
const status = document.getElementById('statusFilter').value;
const rows = document.querySelectorAll('.history-row');
let visible = 0;
rows.forEach(r => {
const show = (!q || r.dataset.search.includes(q)) && (!status || r.dataset.status === status);
r.style.display = show ? '' : 'none';
if (show) visible++;
});
document.getElementById('noHistResults').style.display = visible === 0 ? 'block' : 'none';
}
function uploadDoc() {
const file = document.getElementById('docFile').files[0];
const title = document.getElementById('docTitle').value.trim();
if (!file) { alert('Selecciona un archivo'); return; }
if (!title) { alert('Ingresa un título'); return; }
const fd = new FormData();
fd.append('file', file);
fd.append('title', title);
fd.append('doc_type', document.getElementById('docType').value);
fd.append('equipment_id', document.getElementById('docEquipment').value);
fd.append('description', document.getElementById('docDesc').value);
document.getElementById('docStatus').textContent = 'Subiendo...';
fetch(`/vessels/${VID}/documents/upload`, { method:'POST', body: fd })
.then(r => r.json()).then(d => {
if (d.ok) location.reload();
else document.getElementById('docStatus').textContent = 'Error: ' + d.error;
});
}
function deleteDoc(id, row) {
if (!confirm('¿Eliminar este documento?')) return;
fetch(`/documents/${id}/delete`, {method:'DELETE'})
.then(() => row.remove());
}
</script>
{% endblock %}