Files
pyfleet/templates/tablas/unidades_tabulator.html
T
2026-09-03 19:40:44 +02:00

829 lines
16 KiB
HTML
Executable File

```html
{% extends "base.html" %}
{% block title %}Unidades{% endblock %}
{% block content %}
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/css/tabulator_bootstrap5.min.css"
>
<style>
/* CONTENEDOR TABULATOR */
#tabla-unidades {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
/* CABECERA */
.unidades-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.unidades-search {
position: relative;
width: 500px;
max-width: 100%;
}
.unidades-search svg {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 17px;
height: 17px;
color: var(--muted);
pointer-events: none;
}
.unidades-search input {
width: 100%;
padding: 9px 38px 9px 38px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text);
outline: none;
}
.unidades-search input:focus {
border-color: var(--primary);
}
.clear-search {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: none;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: 0;
background: transparent;
color: var(--muted);
cursor: pointer;
border-radius: 5px;
}
.clear-search:hover {
background: var(--border);
color: var(--text);
}
/* =====================================================
TABULATOR
===================================================== */
.tabulator {
border: 0 !important;
background: var(--surface);
font-size: 14px;
}
.tabulator .tabulator-header {
background: #f8f9fa;
border-bottom: 1px solid var(--border);
}
.tabulator .tabulator-header .tabulator-col {
background: transparent;
border-right: 0;
}
.tabulator .tabulator-row {
background: var(--surface);
border-bottom: 1px solid var(--border);
min-height: 48px;
}
.tabulator .tabulator-row:hover {
background: rgba(0,0,0,.025);
}
/* .tabulator .tabulator-cell {
display: flex;
align-items: center;
border-right: 0;
} */
/* ESTADO */
#estadoTabla {
font-size: 12px;
color: var(--muted);
margin-top: 8px;
min-height: 18px;
}
/* =====================================================
RESPONSIVE
===================================================== */
@media (max-width: 700px) {
.unidades-toolbar {
flex-direction: column;
align-items: stretch;
}
.unidades-search {
width: 100%;
}
}
</style>
<!-- =========================================================
CABECERA
========================================================== -->
<div class="page-header">
<div class="page-eyebrow">
Configuración
</div>
<div class="page-title">
<i class="bi bi-building"></i>
Unidades
</div>
<div class="page-subtitle">
Gestiona las unidades a las que se asignan los vehículos.
</div>
</div>
<div style="
display:flex;
gap:24px;
align-items:flex-start;
flex-wrap:wrap;
">
<!-- =====================================================
FORMULARIO NUEVA UNIDAD
====================================================== -->
<div style="
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
padding:24px;
min-width:400px;
">
<p
class="form-label"
style="margin-bottom:14px;"
>
Nueva unidad
</p>
<form
method="POST"
action="{{ url_for('unidades_nueva') }}"
>
<!-- POBLACIÓN -->
<div class="form-group">
<label class="form-label">
Población
</label>
<div class="select-wrap">
<select
class="form-input form-select"
name="poblacion_id"
required
>
<option value="">
— Selecciona población —
</option>
{% for p in poblaciones %}
<option value="{{ p['id'] }}">
{{ p['poblacion'] }}
</option>
{% endfor %}
</select>
<svg
class="select-arrow"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
>
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
</div>
<!-- UNIDAD -->
<div class="form-group">
<label class="form-label">
Unidad
</label>
<input
class="form-input"
type="text"
name="unidad"
placeholder="Nombre de la unidad"
required
>
</div>
<button
type="submit"
class="btn btn-sm btn-primary"
style="
width:50%;
margin-top:12px;
display:block;
margin-left:auto;
margin-right:auto;
"
>
✚ Añadir
</button>
</form>
</div>
<!-- =====================================================
LISTADO
====================================================== -->
<div style="
flex:1;
min-width:320px;
max-width:800px;
">
<!-- TOOLBAR -->
<div class="unidades-toolbar">
<!-- BUSCADOR -->
<div class="unidades-search">
<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"
id="searchUnidades"
placeholder="Buscar por población o unidad…"
autocomplete="off"
autofocus
>
<button
type="button"
id="clearSearch"
class="clear-search"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
>
<line
x1="18"
y1="6"
x2="6"
y2="18"
/>
<line
x1="6"
y1="6"
x2="18"
y2="18"
/>
</svg>
</button>
</div>
</div>
<!-- CONTADOR -->
<div
id="contador"
style="
margin:8px 0;
font-size:12px;
color:var(--muted);
"
>
{{ rows|length }}
unidad{% if rows|length != 1 %}es{% endif %}
</div>
<!-- TABLA -->
<div id="tabla-unidades"></div>
<!-- ESTADO -->
<div id="estadoTabla"></div>
</div>
</div>
<!-- DATOS PARA TABULATOR=============================== -->
<script>
const unidades = [
{% for r in rows %}
{
id: {{ r['id'] | tojson }},
poblacion_id:
{{ r['poblacion_id'] | tojson }},
poblacion:
{{ (r['poblacion'] or '') | tojson }},
unidad:
{{ r['unidad'] | tojson }}
}
{% if not loop.last %},{% endif %}
{% endfor %}
];
const poblaciones = [
{% for p in poblaciones %}
{
id: {{ p['id'] | tojson }},
nombre:
{{ p['poblacion'] | tojson }}
}
{% if not loop.last %},{% endif %}
{% endfor %}
];
</script>
<!-- TABULATOR -->
<script
src="https://unpkg.com/[email protected]/dist/js/tabulator.min.js">
</script>
<script>
// =========================================================
// ELEMENTOS
// =========================================================
const buscador =
document.getElementById(
"searchUnidades"
);
const clearSearch =
document.getElementById(
"clearSearch"
);
const contador =
document.getElementById(
"contador"
);
const estadoTabla =
document.getElementById(
"estadoTabla"
);
// =========================================================
// LISTA DE POBLACIONES PARA EL SELECT
// =========================================================
const opcionesPoblaciones = {};
poblaciones.forEach(function(p) {
opcionesPoblaciones[p.id] =
p.nombre;
});
// =========================================================
// CONTADOR
// =========================================================
function actualizarContador() {
const cantidad =
tabla.getDataCount("active");
contador.textContent =
cantidad +
" unidad" +
(cantidad !== 1 ? "es" : "");
}
// =========================================================
// MENSAJE
// =========================================================
function mostrarEstado(texto, error = false) {
estadoTabla.textContent = texto;
estadoTabla.style.color =
error
? "#dc3545"
: "#198754";
setTimeout(function() {
estadoTabla.textContent = "";
}, 3000);
}
// TABULATOR
const tabla = new Tabulator(
"#tabla-unidades",
{
data: unidades,
layout: "fitColumns",
height: "500px",
placeholder:
"Sin unidades registradas.",
editTriggerEvent: "click",
columns: [
{ title: "ID",
field: "id",
visible: false
},
{ title: "Población",
field: "poblacion_id",
minWidth: 220,
editor: "list",
editorParams: {
values:
opcionesPoblaciones,
autocomplete: true,
listOnEmpty: true
},
formatter: function(cell) {
const id =
cell.getValue();
return opcionesPoblaciones[id]
|| "—";
}
},
{
title: "Unidad",
field: "unidad",
editor: "input",
validator: "required",
minWidth: 250,
formatter: function(cell) {
const valor =
cell.getValue();
return valor
? `<strong>${escapeHtml(valor)}</strong>`
: "";
}
}
]
}
);
// ESCAPAR HTML
function escapeHtml(text) {
const div =
document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// EDICIÓN DE CELDA
tabla.on(
"cellEdited",
async function(cell) {
const fila =
cell.getRow();
const datos =
fila.getData();
const campo =
cell.getField();
const valor =
cell.getValue();
console.log(
"Edición:",
{
id: datos.id,
campo: campo,
valor: valor
}
);
// Guardar en Flask
try {
const respuesta =
await fetch(
`/api/unidades/${datos.id}`,
{
method: "PATCH",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
campo: campo,
valor: valor
})
}
);
const resultado =
await respuesta.json();
if (!respuesta.ok) {
throw new Error(
resultado.error ||
"No se pudo guardar"
);
}
// Actualizar fila con respuesta de Flask
await fila.update({
id: resultado.id,
poblacion_id: resultado.poblacion_id,
poblacion: resultado.poblacion,
unidad: resultado.unidad
});
mostrarEstado(
"✓ Cambios guardados"
);
}
catch (error) {
console.error(
"Error:",
error
);
mostrarEstado(
error.message,
true
);
// Volvemos a obtener los datos originales
// desde el servidor.
tabla.replaceData(
unidades
);
}
}
);
// BUSCADOR
buscador.addEventListener(
"input",
function() {
const texto =
this.value
.trim()
.toLowerCase();
clearSearch.style.display =
texto
? "flex"
: "none";
if (!texto) {
tabla.clearFilter();
actualizarContador();
return;
}
tabla.setFilter(
function(data) {
const poblacion =
String(
data.poblacion || ""
).toLowerCase();
const unidad =
String(
data.unidad || ""
).toLowerCase();
return (
poblacion.includes(texto) ||
unidad.includes(texto)
);
}
);
setTimeout(
actualizarContador,
0
);
}
);
// LIMPIAR BUSCADOR
clearSearch.addEventListener(
"click",
function() {
buscador.value = "";
clearSearch.style.display =
"none";
tabla.clearFilter();
buscador.focus();
actualizarContador();
}
);
// CONTADOR INICIAL
actualizarContador();
</script>
{% endblock %}
```