392 lines
14 KiB
HTML
Executable File
392 lines
14 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
{% block title %}Vehículos{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
|
|
<div class="page-header">
|
|
<div class="page-eyebrow">Base de datos</div>
|
|
<div class="page-title">Vehículos</div>
|
|
<div class="page-subtitle">Gestión del parque móvil. Búsqueda por m.oficial, m.civil, VIN, marca, modelo, poblacion, unidad, denominación, tipo, operacion, situacion.</div>
|
|
</div>
|
|
|
|
<div class="crud-toolbar">
|
|
<form method="GET" action="{{ url_for('vehiculos_list') }}" class="search-wrap" id="searchForm">
|
|
<svg 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>
|
|
<input
|
|
type="text"
|
|
name="q"
|
|
id="searchInput"
|
|
placeholder="Buscar… palabras separadas por comas: Ford, Transit"
|
|
value="{{ q }}"
|
|
autocomplete="off"
|
|
autofocus
|
|
style="width: 400px;"
|
|
>
|
|
|
|
<button type="button" id="clearBtn" title="Limpiar búsqueda"
|
|
style="display:{{ 'flex' if q else 'none' }};position:absolute;right:-25px;top:50%;
|
|
transform:translateY(-50%);background:none;border:none;cursor:pointer;
|
|
color:var(--muted);padding:4px;align-items:center;transition:color .15s;
|
|
z-index:2;"
|
|
onmouseover="this.style.color='var(--text)'"
|
|
onmouseout="this.style.color='var(--muted)'">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
|
|
style="pointer-events:none;display:block;">
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
|
|
{# Casilla incluir bajas #}
|
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;
|
|
font-size:13px;color:var(--muted);white-space:nowrap;
|
|
padding:8px 12px;border:1px solid var(--border);border-radius:8px;
|
|
transition:border-color .15s,color .15s;user-select:none; margin-left: 50px;"
|
|
onmouseover="this.style.borderColor='var(--accent)';this.style.color='var(--text)'"
|
|
onmouseout="this.style.borderColor='var(--border)';this.style.color='var(--muted)'">
|
|
<input type="checkbox" id="inclBajas" name="incl_bajas"
|
|
style="width:15px;height:15px;accent-color:var(--accent);cursor:pointer;"
|
|
{% if incl_bajas %}checked{% endif %}>
|
|
Incluir bajas
|
|
</label>
|
|
|
|
<div style="display:flex;gap:8px;align-items:center;margin-left:auto;">
|
|
<a href="{{ url_for('vehiculos_exportar', q=q) }}" class="btn btn-ghost" id="btnExcel" 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 / Guardar 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>
|
|
|
|
|
|
<a href="{{ url_for('vehiculos_nuevo') }}" class="btn btn-primary">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
Nuevo vehículo
|
|
</a>
|
|
</div>
|
|
|
|
{# Sin {% if rows %} - siempre visible #}
|
|
<p id="counter" style="margin-bottom:10px;margin-top:1px;font-size:12px;color:var(--muted)">
|
|
{{ rows|length }} registro{{ 's' if rows|length != 1 }}{% if q %} — filtrado por «{{ q }}»{% endif %}
|
|
</p>
|
|
|
|
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<!-- <th hidden>ID</th> -->
|
|
<th>M. Oficial</th>
|
|
<th>M. Civil</th>
|
|
<th>Marca</th>
|
|
<th>Modelo</th>
|
|
<th>Poblacion</th>
|
|
<th>Unidad</th>
|
|
<!-- <th>Grupo</th> -->
|
|
<th>Tipo vehículo</th>
|
|
<th>Situación</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tbody">
|
|
{% include 'vehiculos/rows.html' %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
|
|
<script>
|
|
var input = document.getElementById('searchInput');
|
|
var tbody = document.getElementById('tbody');
|
|
var counter = document.getElementById('counter');
|
|
var clearBtn = document.getElementById('clearBtn');
|
|
var timer;
|
|
|
|
// Inicializar contador al cargar la página
|
|
(function() {
|
|
var filas = document.querySelectorAll('#tbody tr[data-id]').length;
|
|
var q = input.value.trim();
|
|
if (counter) {
|
|
counter.textContent = filas + ' registro' + (filas !== 1 ? 's' : '')
|
|
+ (q ? ' — filtrado por «' + q + '»' : '');
|
|
}
|
|
})();
|
|
|
|
// Búsqueda con debounce
|
|
input.addEventListener('input', function() {
|
|
clearBtn.style.display = this.value ? 'flex' : 'none';
|
|
clearTimeout(timer);
|
|
timer = setTimeout(function() { buscar(input.value.trim()); }, 300);
|
|
});
|
|
|
|
clearBtn.addEventListener('click', function() {
|
|
input.value = '';
|
|
clearBtn.style.display = 'none';
|
|
input.focus();
|
|
buscar('');
|
|
});
|
|
|
|
var inclBajas = document.getElementById('inclBajas');
|
|
|
|
// Al cambiar el checkbox, relanzar búsqueda
|
|
inclBajas.addEventListener('change', function() {
|
|
buscar(input.value.trim());
|
|
});
|
|
|
|
function buscar(q) {
|
|
var bajas = inclBajas.checked ? '1' : '0';
|
|
var btnExcel = document.getElementById('btnExcel');
|
|
|
|
if (btnExcel) {
|
|
btnExcel.href = '{{ url_for("vehiculos_exportar") }}?q=' + encodeURIComponent(q) + '&incl_bajas=' + bajas;;
|
|
}
|
|
|
|
var url = '{{ url_for("vehiculos_list") }}?q='
|
|
+ encodeURIComponent(q) + '&solo_filas=1&incl_bajas=' + bajas;
|
|
|
|
fetch(url)
|
|
.then(function(r) {
|
|
// Si el servidor devuelve 302 o la URL cambió, la sesión expiró
|
|
if (r.redirected || r.url.includes('/login')) {
|
|
window.location = '{{ url_for("login") }}';
|
|
return null;
|
|
}
|
|
return r.text();
|
|
})
|
|
.then(function(html) {
|
|
if (!html) return;
|
|
tbody.innerHTML = html;
|
|
var filas = document.querySelectorAll('#tbody tr[data-id]').length;
|
|
if (counter) {
|
|
counter.textContent = filas + ' registro' + (filas !== 1 ? 's' : '')
|
|
+ (q ? ' — filtrado por «' + q + '»' : '');
|
|
counter.style.display = '';
|
|
}
|
|
})
|
|
.catch(function() {
|
|
window.location = '{{ url_for("login") }}';
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/* ── PDF ── */
|
|
document.getElementById('btnPDF').addEventListener('click', function() {
|
|
var q = input.value.trim();
|
|
var titulo = 'Listado de vehículos' + (q ? ' — Filtro: ' + q : '');
|
|
var fecha = new Date().toLocaleDateString('es-ES', {day:'2-digit', month:'2-digit', year:'numeric'});
|
|
|
|
// Construir tabla con los datos visibles
|
|
var heads = [];
|
|
document.querySelectorAll('thead th').forEach(function(th, i) {
|
|
if (i < 9) heads.push(th.innerText.trim());
|
|
});
|
|
|
|
var filas = '';
|
|
tbody.querySelectorAll('tr[data-id]').forEach(function(tr) {
|
|
var celdas = tr.querySelectorAll('td');
|
|
filas += '<tr>';
|
|
for (var i = 0; i < 10; i++) {
|
|
filas += '<td>' + (celdas[i] ? 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: 11px; color: #1a1d29; margin: 24px; }
|
|
h2 { font-size: 15px; margin-bottom: 4px; }
|
|
.meta { font-size: 10px; color: #666; margin-bottom: 16px; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th { background: #5c6ef8; color: #fff; padding: 7px 8px; text-align: left; font-size: 10px; letter-spacing: .5px; }
|
|
td { padding: 6px 8px; border-bottom: 1px solid #e3e6ef; font-size: 10px; }
|
|
tr:nth-child(even) td { background: #eef0f8; }
|
|
.badge { background: #eef0f8; color: #5c6ef8; padding: 2px 7px; border-radius: 20px;
|
|
border: 1px solid rgba(92,110,248,.3); font-size: 9px; font-weight: 700; }
|
|
@media print { body { margin: 12px; } }
|
|
</style></head><body>
|
|
<h2>${titulo}</h2>
|
|
<div class="meta">Generado el ${fecha} · ${tbody.querySelectorAll('tr[data-id]').length} registros</div>
|
|
<table>
|
|
<thead><tr>${heads.map(function(h){ return '<th>'+h+'</th>'; }).join('')}</tr></thead>
|
|
<tbody>${filas}</tbody>
|
|
</table>
|
|
</body></html>`;
|
|
|
|
var win = window.open('', '_blank', 'width=900,height=650');
|
|
win.document.write(html);
|
|
win.document.close();
|
|
win.focus();
|
|
win.print();
|
|
win.close();
|
|
});
|
|
|
|
/* ── COPIAR ── */
|
|
document.getElementById('btnCopiar').addEventListener('click', function() {
|
|
var btn = this;
|
|
|
|
// Leer filas EN EL MOMENTO del click, no antes
|
|
var heads = ['M. Oficial','M. Civil','Marca','Modelo','Poblacion','Unidad','Grupo','Denominación','Situación'];
|
|
var lineas = [heads.join('\t')];
|
|
|
|
document.querySelectorAll('#tbody tr[data-id]').forEach(function(tr) {
|
|
var fila = [];
|
|
tr.querySelectorAll('td').forEach(function(td) {
|
|
fila.push(td.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); // Para móviles
|
|
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 %}
|
|
|