Files
pyfleet/templates/itv/list.html
T
2026-09-12 15:18:41 +02:00

463 lines
18 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}ITV{% endblock %}
{% block extra_css %}
.filter-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 24px;
}
.filter-row {
display: flex;
align-items: flex-end;
gap: 16px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-group label {
font-size: 11px;
font-weight: 600;
letter-spacing: .7px;
text-transform: uppercase;
color: var(--muted);
}
.filter-group input[type="date"] {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-family: 'Inter', sans-serif;
font-size: 14px;
padding: 9px 12px;
outline: none;
transition: border-color .15s;
min-width: 160px;
}
.filter-group input[type="date"]:focus { border-color: var(--accent); }
.itv-vencida td { color: #f87171 !important; }
.itv-proxima td { color: #f59e0b !important; }
.badge-vencida { background:rgba(248,113,113,.12); color:#f87171; font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px; border:1px solid rgba(248,113,113,.3); white-space:nowrap; }
.badge-proxima { background:rgba(245,158,11,.12); color:#f59e0b; font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px; border:1px solid rgba(245,158,11,.3); white-space:nowrap; }
.badge-ok { background:rgba(52,211,153,.12); color:#34d399; font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px; border:1px solid rgba(52,211,153,.3); white-space:nowrap; }
{% endblock %}
{% block content %}
<div class="page-header">
<div class="page-eyebrow">Revisiones</div>
<div class="page-title"><i class="bi bi-calendar-check"></i> Control de ITV</div>
<div class="page-subtitle">Filtra los vehículos por fecha de vencimiento de la ITV, población, unidad, tipo vehículo.</div>
</div>
<div class="filter-card">
<form method="GET" action="{{ url_for('itv_list') }}" id="filterForm">
<div class="filter-row">
<div class="filter-group">
<label for="desde">Fecha desde</label>
<input type="date" id="desde" name="desde" value="{{ desde }}">
</div>
<div class="filter-group">
<label for="hasta">Fecha hasta</label>
<input type="date" id="hasta" name="hasta" value="{{ hasta }}">
</div>
<!-- <div class="filter-group">
<label for="unidad_itv">Unidad asdf</label>
<input type="text" id="unidad_itv" name="unidad_itv"
value="{{ unidad_itv }}"
placeholder="Filtrar por unidad…"
style="background:var(--bg);border:1px solid var(--border);border-radius:8px;
color:var(--text);font-family:'Inter',sans-serif;font-size:14px;
padding:9px 12px;outline:none;transition:border-color .15s;min-width:200px;"
onfocus="this.style.borderColor='var(--accent)'"
onblur="this.style.borderColor='var(--border)'">
</div> -->
{# Campo de filtro - sustituye el anterior #}
<div class="filter-group" >
<label for="q_itv">Población, Unidad, Tipo vehículo</label>
<div style="position:relative;">
<input type="text" id="q_itv" name="q_itv"
value="{{ q_itv }}"
placeholder="Buscar, palabras separadas por coma"
style="background:var(--bg);border:1px solid var(--border);border-radius:8px;
color:var(--text);font-family:'Inter',sans-serif;font-size:14px;
padding:9px 12px;outline:none;transition:border-color .15s;min-width:280px;"
onfocus="this.style.borderColor='var(--accent)'"
onblur="this.style.borderColor='var(--border)'">
</div>
</div>
<button type="submit" class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
Buscar
</button>
{# Botón limpiar #}
{% if desde or hasta or q_itv %}
<a href="{{ url_for('itv_list') }}" class="btn btn-ghost">✕ Limpiar</a>
{% endif %}
{% if buscado and rows %}
<div style="display: grid; justify-items: start; gap: 2px;">
<!-- <span class="badge-vencida">Vencida</span> -->
<span class="badge-proxima">Próxima (vencimiento - 1 de un mes)</span>
<span class="badge-ok">Al día (vencimiento + 1 de un mes)</span>
</div>
<div style="margin-left:auto;display:flex;gap:8px; padding: 0px;">
<a href="{{ url_for('itv_exportar', desde=desde, hasta=hasta, q_itv=q_itv) }}"
class="btn btn-ghost" title="Exportar a Excel">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="12" y1="18" x2="12" y2="12"/>
<line x1="9" y1="15" x2="12" y2="18"/><line x1="15" y1="15" x2="12" y2="18"/>
</svg>
Excel
</a>
<button type="button" class="btn btn-ghost" id="btnPDF" title="Imprimir PDF">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 6 2 18 2 18 9"/>
<path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"/>
<rect x="6" y="14" width="12" height="8"/>
</svg>
Imprimir - PDF
</button>
<button type="button" class="btn btn-ghost" id="btnCopiar" title="Copiar al portapapeles">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
Copiar
</button>
</div>
{% endif %}
</div>
</form>
</div>
{% if not buscado %}
<div class="empty-state" style="padding:60px 24px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
style="width:48px;height:48px;margin-bottom:16px;opacity:.3">
<rect x="3" y="4" width="18" height="18" rx="2"/>
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/><path d="M9 16l2 2 4-4"/>
</svg>
<p style="color:var(--muted);font-size:14px;">Selecciona un rango de fechas y pulsa Buscar.</p>
</div>
{% elif rows %}
<p style="margin-top:12px;margin-bottom:10px;font-size:12px;color:var(--muted)">
{{ rows|length }} vehículo{{ 's' if rows|length != 1 }}
{% if desde and hasta %} con ITV entre {{ desde | fecha_es}} y {{ hasta | fecha_es }}
{% elif desde %} con ITV desde {{ desde | fecha_es}}
{% elif hasta %} con ITV hasta {{ hasta | fecha_es}}{% endif %}
{% if unidad %} · unidad_itv: <strong>{{ unidad_itv }}</strong>{% endif %}
</p>
<div class="table-wrap">
<table id="tablaITV">
<thead>
<tr>
<th hidden>ID</th>
<th>M. Oficial</th>
<th>M. Reservada</th>
<th>Marca</th>
<th>Modelo</th>
<th>Tipo</th>
<th>Poblacion</th>
<th>Unidad</th>
<th>Fecha ITV</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
{% set hoy = now %}
{% for r in rows %}
{% set itv_str = r['itv'][:10] if r['itv'] else '' %}
<tr onclick="abrirVehiculo({{ r['id'] }})"
style="cursor:pointer;"
onmouseover="this.style.background='var(--pill-bg)'"
onmouseout="this.style.background=''">
<td hidden class="td-id">#{{ r['id'] }}</td>
<td>{{ r['m_oficial'] or '—' }}</td>
<td>{{ r['m_reservada'] or '—' }}</td>
<td><strong>{{ r['marca'] or '—' }}</strong></td>
<td>{{ r['modelo'] or '—' }}</td>
<td>{{ r['tipo'] or '—' }}</td>
<td>{{ r['poblacion'] or '—' }}</td>
<td>{{ r['unidad'] or '—' }}</td>
<td style="white-space:nowrap;font-family:monospace">{{ itv_str | fecha_es}}</td>
<td id="estado-{{ r['id'] }}" data-itv="{{ itv_str }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state" style="padding:48px 24px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
style="width:40px;height:40px;margin-bottom:12px;opacity:.3">
<circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/>
</svg>
<p style="color:var(--muted);font-size:14px;">
No se encontraron vehículos con ITV en ese rango de fechas.
</p>
</div>
{% endif %}
<script>
function abrirVehiculo(id) {
var desde = document.getElementById('desde').value;
var hasta = document.getElementById('hasta').value;
var qItv = document.getElementById('q_itv').value;
window.location = '/vehiculos/' + id + '/editar'
+ '?desde=' + encodeURIComponent(desde)
+ '&hasta=' + encodeURIComponent(hasta)
+ '&q_itv=' + encodeURIComponent(qItv);
}
/* ── Estado ITV según fecha ── */
var hoy = new Date();
hoy.setHours(0,0,0,0);
var treintaDias = new Date(hoy); treintaDias.setDate(hoy.getDate() + 30);
document.querySelectorAll('[data-itv]').forEach(function(td) {
var itv = td.getAttribute('data-itv');
if (!itv) return;
var fecha = new Date(itv);
var tr = td.closest('tr');
if (fecha < hoy) {
td.innerHTML = '<span class="badge-vencida">Vencida</span>';
tr.classList.add('itv-vencida');
} else if (fecha <= treintaDias) {
td.innerHTML = '<span class="badge-proxima">Próxima</span>';
tr.classList.add('itv-proxima');
} else {
td.innerHTML = '<span class="badge-ok">Al día</span>';
}
});
/* ── PDF ── */
document.getElementById('btnPDF').addEventListener('click', function() {
var desde = '{{ desde }}', hasta = '{{ hasta }}';
var titulo = 'Control ITV' + (desde ? ' — Desde: ' + desde : '') + (hasta ? ' Hasta: ' + hasta : '');
var fecha = new Date().toLocaleDateString('es-ES');
var heads = ['M. Oficial','M. Reservada','Marca','Modelo','Tipo','Poblacion',"Unidad",'Fecha ITV','Estado'];
var filas = '';
document.querySelectorAll('#tablaITV tbody tr').forEach(function(tr) {
filas += '<tr>';
var celdas = tr.querySelectorAll('td');
// Empezar desde índice 1 para saltar el ID (índice 0)
for (var i = 1; i < celdas.length; i++) {
filas += '<td>' + celdas[i].innerText.trim() + '</td>';
}
filas += '</tr>';
});
var html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${titulo}</title>
<style>
body{font-family:Arial,sans-serif;font-size:10px;color:#1a1d29;margin:20px}
h2{font-size:14px;margin-bottom:4px}.meta{font-size:9px;color:#666;margin-bottom:14px}
table{width:100%;border-collapse:collapse}
th{background:#5c6ef8;color:#fff;padding:6px 7px;text-align:left;font-size:9px}
td{padding:5px 7px;border-bottom:1px solid #e3e6ef;font-size:9px}
tr:nth-child(even) td{background:#eef0f8}
.v{color:#ef4444;font-weight:700}.p{color:#d97706;font-weight:700}.o{color:#059669;font-weight:700}
@media print{body{margin:10px}}
</style></head><body>
<h2>${titulo}</h2>
<div class="meta">Generado el ${fecha} · ${document.querySelectorAll('#tablaITV tbody tr').length} vehículos</div>
<table><thead><tr>${heads.map(h=>'<th>'+h+'</th>').join('')}</tr></thead>
<tbody>${filas}</tbody></table></body></html>`;
var win = window.open('','_blank','width=1000,height=700');
win.document.write(html); win.document.close(); win.focus(); win.print(); win.close();
});
// COPIAR
document.getElementById('btnCopiar') && document.getElementById('btnCopiar').addEventListener('click', function() {
var btn = this;
var heads = ['M. Oficial','M. Reservada','Marca','Modelo','Tipo','Poblacion ','Unidad','Fecha ITV','Estado'];
var lineas = [heads.join('\t')];
document.querySelectorAll('#tablaITV tbody tr').forEach(function(tr) {
var celdas = tr.querySelectorAll('td');
var fila = [];
// Empezar desde índice 1 para saltar el ID
for (var i = 1; i < celdas.length; i++) {
fila.push(celdas[i].innerText.trim());
}
lineas.push(fila.join('\t'));
});
var texto = lineas.join('\n');
if (!texto.trim() || lineas.length <= 1) {
alert('No hay datos para copiar.');
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(texto)
.then(function() { mostrarCopiado(btn); })
.catch(function() { copiarFallback(texto, btn); });
} else {
copiarFallback(texto, btn);
}
});
function copiarFallback(texto, btn) {
var ta = document.createElement('textarea');
ta.value = texto;
ta.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;';
document.body.appendChild(ta);
ta.focus();
ta.select();
ta.setSelectionRange(0, 99999);
var ok = false;
try { ok = document.execCommand('copy'); } catch(e) { ok = false; }
document.body.removeChild(ta);
if (ok) {
mostrarCopiado(btn);
} else {
alert('No se pudo copiar automáticamente.\nSelecciona el texto manualmente con Ctrl+A y Ctrl+C.');
}
}
function mostrarCopiado(btn) {
var orig = btn.innerHTML;
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Copiado';
btn.style.color = 'var(--success)';
btn.style.borderColor = 'var(--success)';
setTimeout(function() {
btn.innerHTML = orig;
btn.style.color = '';
btn.style.borderColor = '';
}, 2000);
}
</script>
<script>
/* ── ORDENACIÓN POR COLUMNAS ── */
(function() {
var estado = { col: -1, asc: true };
document.querySelectorAll('thead th').forEach(function(th, idx) {
// No ordenar la última columna (acciones/estado)
th.style.cursor = 'pointer';
th.style.userSelect = 'none';
th.title = 'Pulsa para ordenar';
// Añadir icono de orden
var icon = document.createElement('span');
icon.style.cssText = 'margin-left:5px;opacity:.3;font-size:10px;';
icon.textContent = '⇅';
icon.className = 'sort-icon';
th.appendChild(icon);
th.addEventListener('click', function() {
var tbody = document.querySelector('tbody');
var filas = Array.from(tbody.querySelectorAll('tr[data-id], tr[onclick]'));
if (!filas.length) return;
var mismaCol = estado.col === idx;
estado.asc = mismaCol ? !estado.asc : true;
estado.col = idx;
// Actualizar iconos
document.querySelectorAll('.sort-icon').forEach(function(ic) {
ic.textContent = '⇅';
ic.style.opacity = '.3';
});
icon.textContent = estado.asc ? '↑' : '↓';
icon.style.opacity = '1';
icon.style.color = 'var(--accent)';
filas.sort(function(a, b) {
var celdaA = a.querySelectorAll('td')[idx];
var celdaB = b.querySelectorAll('td')[idx];
if (!celdaA || !celdaB) return 0;
var valA = celdaA.innerText.trim().toLowerCase();
var valB = celdaB.innerText.trim().toLowerCase();
// Detectar si es número
var numA = parseFloat(valA.replace(/[^0-9.-]/g, ''));
var numB = parseFloat(valB.replace(/[^0-9.-]/g, ''));
var esNumero = !isNaN(numA) && !isNaN(numB);
// Detectar si es fecha (formato YYYY-MM-DD o DD-MM-YYYY)
var fechaRegex = /^\d{2}[-\/]\d{2}[-\/]\d{4}$|^\d{4}[-\/]\d{2}[-\/]\d{2}$/;
var esFecha = fechaRegex.test(celdaA.innerText.trim());
var resultado;
if (esNumero) {
resultado = numA - numB;
} else if (esFecha) {
// Normalizar DD-MM-YYYY a YYYY-MM-DD para comparar
var normA = valA.replace(/(\d{2})-(\d{2})-(\d{4})/, '$3-$2-$1');
var normB = valB.replace(/(\d{2})-(\d{2})-(\d{4})/, '$3-$2-$1');
resultado = normA.localeCompare(normB);
} else {
resultado = valA.localeCompare(valB, 'es');
}
return estado.asc ? resultado : -resultado;
});
// Reinsertar filas ordenadas
filas.forEach(function(fila) { tbody.appendChild(fila); });
});
});
})();
</script>
{% endblock %}
<!-- /* ── COPIAR funciona en localhost y https ── */
document.getElementById('btnCopiar') && document.getElementById('btnCopiar').addEventListener('click', function() {
var btn = this;
var heads = ['ID','M. Oficial','M. Civil','Marca','Modelo','Denominación','Tipo','Departamento','Fecha ITV','Estado'];
var lineas = [heads.join('\t')];
document.querySelectorAll('#tablaITV tbody tr').forEach(function(tr) {
var fila = [];
tr.querySelectorAll('td').forEach(function(td) { fila.push(td.innerText.trim()); });
lineas.push(fila.join('\t'));
});
navigator.clipboard.writeText(lineas.join('\n')).then(function() {
var orig = btn.innerHTML;
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Copiado';
btn.style.color = 'var(--success)'; btn.style.borderColor = 'var(--success)';
setTimeout(function(){ btn.innerHTML = orig; btn.style.color=''; btn.style.borderColor=''; }, 2000);
});
});