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>
186 lines
7.9 KiB
HTML
186 lines
7.9 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Órdenes de Trabajo{% endblock %}
|
|
{% block page_title %}Órdenes de Trabajo{% endblock %}
|
|
{% block topbar_actions %}
|
|
<a href="{{ url_for('work_order_new') }}" class="btn btn-primary">+ Nueva Orden</a>
|
|
{% endblock %}
|
|
{% block head %}
|
|
<style>
|
|
/* ── Filtros ── */
|
|
.wo-filters {
|
|
display:flex;gap:8px;align-items:center;margin-bottom:14px;flex-wrap:wrap;
|
|
}
|
|
.wo-filters input {
|
|
flex:1;min-width:180px;padding:7px 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;
|
|
}
|
|
|
|
/* ── Tabla (desktop) ── */
|
|
.wo-table-wrap { display:block; }
|
|
.wo-cards-wrap { display:none; }
|
|
|
|
/* ── Tarjetas (móvil) ── */
|
|
@media (max-width: 768px) {
|
|
.wo-table-wrap { display:none; }
|
|
.wo-cards-wrap { display:block; }
|
|
}
|
|
|
|
.wo-card {
|
|
background:var(--navy2);
|
|
border:1px solid rgba(0,180,216,0.12);
|
|
border-radius:10px;
|
|
margin-bottom:10px;
|
|
overflow:hidden;
|
|
transition:border-color 0.2s;
|
|
}
|
|
.wo-card:active { border-color:rgba(0,180,216,0.4); }
|
|
|
|
.wo-card-header {
|
|
display:flex;justify-content:space-between;align-items:center;
|
|
padding:10px 14px;
|
|
background:rgba(0,0,0,0.2);
|
|
border-bottom:1px solid rgba(255,255,255,0.05);
|
|
}
|
|
.wo-card-num { font-size:12px;color:var(--cyan);font-weight:600;font-family:monospace; }
|
|
.wo-card-body { padding:10px 14px; }
|
|
.wo-card-vessel {
|
|
font-size:15px;font-weight:600;color:var(--white);margin-bottom:4px;
|
|
}
|
|
.wo-card-scope {
|
|
font-size:13px;color:var(--gray);margin-bottom:6px;
|
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
|
|
}
|
|
.wo-card-meta {
|
|
display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--gray);margin-bottom:10px;
|
|
}
|
|
.wo-card-meta span { display:flex;align-items:center;gap:4px; }
|
|
.wo-card-actions {
|
|
display:flex;gap:8px;padding:8px 14px;
|
|
border-top:1px solid rgba(255,255,255,0.05);
|
|
background:rgba(0,0,0,0.1);
|
|
}
|
|
.wo-card-actions a, .wo-card-actions button {
|
|
flex:1;text-align:center;font-size:13px;padding:8px 4px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
{% block content %}
|
|
|
|
<div class="wo-filters">
|
|
<a href="{{ url_for('work_orders') }}" class="btn btn-sm {% if not status_filter %}btn-primary{% else %}btn-secondary{% endif %}">Todas</a>
|
|
<a href="{{ url_for('work_orders', status='open') }}" class="btn btn-sm {% if status_filter=='open' %}btn-primary{% else %}btn-secondary{% endif %}">Abiertas</a>
|
|
<a href="{{ url_for('work_orders', status='in_progress') }}" class="btn btn-sm {% if status_filter=='in_progress' %}btn-warning{% else %}btn-secondary{% endif %}">En Progreso</a>
|
|
<a href="{{ url_for('work_orders', status='completed') }}" class="btn btn-sm {% if status_filter=='completed' %}btn-success{% else %}btn-secondary{% endif %}">Completadas</a>
|
|
<input type="text" id="woSearch" placeholder="🔍 Buscar embarcación, scope, técnico..."
|
|
oninput="filterWO(this.value)">
|
|
</div>
|
|
|
|
<!-- TABLA DESKTOP -->
|
|
<div class="wo-table-wrap card">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr><th>Orden</th><th>🚢 Embarcación</th><th>Tipo</th><th>Scope</th><th>Técnico</th><th>Fecha</th><th>Estado</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for o in orders %}
|
|
<tr class="wo-row" data-search="{{ (o.vessel_name ~ ' ' ~ (o.scope or '') ~ ' ' ~ (o.technician or '') ~ ' ' ~ o.order_number)|lower }}">
|
|
<td class="text-cyan" style="white-space:nowrap;font-family:monospace;font-size:12px">{{ o.order_number }}</td>
|
|
<td><a href="{{ url_for('vessel_history', vid=o.vessel_id) }}" class="text-cyan" style="font-weight:600">{{ o.vessel_name }}</a></td>
|
|
<td>{{ o.work_type or '—' }}</td>
|
|
<td style="max-width:240px">{{ o.scope or (o.description[:60] if o.description else '—') }}</td>
|
|
<td>{{ o.technician or '—' }}</td>
|
|
<td class="text-gray" style="white-space:nowrap">{{ o.start_date or o.created_at[:10] }}</td>
|
|
<td><span class="badge badge-{{ o.status }}">{{ o.status.replace('_',' ') }}</span></td>
|
|
<td class="flex gap-2" 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>
|
|
<button onclick="deleteWO({{ o.id }})" class="btn btn-sm btn-danger">🗑️</button>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="8" class="text-gray" style="text-align:center;padding:30px">No hay órdenes.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="noWOResultsTable" style="display:none;text-align:center;padding:20px;color:var(--gray)">
|
|
No se encontraron órdenes.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TARJETAS MÓVIL -->
|
|
<div class="wo-cards-wrap">
|
|
{% for o in orders %}
|
|
<div class="wo-card" id="card-{{ o.id }}"
|
|
data-search="{{ (o.vessel_name ~ ' ' ~ (o.scope or '') ~ ' ' ~ (o.technician or '') ~ ' ' ~ o.order_number)|lower }}">
|
|
<div class="wo-card-header">
|
|
<span class="wo-card-num">{{ o.order_number }}</span>
|
|
<span class="badge badge-{{ o.status }}">{{ o.status.replace('_',' ') }}</span>
|
|
</div>
|
|
<div class="wo-card-body">
|
|
<div class="wo-card-vessel">
|
|
<a href="{{ url_for('vessel_history', vid=o.vessel_id) }}" style="color:var(--cyan)">
|
|
🚢 {{ o.vessel_name }}
|
|
</a>
|
|
</div>
|
|
<div class="wo-card-scope">{{ o.scope or (o.description[:100] if o.description else 'Sin descripción') }}</div>
|
|
<div class="wo-card-meta">
|
|
{% if o.work_type %}<span>🔧 {{ o.work_type }}</span>{% endif %}
|
|
{% if o.technician %}<span>👤 {{ o.technician }}</span>{% endif %}
|
|
<span>📅 {{ o.start_date or o.created_at[:10] }}</span>
|
|
{% if o.billing_type == 'lump_sum' %}<span>💰 Todo costo</span>
|
|
{% elif o.billing_type == 'labor_only' %}<span>🔧 Solo M.O.</span>
|
|
{% else %}<span>📋 M.O.+Mat.</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="wo-card-actions">
|
|
<a href="{{ url_for('work_order_detail', woid=o.id) }}" class="btn btn-sm btn-primary">Ver detalle</a>
|
|
{% if o.status != 'completed' %}
|
|
<a href="{{ url_for('work_order_edit', woid=o.id) }}" class="btn btn-sm btn-secondary">✏️ Editar</a>
|
|
<button onclick="deleteWO({{ o.id }})" class="btn btn-sm btn-danger">🗑️</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div style="text-align:center;padding:40px;color:var(--gray)">No hay órdenes.</div>
|
|
{% endfor %}
|
|
<div id="noWOResultsCards" style="display:none;text-align:center;padding:20px;color:var(--gray)">
|
|
No se encontraron órdenes.
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
function filterWO(q) {
|
|
q = q.toLowerCase().trim();
|
|
let visibleT = 0, visibleC = 0;
|
|
document.querySelectorAll('.wo-row').forEach(r => {
|
|
const show = !q || r.dataset.search.includes(q);
|
|
r.style.display = show ? '' : 'none';
|
|
if (show) visibleT++;
|
|
});
|
|
document.querySelectorAll('.wo-card').forEach(c => {
|
|
const show = !q || c.dataset.search.includes(q);
|
|
c.style.display = show ? '' : 'none';
|
|
if (show) visibleC++;
|
|
});
|
|
document.getElementById('noWOResultsTable').style.display = visibleT === 0 ? 'block' : 'none';
|
|
document.getElementById('noWOResultsCards').style.display = visibleC === 0 ? 'block' : 'none';
|
|
}
|
|
function deleteWO(id) {
|
|
if (!confirm('¿Eliminar esta orden? Esta acción no se puede deshacer.')) return;
|
|
fetch('/work-orders/' + id + '/delete', {method:'DELETE'})
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (d.ok) location.reload();
|
|
else alert('Error: ' + d.error);
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|