829 lines
16 KiB
HTML
Executable File
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 %}
|
|
```
|