git pyfleet en gitea.xiberus
This commit is contained in:
Executable
+457
@@ -0,0 +1,457 @@
|
||||
{% 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user