457 lines
18 KiB
HTML
Executable File
457 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">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="margin-left:auto;display:flex;gap:8px;">
|
|
<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. Civil</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_civil'] 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. Civil','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. Civil','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);
|
|
});
|
|
}); |