git pyfleet en gitea.xiberus
This commit is contained in:
Executable
+872
@@ -0,0 +1,872 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>{% block title %}App{% endblock %} · PyFleet</title>
|
||||
<script>
|
||||
(function() {
|
||||
var theme = localStorage.getItem('nexus-theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="static/css/animate.min.css"/>
|
||||
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='css/animate.css') }}"> -->
|
||||
<!-- <link rel="stylesheet" type="text/css" href="DataTables/DataTables-1.11.4/css/dataTables.bootstrap5.css"/> -->
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> -->
|
||||
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d0f14;
|
||||
--surface: #13161e;
|
||||
--nav-bg: #0a0c10;
|
||||
--border: #1e2230;
|
||||
--accent: #5c6ef8;
|
||||
--accent2: #8b5cf6;
|
||||
--text: #e2e6f0;
|
||||
--muted: #5a607a;
|
||||
--pill-bg: #1a1f2e;
|
||||
--success: #34d399;
|
||||
--nav-w: 220px;
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg: #f5f6fa;
|
||||
--surface: #ffffff;
|
||||
--nav-bg: #ffffff;
|
||||
--border: #e3e6ef;
|
||||
--accent: #5c6ef8;
|
||||
--accent2: #8b5cf6;
|
||||
--text: #1a1d29;
|
||||
--muted: #8a8fa3;
|
||||
--pill-bg: #eef0f8;
|
||||
--success: #10b981;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
transition: background .2s ease, color .2s ease;
|
||||
}
|
||||
|
||||
.input-disabled-look {
|
||||
background-color: #f5f5f5;
|
||||
color: #6c757d;
|
||||
border: 1px solid #ced4da;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
nav, .card, .msg-banner, .msg-form input, .nav-links a {
|
||||
transition: background .2s ease, border-color .2s ease, color .2s ease;
|
||||
}
|
||||
|
||||
/* ── NAVBAR ── */
|
||||
nav {
|
||||
width: var(--nav-w);
|
||||
background: var(--nav-bg);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 28px 16px 24px;
|
||||
position: fixed;
|
||||
top: 0; left: 0; bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.5px;
|
||||
padding: 0 8px 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.nav-logo span.dot {
|
||||
width: 8px; height: 8px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 8px var(--accent);
|
||||
}
|
||||
|
||||
.nav-section-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
padding: 0 8px 10px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.nav-links a:hover { background: var(--border); color: var(--text); }
|
||||
.nav-links a.active {
|
||||
background: linear-gradient(135deg, rgba(92,110,248,.18), rgba(139,92,246,.1));
|
||||
color: var(--text);
|
||||
border: 1px solid rgba(92,110,248,.25);
|
||||
}
|
||||
.nav-links a .icon {
|
||||
width: 18px; height: 18px;
|
||||
opacity: .6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-links a.active .icon { opacity: 1; }
|
||||
|
||||
/* indicator bar */
|
||||
.nav-links a.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
width: 3px; height: 18px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
.nav-links a { position: relative; }
|
||||
|
||||
/* message badge in nav */
|
||||
.msg-badge {
|
||||
margin-left: auto;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* ── THEME SWITCH ── */
|
||||
.theme-switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 8px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.theme-switch-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 23px;
|
||||
background: var(--pill-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.theme-toggle::after {
|
||||
content: '☾';
|
||||
position: absolute;
|
||||
top: 1px; left: 1px;
|
||||
width: 17px; height: 17px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
transition: transform .25s cubic-bezier(.4,0,.2,1), background .2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
}
|
||||
[data-theme="light"] .theme-toggle::after {
|
||||
content: '☀';
|
||||
transform: translateX(19px);
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
/* ── MAIN CONTENT ── */
|
||||
main {
|
||||
margin-left: var(--nav-w);
|
||||
flex: 1;
|
||||
padding: 40px 48px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.page-eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.page-title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.page-subtitle {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ── MENSAJES MÚLTIPLES ── */
|
||||
.msg-banner-wrap {
|
||||
background: linear-gradient(135deg, rgba(92,110,248,.1), rgba(139,92,246,.06));
|
||||
border: 1px solid rgba(92,110,248,.25);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
animation: slideIn .25s ease;
|
||||
}
|
||||
.msg-banner-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid rgba(92,110,248,.15);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .7px;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
.msg-clear-all {
|
||||
background: none;
|
||||
border: 1px solid rgba(92,110,248,.3);
|
||||
border-radius: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
transition: color .15s, border-color .15s;
|
||||
}
|
||||
.msg-clear-all:hover { color: var(--text); border-color: var(--muted); }
|
||||
.msg-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid rgba(92,110,248,.08);
|
||||
gap: 12px;
|
||||
}
|
||||
.msg-item:last-child { border-bottom: none; }
|
||||
.msg-fecha {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.msg-texto { font-size: 13px; color: var(--text); }
|
||||
.msg-del-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
transition: color .15s, background .15s;
|
||||
}
|
||||
.msg-del-btn:hover { color: #f87171; background: rgba(248,113,113,.1); }
|
||||
|
||||
|
||||
/* ── MESSAGE BANNER ── */
|
||||
.msg-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
background: linear-gradient(135deg, rgba(92,110,248,.12), rgba(139,92,246,.08));
|
||||
border: 1px solid rgba(92,110,248,.3);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 28px;
|
||||
animation: slideIn .25s ease;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from { opacity:0; transform: translateY(-6px); }
|
||||
to { opacity:1; transform: translateY(0); }
|
||||
}
|
||||
.msg-banner .label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .8px;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.msg-banner .text { color: var(--text); font-size: 14px; }
|
||||
.msg-banner a.clear-btn {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
transition: color .15s, border-color .15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.msg-banner a.clear-btn:hover { color: var(--text); border-color: var(--muted); }
|
||||
|
||||
/* ── MESSAGE FORM ── */
|
||||
.msg-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 28px;
|
||||
max-width: 560px;
|
||||
}
|
||||
.msg-form input {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
outline: none;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.msg-form input::placeholder { color: var(--muted); }
|
||||
.msg-form input:focus { border-color: var(--accent); }
|
||||
.msg-form button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
transition: opacity .15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.msg-form button:hover { opacity: .85; }
|
||||
|
||||
/* ── CARDS ── */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.card:hover { border-color: rgba(92,110,248,.35); }
|
||||
.card-icon {
|
||||
font-size: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.card-desc { font-size: 13px; color: var(--muted); line-height: 1.5; }
|
||||
|
||||
/* ── SELECT / DROPDOWN ── */
|
||||
.select-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.form-select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
cursor: pointer;
|
||||
padding-right: 38px;
|
||||
}
|
||||
.form-select option {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--muted);
|
||||
pointer-events: none;
|
||||
transition: transform .2s;
|
||||
}
|
||||
.select-wrap:focus-within .select-arrow {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
color: var(--accent);
|
||||
}
|
||||
.form-select:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
thead th {
|
||||
position: relative;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
thead th:hover {
|
||||
background: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
thead th:hover .sort-icon {
|
||||
opacity: .7 !important;
|
||||
}
|
||||
|
||||
|
||||
/* ── CRUD TABLE ── */
|
||||
.crud-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.search-wrap > svg:first-child {
|
||||
/* .search-wrap svg { */
|
||||
position: absolute;
|
||||
left: 12px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px; height: 16px;
|
||||
color: var(--muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-wrap input {
|
||||
width: 100%;
|
||||
padding: 9px 14px 9px 38px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.search-wrap input::placeholder { color: var(--muted); }
|
||||
.search-wrap input:focus { border-color: var(--accent); }
|
||||
|
||||
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 9px 18px; border-radius: 8px; font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 600; cursor: pointer; border: none; text-decoration: none; transition: opacity .15s, background .15s; }
|
||||
.btn-primary { background: var(--accent); color: #fff; }
|
||||
.btn-primary:hover { opacity: .85; }
|
||||
.btn-ghost { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
|
||||
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.btn-danger { background: transparent; color: #f87171; border: 1px solid rgba(248,113,113,.35); }
|
||||
.btn-danger:hover { background: rgba(248,113,113,.08); }
|
||||
.btn-sm { padding: 5px 12px; font-size: 12px; }
|
||||
|
||||
.table-wrap {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead tr { background: var(--pill-bg); }
|
||||
th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .8px;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
td {
|
||||
padding: 13px 16px;
|
||||
font-size: 13px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
tbody tr { transition: background .12s; padding-bottom: }
|
||||
tbody tr:hover { background: var(--pill-bg); }
|
||||
.td-id { font-family: monospace; color: var(--muted); font-size: 12px; }
|
||||
.td-actions { display: flex; gap: 8px; align-items: center; }
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.empty-state svg { width: 40px; height: 40px; margin-bottom: 12px; opacity: .3; }
|
||||
|
||||
/* ── FORM ── */
|
||||
.form-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 32px;
|
||||
max-width: 1560px; /* aqui se establece el ancho de los formularios */
|
||||
}
|
||||
|
||||
|
||||
.form-group { margin-bottom: 5px; }
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .5px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 7px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
outline: none;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
|
||||
.nav-submenu-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.nav-submenu-toggle:hover,
|
||||
.nav-submenu-toggle.active { background: var(--border); color: var(--text); }
|
||||
.nav-submenu-items {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding-left: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.nav-submenu-items.open { display: flex; }
|
||||
.nav-submenu-items a {
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
|
||||
.form-input::placeholder { color: var(--muted); }
|
||||
.form-input:focus { border-color: var(--accent); }
|
||||
.form-input:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.form-actions { display: flex; gap: 10px; margin-top: 28px; }
|
||||
|
||||
/* ── FLASH ── */
|
||||
.flash { padding: 11px 16px; border-radius: 8px; font-size: 13px; margin-bottom: 20px; animation: slideIn .2s ease; }
|
||||
.flash.ok { background: rgba(52,211,153,.12); border: 1px solid rgba(52,211,153,.3); color: #34d399; }
|
||||
.flash.error { background: rgba(248,113,113,.1); border: 1px solid rgba(248,113,113,.3); color: #f87171; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<!-- class="animate__animated animate__backInLeft" -->
|
||||
|
||||
<nav id="navbar"
|
||||
{% if session.pop("animate_navbar", None) %}
|
||||
class="animate__animated animate__backInUp"
|
||||
{% endif %}>
|
||||
|
||||
|
||||
<div style="padding: 0 8px 20px; text-align:center;">
|
||||
<img src="{{ url_for('static', filename='img/logo.png') }}"
|
||||
alt="Logo"
|
||||
style="max-width:160px; width:100%; height:auto;">
|
||||
</div>
|
||||
|
||||
<div class="nav-logo">
|
||||
<span class="dot"></span>PyFleet
|
||||
<!-- <span class="theme-switch-label">Fleet Operations Platform</span> -->
|
||||
</div>
|
||||
|
||||
<div class="theme-switch-row">
|
||||
<span class="theme-switch-label">Tema</span>
|
||||
<button class="theme-toggle" id="themeToggle" aria-label="Cambiar tema claro/oscuro" type="button"></button>
|
||||
</div>
|
||||
|
||||
<div class="nav-section-label">Navegación</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="{{ url_for('index') }}" class="{% if page == 'inicio' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
Inicio
|
||||
<!-- {% if session.get('mensaje') and page == 'inicio' %}<span class="msg-badge">!</span>{% endif %} -->
|
||||
</a>
|
||||
|
||||
<!-- <a href="{{ url_for('explorar') }}" class="{% if page == 'explorar' %}active{% endif %}">
|
||||
<svg class="icon" 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>
|
||||
Explorar
|
||||
{% if session.get('mensaje') and page == 'explorar' %}<span class="msg-badge">!</span>{% endif %}
|
||||
</a> -->
|
||||
|
||||
|
||||
<a href="{{ url_for('vehiculos_list') }}" class="{% if page == 'vehiculos' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 17H3a2 2 0 01-2-2V9a2 2 0 012-2h11l4 4v4a2 2 0 01-2 2h-1"/><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/>
|
||||
</svg>
|
||||
Vehículos
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('itv_list') }}" class="{% if page == 'itv' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
ITV
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('viat_list') }}" class="{% if page == 'viat' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="5" y="7" width="10" height="10" rx="2"/>
|
||||
<circle cx="10" cy="12" r="1"/>
|
||||
<path d="M17 9c1.7 1.7 1.7 4.3 0 6"/>
|
||||
<path d="M19.5 6.5c3 3 3 8 0 11"/>
|
||||
</svg>
|
||||
Via-T
|
||||
</a>
|
||||
|
||||
<!-- <a href="{{ url_for('campus_list') }}" class="{% if page == 'campus' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/>
|
||||
</svg>
|
||||
Campus
|
||||
</a> -->
|
||||
|
||||
<a href="{{ url_for('contacto') }}" class="{% if page == 'contacto' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
|
||||
</svg>
|
||||
Contacto
|
||||
{% if session.get('mensaje') and page == 'contacto' %}<span class="msg-badge">!</span>{% endif %}
|
||||
</a>
|
||||
|
||||
|
||||
<div class="nav-submenu" style="margin-top: 100px;">
|
||||
<hr style="opacity: 0.5;margin-bottom: 5px;">
|
||||
<button class="nav-submenu-toggle {% if page in ['poblaciones','unidades','tipo'] %}active{% endif %}"
|
||||
onclick="toggleSubmenu(this)">
|
||||
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" style="width: 15px;" stroke="currentColor" stroke-width="2">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
||||
</svg>
|
||||
Configuración
|
||||
<svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="margin-top:10px;margin-left:auto;transition:transform .2s">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="nav-submenu-items {% if page in ['poblaciones','unidades','tipo'] %}open{% endif %}">
|
||||
<!-- <a href="{{ url_for('poblaciones_list') }}" class="{% if page == 'poblaciones' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>
|
||||
</svg>
|
||||
Poblaciones
|
||||
</a> -->
|
||||
<a href="{{ url_for('unidades_list') }}" class="{% if page == 'unidades' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
Unidades
|
||||
</a>
|
||||
<a href="{{ url_for('tipo_list') }}" class="{% if page == 'tipo' %}active{% endif %}">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>
|
||||
Tipo vehiculo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="nav-footer">
|
||||
{% if mensajes %}
|
||||
<a href="{{ url_for('index') }}" style="
|
||||
display:flex;align-items:center;gap:6px;
|
||||
color:var(--accent);text-decoration:none;font-size:12px;
|
||||
padding:6px 8px;border-radius:7px;transition:background .15s;
|
||||
margin-bottom:4px;"
|
||||
onmouseover="this.style.background='var(--border)'"
|
||||
onmouseout="this.style.background=''">
|
||||
💬 {{ mensajes|length }} mensaje{{ 's' if mensajes|length != 1 }} activo{{ 's' if mensajes|length != 1 }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span style="font-size:12px;padding:6px 8px;display:block;">Sin mensajes</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<a href="{{ url_for('logout') }}" style="
|
||||
display:flex;align-items:center;gap:8px;margin-top:12px;
|
||||
color:var(--muted);font-size:13px;text-decoration:none;
|
||||
padding:7px 8px;border-radius:7px;transition:background .15s,color .15s;
|
||||
" onmouseover="this.style.background='var(--border)';this.style.color='var(--text)'"
|
||||
onmouseout="this.style.background='';this.style.color='var(--muted)'">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
Cerrar sesión
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
|
||||
|
||||
{# Mensajes solo en página de inicio #}
|
||||
{% if page == 'inicio' %}
|
||||
{% if mensajes %}
|
||||
<div class="msg-banner-wrap">
|
||||
<div class="msg-banner-header">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
|
||||
</svg>
|
||||
<span>{{ mensajes|length }} mensaje{{ 's' if mensajes|length != 1 }}</span>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('mensajes_eliminar_todos') }}"
|
||||
onsubmit="return confirm('¿Eliminar todos los mensajes?')">
|
||||
<button type="submit" class="msg-clear-all">✕ Eliminar todos</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for m in mensajes %}
|
||||
<div class="msg-item">
|
||||
<div>
|
||||
<div class="msg-fecha">{{ m['fecha'] | fecha_es_hora }}</div>
|
||||
<div class="msg-texto">{{ m['texto'] }}</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('mensaje_eliminar', id=m['id']) }}">
|
||||
<button type="submit" class="msg-del-btn" title="Eliminar este mensaje">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
var toggle = document.getElementById('themeToggle');
|
||||
var html = document.documentElement;
|
||||
|
||||
function applyTheme(theme) {
|
||||
html.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('nexus-theme', theme);
|
||||
}
|
||||
|
||||
// Sync button state on load
|
||||
applyTheme(localStorage.getItem('nexus-theme') || 'light');
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
var current = html.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
||||
var next = current === 'light' ? 'dark' : 'light';
|
||||
applyTheme(next);
|
||||
});
|
||||
|
||||
function toggleSubmenu(btn) {
|
||||
var items = btn.nextElementSibling;
|
||||
items.classList.toggle('open');
|
||||
btn.querySelector('.chevron').style.transform =
|
||||
items.classList.contains('open') ? 'rotate(180deg)' : '';
|
||||
}
|
||||
</script>
|
||||
∫
|
||||
</body>
|
||||
</html>
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Editar' if campus else 'Nuevo' }} Campus{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Campus</div>
|
||||
<div class="page-title">{{ 'Editar campus' if campus else 'Nuevo campus' }}</div>
|
||||
<div class="page-subtitle">
|
||||
{{ 'Modifica los datos del campus #' ~ campus['id'] if campus else 'Completa el formulario para registrar un nuevo campus.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<form method="POST">
|
||||
|
||||
{% if campus %}
|
||||
<div class="form-group">
|
||||
<label class="form-label">ID</label>
|
||||
<input class="form-input" type="text" value="#{{ campus['id'] }}" disabled>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="nombre">Nombre *</label>
|
||||
<input
|
||||
class="form-input"
|
||||
type="text"
|
||||
id="nombre"
|
||||
name="nombre"
|
||||
placeholder="Ej. Campus Norte"
|
||||
value="{{ campus['nombre'] if campus else request.form.get('nombre','') }}"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="telefono">Teléfono *</label>
|
||||
<input
|
||||
class="form-input"
|
||||
type="tel"
|
||||
id="telefono"
|
||||
name="telefono"
|
||||
placeholder="Ej. +52 55 1234 5678"
|
||||
value="{{ campus['telefono'] if campus else request.form.get('telefono','') }}"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Email *</label>
|
||||
<input
|
||||
class="form-input"
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="Ej. [email protected]"
|
||||
value="{{ campus['email'] if campus else request.form.get('email','') }}"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="categoria_id">Categoría</label>
|
||||
<div class="select-wrap">
|
||||
<select class="form-input form-select" id="categoria_id" name="categoria_id">
|
||||
<option value="">— Sin categoría —</option>
|
||||
{% for cat in categorias %}
|
||||
<option value="{{ cat['id'] }}"
|
||||
{% if campus and campus['categoria_id'] == cat['id'] %}selected
|
||||
{% elif request.form.get('categoria_id') == cat['id']|string %}selected
|
||||
{% endif %}>
|
||||
{{ cat['tipo'] }}
|
||||
</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>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ '💾 Guardar cambios' if campus else '✚ Crear campus' }}
|
||||
</button>
|
||||
<a href="{{ url_for('campus_list') }}" class="btn btn-ghost">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Campus{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Base de datos</div>
|
||||
<div class="page-title">Campus</div>
|
||||
<div class="page-subtitle">Gestiona los registros de campus: búsqueda, alta, edición y baja.</div>
|
||||
</div>
|
||||
|
||||
<div class="crud-toolbar">
|
||||
<form method="GET" action="{{ url_for('campus_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 por ID, nombre, teléfono o email…"
|
||||
value="{{ q }}"
|
||||
autocomplete="off"
|
||||
>
|
||||
</form>
|
||||
<a href="{{ url_for('campus_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 campus
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>Teléfono</th>
|
||||
<th>Email</th>
|
||||
<th>Categoría</th>
|
||||
<th style="text-align:right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
{% if rows %}
|
||||
{% for r in rows %}
|
||||
<tr data-id="{{ r['id'] }}">
|
||||
<td class="td-id">#{{ r['id'] }}</td>
|
||||
<td><strong>{{ r['nombre'] }}</strong></td>
|
||||
<td>{{ r['telefono'] }}</td>
|
||||
<td>{{ r['email'] }}</td>
|
||||
<td>{% if r['categoria'] %}<span style="background:rgba(92,110,248,.12);color:var(--accent);font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(92,110,248,.25)">{{ r['categoria'] }}</span>{% else %}<span style="color:var(--muted);font-size:12px">—</span>{% endif %}</td>
|
||||
|
||||
<!-- <td>{{ r['categoria'] }}</td> -->
|
||||
<td>
|
||||
<div class="td-actions" style="justify-content:flex-end">
|
||||
<a href="{{ url_for('campus_editar', id=r['id']) }}" class="btn btn-ghost btn-sm">Editar</a>
|
||||
<form method="POST" action="{{ url_for('campus_eliminar', id=r['id']) }}"
|
||||
onsubmit="return confirm('¿Eliminar «{{ r['nombre'] }}»? Esta acción no se puede deshacer.')">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/>
|
||||
</svg>
|
||||
{% if q %}
|
||||
No se encontraron resultados para <strong>"{{ q }}"</strong>.
|
||||
<br><a href="{{ url_for('campus_list') }}" style="color:var(--accent);text-decoration:none">Ver todos</a>
|
||||
{% else %}
|
||||
No hay campus registrados aún.
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if rows %}
|
||||
<p id="counter" style="margin-top:12px;font-size:12px;color:var(--muted)">
|
||||
{{ rows|length }} registro{{ 's' if rows|length != 1 }}
|
||||
{% if q %} — filtrado por «{{ q }}»{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
var input = document.getElementById('searchInput');
|
||||
var tbody = document.getElementById('tbody');
|
||||
var counter = document.getElementById('counter');
|
||||
var timer;
|
||||
|
||||
/* Actualiza la tabla sin recargar la página (el foco no se pierde) */
|
||||
input.addEventListener('input', function() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function() { buscar(input.value.trim()); }, 300);
|
||||
});
|
||||
|
||||
function buscar(q) {
|
||||
var url = '{{ url_for("campus_list") }}?q=' + encodeURIComponent(q) + '&solo_filas=1';
|
||||
fetch(url)
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(html) {
|
||||
tbody.innerHTML = html;
|
||||
if (counter) {
|
||||
var filas = tbody.querySelectorAll('tr[data-id]').length;
|
||||
counter.textContent = filas + ' registro' + (filas !== 1 ? 's' : '')
|
||||
+ (q ? ' — filtrado por «' + q + '»' : '');
|
||||
counter.style.display = filas ? '' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
{% if rows %}
|
||||
{% for r in rows %}
|
||||
<tr data-id="{{ r['id'] }}">
|
||||
<td class="td-id">#{{ r['id'] }}</td>
|
||||
<td><strong>{{ r['nombre'] }}</strong></td>
|
||||
<td>{{ r['telefono'] }}</td>
|
||||
<td>{{ r['email'] }}</td>
|
||||
<td>{% if r['categoria'] %}<span style="background:rgba(92,110,248,.12);color:var(--accent);font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(92,110,248,.25)">{{ r['categoria'] }}</span>{% else %}<span style="color:var(--muted);font-size:12px">—</span>{% endif %}</td>
|
||||
<td>
|
||||
<div class="td-actions" style="justify-content:flex-end">
|
||||
<a href="/campus/{{ r['id'] }}/editar" class="btn btn-ghost btn-sm">Editar</a>
|
||||
<form method="POST" action="/campus/{{ r['id'] }}/eliminar"
|
||||
onsubmit="return confirm('¿Eliminar «{{ r['nombre'] }}»? Esta acción no se puede deshacer.')">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/>
|
||||
</svg>
|
||||
{% if q %}
|
||||
No se encontraron resultados para <strong>"{{ q }}"</strong>.
|
||||
{% else %}
|
||||
No hay campus registrados aún.
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Contacto{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<style>
|
||||
.card-row {
|
||||
display: grid;
|
||||
grid-template-columns: 70px 1fr;
|
||||
gap: 10px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.card-row .label {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Comunicación</div>
|
||||
<div class="page-title">Contacto</div>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card" style="width:300px;">
|
||||
<div class="card-icon">✉️</div>
|
||||
<div class="card-title">Correo electrónico</div>
|
||||
<div class="card-row card-desc"><span class="label">Oficina</span><span>[email protected]</span></div>
|
||||
<div class="card-row card-desc"><span class="label">Taller</span><span>[email protected]</span></div>
|
||||
<div class="card-row card-desc"><span class="label">Jefatura</span><span>malaga.jefeautomocion.es</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="width:300px; margin-left:50px;">
|
||||
<div class="card-icon">📞</div>
|
||||
<div class="card-title">Teléfono</div>
|
||||
<div class="card-row card-desc"><span class="label">Oficina</span><span>952 046 317</span></div>
|
||||
<div class="card-row card-desc"><span class="label">Taller</span><span>952 046 221</span></div>
|
||||
<div class="card-row card-desc"><span class="label">Jefatura</span><span>952 046 453</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Explorar{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Descubrimiento</div>
|
||||
<div class="page-title">Explorar contenido</div>
|
||||
<div class="page-subtitle">Navega entre secciones: el mensaje persiste en todas ellas.</div>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">🗺️</div>
|
||||
<div class="card-title">Descubre</div>
|
||||
<div class="card-desc">Encuentra recursos, artículos y más dentro de la plataforma.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📊</div>
|
||||
<div class="card-title">Estadísticas</div>
|
||||
<div class="card-desc">Visualiza datos y métricas de actividad reciente.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔖</div>
|
||||
<div class="card-title">Guardados</div>
|
||||
<div class="card-desc">Accede al contenido que marcaste como favorito.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🧩</div>
|
||||
<div class="card-title">Integraciones</div>
|
||||
<div class="card-desc">Conecta herramientas externas con tu flujo de trabajo.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="msg-form" method="POST">
|
||||
<input type="text" name="mensaje" placeholder="Escribe un mensaje…">
|
||||
<button type="submit">Enviar</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Inicio{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Panel principal</div>
|
||||
<div class="page-title">Bienvenido a PyFleet</div>
|
||||
<div class="page-subtitle">Gestión de flota.</div>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">🏠</div>
|
||||
<div class="card-title">Página de Inicio</div>
|
||||
<div class="card-desc">Desde el menu lateral puedes navegar a las distintas secciones.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔔</div>
|
||||
<div class="card-title">Notificaciones</div>
|
||||
<div class="card-desc">El mensaje informativo que escribas aparecerá en cualquier sección.</div>
|
||||
<div class="card-desc">La app mantiene los mensajes activos hasta que sean eliminados.</div>
|
||||
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">⚡</div>
|
||||
<div class="card-title">Escalable</div>
|
||||
<div class="card-desc">En la seccion CONFIGURACIÓN (Unidades - Tipo vehículo) podrás añadir o eliminar datos.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="msg-form" method="POST">
|
||||
<input type="text" name="mensaje" placeholder="Escribe un mensaje…" autofocus>
|
||||
<button type="submit">Enviar</button>
|
||||
</form>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
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);
|
||||
});
|
||||
});
|
||||
Executable
+298
@@ -0,0 +1,298 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Acceso · Fleet</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
(function() {
|
||||
var t = localStorage.getItem('nexus-theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #f5f6fa;
|
||||
--surface: #ffffff;
|
||||
--border: #e3e6ef;
|
||||
--accent: #5c6ef8;
|
||||
--text: #1a1d29;
|
||||
--muted: #8a8fa3;
|
||||
--error-bg: rgba(248,113,113,.1);
|
||||
--error-brd: rgba(248,113,113,.35);
|
||||
--error-txt: #ef4444;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg: #0d0f14;
|
||||
--surface: #13161e;
|
||||
--border: #1e2230;
|
||||
--text: #e2e6f0;
|
||||
--muted: #5a607a;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
transition: background .2s, color .2s;
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
text-align: center;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.login-logo .dot-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.login-logo .dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
.login-logo .dot:nth-child(2) { opacity: .5; transform: scale(.8); }
|
||||
.login-logo .dot:nth-child(3) { opacity: .25; transform: scale(.6); }
|
||||
.login-logo h1 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.login-logo p {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .7px;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pwd-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.pwd-wrap input {
|
||||
width: 100%;
|
||||
padding: 11px 42px 11px 14px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: border-color .15s;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.pwd-wrap input::placeholder { letter-spacing: 0; color: var(--muted); }
|
||||
.pwd-wrap input:focus { border-color: var(--accent); }
|
||||
|
||||
.toggle-pwd {
|
||||
position: absolute;
|
||||
right: 12px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.toggle-pwd:hover { color: var(--text); }
|
||||
|
||||
.error-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-brd);
|
||||
color: var(--error-txt);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 18px;
|
||||
animation: shake .3s ease;
|
||||
}
|
||||
@keyframes shake {
|
||||
0%,100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-6px); }
|
||||
75% { transform: translateX(6px); }
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.btn-login:hover { opacity: .88; }
|
||||
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
transition: color .15s;
|
||||
}
|
||||
.theme-btn:hover { color: var(--text); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div class="login-wrap">
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div style="
|
||||
display:flex;align-items:center;gap:8px;
|
||||
background:rgba({% if category == 'error' %}248,113,113{% else %}52,211,153{% endif %},.12);
|
||||
border:1px solid rgba({% if category == 'error' %}248,113,113{% else %}52,211,153{% endif %},.35);
|
||||
color:{% if category == 'error' %}#f87171{% else %}#34d399{% endif %};
|
||||
border-radius:8px;padding:10px 14px;font-size:13px;margin-bottom:18px;">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
<div class="login-logo">
|
||||
<div class="dot-row">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<h1>PyFleet</h1>
|
||||
<p>Fleet Operations Platform</p>
|
||||
<p>Introduce la contraseña para continuar</p>
|
||||
</div>
|
||||
|
||||
<div class="login-card">
|
||||
{% if error %}
|
||||
<div class="error-msg">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<label class="field-label" for="password">Contraseña</label>
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<div class="pwd-wrap">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
>
|
||||
<button type="button" class="toggle-pwd" id="togglePwd" aria-label="Mostrar contraseña">
|
||||
<svg id="eyeIcon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" class="btn-login">Entrar</button>
|
||||
</form>
|
||||
|
||||
<button class="theme-btn" id="themeBtn" type="button">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
<span id="themeLabel">Cambiar tema</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* Mostrar/ocultar contraseña */
|
||||
var togglePwd = document.getElementById('togglePwd');
|
||||
var pwdInput = document.getElementById('password');
|
||||
var eyeIcon = document.getElementById('eyeIcon');
|
||||
togglePwd.addEventListener('click', function() {
|
||||
var visible = pwdInput.type === 'text';
|
||||
pwdInput.type = visible ? 'password' : 'text';
|
||||
eyeIcon.innerHTML = visible
|
||||
? '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'
|
||||
: '<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/>';
|
||||
});
|
||||
|
||||
/* Tema */
|
||||
var html = document.documentElement;
|
||||
var themeBtn = document.getElementById('themeBtn');
|
||||
var themeLabel = document.getElementById('themeLabel');
|
||||
function updateLabel() {
|
||||
var isDark = html.getAttribute('data-theme') === 'dark';
|
||||
themeLabel.textContent = isDark ? 'Modo claro' : 'Modo oscuro';
|
||||
}
|
||||
updateLabel();
|
||||
themeBtn.addEventListener('click', function() {
|
||||
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('nexus-theme', next);
|
||||
updateLabel();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Poblaciones{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-icons/bootstrap-icons.css') }}">
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Configuración</div>
|
||||
<div class="page-title">Poblaciones</div>
|
||||
<div class="page-subtitle">Gestiona las poblaciones administradas.</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap;">
|
||||
|
||||
{# Formulario añadir #}
|
||||
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;min-width:260px;">
|
||||
<p class="form-label" style="margin-bottom:14px;">Nueva población</p>
|
||||
<form method="POST" action="{{ url_for('poblaciones_nueva') }}">
|
||||
<div class="form-group">
|
||||
<input class="form-input" type="text" name="poblacion"
|
||||
placeholder="Nombre de la población" required autofocus>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;margin-top:8px;">
|
||||
✚ Añadir
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Listado #}
|
||||
<div style="flex:1;min-width:280px;max-width: 800px;">
|
||||
<p style="margin-top:8px;font-size:12px;color:var(--muted)">{{ rows|length }} poblaciones</p>
|
||||
<div class="table-wrap border">
|
||||
<table id="tabla" class="">
|
||||
<thead class="">
|
||||
<tr class="">
|
||||
<th hidden >ID</th>
|
||||
<th>Población</th>
|
||||
<!-- <th style="text-align:right">Acción</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
{% set nombre_pob = r['poblacion'] %}
|
||||
<tr class="border-bottom">
|
||||
<td hidden class="td-id">{{ r['id'] }}</td>
|
||||
<td>{{ r['poblacion'] }}</td>
|
||||
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3"><div class="empty-state">Sin poblaciones registradas.</div></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/jquery-3.7.1.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/tabledit.min.js') }}"></script>
|
||||
|
||||
|
||||
<script>
|
||||
$('#tabla').Tabledit({
|
||||
url:'/editar_poblaciones',
|
||||
|
||||
editButton:true,
|
||||
deleteButton:true,
|
||||
restoreButton:false,
|
||||
hideIdentifier:false,
|
||||
|
||||
buttons:{
|
||||
edit:{
|
||||
class:'btn btn-sm border rounded me-1',
|
||||
html:'<i class="bi bi-pencil"></i> '
|
||||
},
|
||||
delete:{
|
||||
class:'btn btn-sm btn-light text-danger border me-1',
|
||||
html:'<i class="bi bi-trash"></i>'
|
||||
// html:'Borrar'
|
||||
},
|
||||
save:{
|
||||
class:'btn btn-sm btn-success',
|
||||
html:'Guardar'
|
||||
},
|
||||
confirm:{
|
||||
class:'btn btn-sm btn-warning',
|
||||
html:'¿Seguro?'
|
||||
}
|
||||
},
|
||||
|
||||
columns:{
|
||||
identifier:[0,'id'],
|
||||
editable:[
|
||||
[1,'poblacion']
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
onSuccess:function(data){
|
||||
if (data.action == "edit") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
if (data.action == "delete") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
|
||||
$('#tabla td:last-child').attr(
|
||||
'style',
|
||||
'white-space: nowrap; width:185px;'
|
||||
);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Executable
+190
@@ -0,0 +1,190 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Denominación{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-icons/bootstrap-icons.css') }}">
|
||||
|
||||
<body style="background-color: #f5f6fa;">
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Configuración</div>
|
||||
<div class="page-title">Tipo vehículo</div>
|
||||
<div class="page-subtitle">Gestiona los tipos de vehículos.</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap;">
|
||||
|
||||
{# Formulario añadir #}
|
||||
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;min-width:300px;">
|
||||
<p class="form-label" style="margin-bottom:14px;">Nuevo tipo vehículo</p>
|
||||
<form method="POST" action="{{ url_for('tipo_nueva') }}">
|
||||
<div class="form-group">
|
||||
<input class="form-input" type="text" name="tipo"
|
||||
placeholder="" 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;"> -->
|
||||
<div style="flex:1;min-width:320px;max-width: 800px;">
|
||||
|
||||
|
||||
{# Buscador #}
|
||||
<div class="search-wrap" style="margin-bottom:16px;max-width:100%">
|
||||
<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="searchDenom"
|
||||
placeholder="Buscar por tipo vehículo…"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
style="padding-right:32px; width: 500px;">
|
||||
<button type="button" id="clearSearch"
|
||||
style="display:none;position:absolute;right:325px;top:50%;
|
||||
transform:translateY(-50%);background:none;border:none;
|
||||
cursor:pointer;color:var(--muted);padding:4px;align-items:center;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>
|
||||
</div>
|
||||
|
||||
<p id="contador" style="margin-top:8px;font-size:12px;color:var(--muted)">
|
||||
{{ rows|length }} tipos vehículo
|
||||
</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table id="tabla">
|
||||
<thead>
|
||||
<tr">
|
||||
<th hidden>ID</th>
|
||||
<th>Tipo</th>
|
||||
<!-- <th>Literal</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyDenom">
|
||||
{% for r in rows %}
|
||||
{% set nombre_den = r['tipo'] %}
|
||||
<tr class="border-bottom">
|
||||
<td hidden class="td-id">{{ r['id'] }}</td>
|
||||
<td><strong>{{ r['tipo'] }}</strong></td>
|
||||
<!-- <td style="color:var(--muted);font-size:13px">{{ r['literal'] or '—' }}</td> -->
|
||||
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="empty-state">Sin tipo registradas.</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/jquery-3.7.1.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/tabledit.min.js') }}"></script>
|
||||
|
||||
<script>
|
||||
var input = document.getElementById('searchDenom');
|
||||
var clearBtn = document.getElementById('clearSearch');
|
||||
var contador = document.getElementById('contador');
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
clearBtn.style.display = this.value ? 'flex' : 'none';
|
||||
filtrar(this.value.trim().toLowerCase());
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', function() {
|
||||
input.value = '';
|
||||
this.style.display = 'none';
|
||||
input.focus();
|
||||
filtrar('');
|
||||
});
|
||||
|
||||
function filtrar(q) {
|
||||
var filas = document.querySelectorAll('#tbodyDenom tr');
|
||||
var visibles = 0;
|
||||
filas.forEach(function(tr) {
|
||||
var texto = tr.innerText.toLowerCase();
|
||||
var mostrar = !q || texto.indexOf(q) !== -1;
|
||||
tr.style.display = mostrar ? '' : 'none';
|
||||
if (mostrar) visibles++;
|
||||
});
|
||||
contador.textContent = visibles + ' tipos'
|
||||
+ (q ? ' — filtrado por «' + q + '»' : '');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$('#tabla').Tabledit({
|
||||
url:'/editar_tipo',
|
||||
|
||||
editButton:true,
|
||||
deleteButton:true,
|
||||
restoreButton:false,
|
||||
hideIdentifier:false,
|
||||
|
||||
buttons:{
|
||||
edit:{
|
||||
class:'btn btn-sm border rounded me-1',
|
||||
html:'<i class="bi bi-pencil"></i> '
|
||||
},
|
||||
// delete:{
|
||||
// class:'btn btn-sm btn-light text-danger border me-1',
|
||||
// html:'<i class="bi bi-trash"></i>'
|
||||
// },
|
||||
save:{
|
||||
class:'btn btn-sm btn-success',
|
||||
html:'Guardar'
|
||||
},
|
||||
// confirm:{
|
||||
// class:'btn btn-sm btn-warning',
|
||||
// html:'¿Seguro?'
|
||||
// }
|
||||
},
|
||||
|
||||
columns:{
|
||||
identifier:[0,'id'],
|
||||
editable:[
|
||||
[1,'tipo'],
|
||||
// [2,'literal'],
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
onSuccess:function(data){
|
||||
if (data.action == "edit") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
if (data.action == "delete") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
|
||||
$('#tabla td:last-child').attr(
|
||||
'style',
|
||||
'white-space: nowrap; width:185px;'
|
||||
);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Unidades{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-icons/bootstrap-icons.css') }}">
|
||||
|
||||
<body style="background-color: #f5f6fa;">
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Configuración</div>
|
||||
<div class="page-title">Unidades</div>
|
||||
<div class="page-subtitle">Gestiona los 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 añadir #}
|
||||
<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') }}">
|
||||
<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>
|
||||
<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;">
|
||||
<!-- <div style="flex:1;min-width:320px;"> -->
|
||||
|
||||
{# Buscador #}
|
||||
<div class="search-wrap" style="margin-bottom:16px;max-width:100%">
|
||||
<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
|
||||
style="padding-right:32px; width: 500px;">
|
||||
<button type="button" id="clearSearch"
|
||||
style="display:none;position:absolute;right:325px;top:50%;
|
||||
transform:translateY(-50%);background:none;border:none;
|
||||
cursor:pointer;color:var(--muted);padding:4px;align-items:center;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>
|
||||
</div>
|
||||
|
||||
<p id="contador" style="margin-top:8px;font-size:12px;color:var(--muted)">
|
||||
{{ rows|length }} unidades
|
||||
</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table id="tabla">
|
||||
<thead>
|
||||
<tr>
|
||||
<th hidden>ID</th>
|
||||
<th>Población</th>
|
||||
<th>Unidad</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyUnidades">
|
||||
{% for r in rows %}
|
||||
{% set nombre_uni = r['unidad'] %}
|
||||
<tr class="border-bottom">
|
||||
<td hidden class="td-id">{{ r['id'] }}</td>
|
||||
<td><strong>{{ r['poblacion'] or '—' }}</strong></td>
|
||||
<td><strong>{{ r['unidad'] }}</strong></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr id="sinDatos">
|
||||
<td colspan="4"><div class="empty-state">Sin unidades registradas.</div></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/jquery-3.7.1.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/tabledit.min.js') }}"></script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
var input = document.getElementById('searchUnidades');
|
||||
var clearBtn = document.getElementById('clearSearch');
|
||||
var contador = document.getElementById('contador');
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
clearBtn.style.display = this.value ? 'flex' : 'none';
|
||||
filtrar(this.value.trim().toLowerCase());
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', function() {
|
||||
input.value = '';
|
||||
this.style.display = 'none';
|
||||
input.focus();
|
||||
filtrar('');
|
||||
});
|
||||
|
||||
function filtrar(q) {
|
||||
var filas = document.querySelectorAll('#tbodyUnidades tr');
|
||||
var visibles = 0;
|
||||
filas.forEach(function(tr) {
|
||||
var texto = tr.innerText.toLowerCase();
|
||||
var mostrar = !q || texto.indexOf(q) !== -1;
|
||||
tr.style.display = mostrar ? '' : 'none';
|
||||
if (mostrar) visibles++;
|
||||
});
|
||||
contador.textContent = visibles + ' unidad' + (visibles !== 1 ? 'es' : '')
|
||||
+ (q ? ' — filtrado por «' + q + '»' : '');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$('#tabla').Tabledit({
|
||||
url:'/editar_unidades',
|
||||
|
||||
editButton:true,
|
||||
deleteButton:false,
|
||||
restoreButton:false,
|
||||
hideIdentifier:false,
|
||||
|
||||
buttons:{
|
||||
edit:{
|
||||
class:'btn btn-sm border rounded me-1 ',
|
||||
html:'<i class="bi bi-pencil"></i> '
|
||||
},
|
||||
// delete:{
|
||||
// class:'btn btn-sm btn-light text-danger border me-1',
|
||||
// html:'<i class="bi bi-trash"></i>'
|
||||
// // html:'Borrar'
|
||||
// },
|
||||
save:{
|
||||
class:'btn btn-sm btn-success',
|
||||
html:'Guardar'
|
||||
},
|
||||
confirm:{
|
||||
class:'btn btn-sm btn-warning',
|
||||
html:'¿Seguro?'
|
||||
}
|
||||
},
|
||||
|
||||
columns:{
|
||||
identifier:[0,'id'],
|
||||
editable:[
|
||||
[1,'poblacion'],
|
||||
[2,'unidad'],
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
onSuccess:function(data){
|
||||
if (data.action == "edit") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
if (data.action == "delete") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
|
||||
$('#tabla td:last-child').attr(
|
||||
'style',
|
||||
'white-space: nowrap; width:170px;'
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
Executable
+805
@@ -0,0 +1,805 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Editar' if vehiculo else 'Nuevo' }} Vehículo{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
.form-card { max-width: 860px; }
|
||||
.section-title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin: 28px 0 20px;
|
||||
}
|
||||
.section-title:first-of-type { margin-top: 0; }
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
.col-full { grid-column: 1 / -1; }
|
||||
.col-2 { grid-column: span 2; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Vehículos</div>
|
||||
<div class="page-title">{{ 'Editar vehículo ' ~ vehiculo['m_oficial'] if vehiculo else 'Nuevo vehículo' }}</div>
|
||||
|
||||
<div class="page-subtitle">
|
||||
{{ 'Modifica los datos del vehículo.' if vehiculo else 'Completa el formulario para registrar un nuevo vehículo.' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-card">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="_q" value="{{ q|default('') }}">
|
||||
<input type="hidden" name="_desde" value="{{ desde|default('') }}">
|
||||
<input type="hidden" name="_hasta" value="{{ hasta|default('') }}">
|
||||
<input type="hidden" name="_q_itv" value="{{ q_itv|default('') }}">
|
||||
<input type="hidden" name="_origen" value="{{ origen|default('') }}">
|
||||
<input type="hidden" name="_q_viat" value="{{ q_viat|default('') }}">
|
||||
|
||||
{# ── IDENTIFICACIÓN ── #}
|
||||
<div class="section-title">Identificación {{ '- id' ~ vehiculo['id'] if vehiculo }}</div>
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="m_oficial">Matrícula oficial</label>
|
||||
<input class="form-input" type="text" id="m_oficial" name="m_oficial"
|
||||
placeholder="Ej. 1234ABC" required
|
||||
oninput="this.value = this.value.toUpperCase();"
|
||||
value="{{ (vehiculo['m_oficial'] or '') if vehiculo else request.form.get('m_oficial', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="m_civil">Matrícula civil</label>
|
||||
<input class="form-input" type="text" id="m_civil" name="m_civil"
|
||||
placeholder="Ej. 5678XYZ"
|
||||
oninput="this.value = this.value.toUpperCase();"
|
||||
value="{{ (vehiculo['m_civil'] or '') if vehiculo else request.form.get('m_civil', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="m_propia">Matrícula propia</label>
|
||||
<input class="form-input" type="text" id="m_propia" name="m_propia"
|
||||
placeholder="Ej. 0123ZXZ"
|
||||
oninput="this.value = this.value.toUpperCase();"
|
||||
value="{{ (vehiculo['m_propia'] or '') if vehiculo else request.form.get('m_propia', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="VIN">VIN</label>
|
||||
<input class="form-input" type="text" id="VIN" name="VIN"
|
||||
placeholder="VIN"
|
||||
oninput="this.value = this.value.toUpperCase();"
|
||||
value="{{ (vehiculo['VIN'] or '') if vehiculo else request.form.get('VIN', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="poliza">Poliza</label>
|
||||
<input class="form-input" type="text" id="poliza" name="poliza"
|
||||
oninput="this.value = this.value.toUpperCase();"
|
||||
value="{{ (vehiculo['Poliza'] or '') if vehiculo else request.form.get('poliza', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="viat">Via-T</label>
|
||||
<input class="form-input" type="text" id="viat" name="viat" style="width: 210px; padding-left: 10px;"
|
||||
oninput="this.value = this.value.toUpperCase();"
|
||||
value="{{ (vehiculo['viat'] or '') if vehiculo else request.form.get('viat', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="legajo">Legajo</label>
|
||||
<input class="form-input" id="legajo" name="legajo"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['legajo'] or '') if vehiculo else request.form.get('viat', '') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── CARACTERÍSTICAS TÉCNICAS ── #}
|
||||
<div class="section-title">Características técnicas</div>
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="marca">Marca</label>
|
||||
<input class="form-input" type="text" id="marca" name="marca"
|
||||
placeholder="Ej. Ford, Renault…" required
|
||||
value="{{ (vehiculo['marca'] or '') if vehiculo else request.form.get('marca', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modelo">Modelo</label>
|
||||
<input class="form-input" type="text" id="modelo" name="modelo"
|
||||
placeholder="Ej. Transit, Kangoo…" required
|
||||
value="{{ (vehiculo['modelo'] or '') if vehiculo else request.form.get('modelo', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="tipo">Tipo vehículo</label>
|
||||
<div class="select-wrap">
|
||||
<select class="form-input form-select" id="tipo" name="tipo" required>
|
||||
<option value="">— Selecciona tipo —</option>
|
||||
{% for tip in tipo %}
|
||||
<option value="{{ tip['tipo'] }}"
|
||||
{% if vehiculo and vehiculo['tipo'] == tip['tipo'] %}selected
|
||||
{% elif request.form.get('tipo') == tip['tipo'] %}selected
|
||||
{% endif %}>
|
||||
{{ tip['tipo'] }}
|
||||
</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>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="color">Color</label>
|
||||
<input class="form-input" type="text" id="color" name="color"
|
||||
placeholder="Ej. Blanco, Negro…"
|
||||
value="{{ (vehiculo['color'] or '') if vehiculo else request.form.get('color', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cilindrada">Cilindrada</label>
|
||||
<input class="form-input" type="text" id="cilindrada" name="cilindrada"
|
||||
placeholder="Ej. 1600cc"
|
||||
value="{{ (vehiculo['cilindrada'] or '') if vehiculo else request.form.get('cilindrada', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="potencia">Potencia (CV)</label>
|
||||
<input class="form-input" type="number" id="potencia" name="potencia"
|
||||
placeholder="Ej. 120"
|
||||
value="{{ (vehiculo['potencia'] or '') if vehiculo else request.form.get('potencia', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="combustible">Combustible</label>
|
||||
<input class="form-input" type="text" id="combustible" name="combustible"
|
||||
placeholder="Gasolina / Diésel / Eléctrico…"
|
||||
value="{{ (vehiculo['combustible'] or '') if vehiculo else request.form.get('combustible', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="neumaticos">Neumáticos</label>
|
||||
<input class="form-input" type="text" id="neumaticos" name="neumaticos"
|
||||
placeholder="Ej. 205/55 R16"
|
||||
value="{{ (vehiculo['neumaticos'] or '') if vehiculo else request.form.get('neumaticos', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="plazas">Plazas</label>
|
||||
<input class="form-input" type="text" id="plazas" name="plazas"
|
||||
placeholder="Nº de plazas"
|
||||
value="{{ (vehiculo['plazas'] or '') if vehiculo else request.form.get('plazas', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="width: 280px;">
|
||||
<label class="form-label"> </label>
|
||||
<label style="margin-top:22px;display:flex;align-items:center;gap:10px;cursor:pointer;
|
||||
padding:10px 14px;background:var(--bg);border:1px solid var(--border);
|
||||
border-radius:8px;transition:border-color .15s;"
|
||||
onmouseover="this.style.borderColor='var(--accent)'"
|
||||
onmouseout="this.style.borderColor='var(--border)'">
|
||||
|
||||
<input type="checkbox" name="lanzadestellos" value="1"
|
||||
{% if vehiculo and vehiculo['lanzadestellos'] == 1 %}checked{% endif %}
|
||||
style="width:16px;height:16px;accent-color:var(--accent);cursor:pointer;">
|
||||
|
||||
<span style="font-size:13px;color:var(--text)">Lanzadestellos</span>
|
||||
<span style="font-size:11px;color:var(--muted);margin-left:4px;">
|
||||
|
||||
({{ '✓ Con lanzadestellos' if vehiculo and vehiculo['lanzadestellos'] == 1 else '✗ Sin lanzadestellos' }})
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if vehiculo %}
|
||||
{% set sit = vehiculo['situacion'] or '' %}
|
||||
{% if sit | upper == 'BAJA' %}
|
||||
{% set color_borde = '#f87171' %}
|
||||
{% set color_fondo = 'rgba(248,113,113,.08)' %}
|
||||
{% elif sit | upper == 'ACTIVO' %}
|
||||
{% set color_borde = '#34d399' %}
|
||||
{% set color_fondo = 'rgba(52,211,153,.08)' %}
|
||||
{% elif sit | upper == 'ALTA' %}
|
||||
{% set color_borde = '#60a5fa' %}
|
||||
{% set color_fondo = 'rgba(96,165,250,.08)' %}
|
||||
{% else %}
|
||||
{% set color_borde = 'var(--border)' %}
|
||||
{% set color_fondo = 'var(--pill-bg)' %}
|
||||
{% endif %}
|
||||
|
||||
{% set estilo_sit = 'background:' ~ color_fondo ~ ';border:1.5px solid ' ~ color_borde ~ ';color:var(--text);font-weight:600;cursor: not-allowed;' %}
|
||||
|
||||
{# ── SITUACIÓN Y DESTINO ── #}
|
||||
<div class="section-title">Situación y destino (No editable)</div>
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="situacion">Situación</label>
|
||||
<input class="form-input" type="text" id="situacion" name="situacion" style="{{ estilo_sit }}" readonly
|
||||
value="{{ (vehiculo['situacion'] or '') if vehiculo else request.form.get('situacion', '') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="poblacion">Población</label>
|
||||
<input class="form-input" type="text" id="poblacion" name="poblacion" style="{{ estilo_sit }}" readonly
|
||||
value="{{ (vehiculo['poblacion'] or '') if vehiculo else request.form.get('poblacion','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="width: 400px">
|
||||
<label class="form-label" for="unidad">Unidad</label>
|
||||
<input class="form-input" type="text" id="unidad" name="unidad"
|
||||
style="{{ estilo_sit }}" readonly
|
||||
value="{{ (vehiculo['unidad'] or '') if vehiculo else request.form.get('unidad','') }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{# ── FECHAS Y KILOMETRAJE ── #}
|
||||
<div class="section-title">Kilometraje - ITV</div>
|
||||
<div class="form-grid">
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="km">Km actuales</label>
|
||||
<input class="form-input" id="km" name="km"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['km'] or '') if vehiculo else request.form.get('km','') }}">
|
||||
|
||||
<!-- value="{{ vehiculo['km'] if vehiculo else request.form.get('km','') }}"> -->
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="km_1">Km anterior</label>
|
||||
<input class="form-input" id="km_1" name="km_1"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['km_1'] or '') if vehiculo else request.form.get('km_1','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="itv_ultima">Última ITV</label>
|
||||
<input class="form-input" type="date" id="itv_ultima" name="itv_ultima"
|
||||
value="{{ (vehiculo['itv_ultima'][:10] or '') if vehiculo and vehiculo['itv_ultima'] else request.form.get('itv_ultima','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="itv">Próxima ITV</label>
|
||||
<input class="form-input" type="date" id="itv" name="itv"
|
||||
value="{{ (vehiculo['itv'][:10] or '') if vehiculo and vehiculo['itv'] else request.form.get('itv','') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── NOTAS ── #}
|
||||
<div class="section-title">NOTAS</div>
|
||||
<div class="form-group" style="max-width: 1560px;">
|
||||
<!-- <textarea name="observaciones">{{ (vehiculo['observaciones'] or '') if vehiculo else '' }}</textarea> -->
|
||||
<textarea class="form-input" id="notas" name="notas"
|
||||
rows="5" placeholder="Notas adicionales…">{{ (vehiculo['notas'] or '') if vehiculo else request.form.get('notas','') }}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
|
||||
{# ── DATOS INTERVENIDO ── #}
|
||||
<!-- {% set m_oficial = vehiculo['m_oficial'] if vehiculo else '' %} -->
|
||||
<div id="bloque-intervenido"
|
||||
style="display:{{ 'block' if m_oficial and m_oficial.strip().upper().endswith('Q') else 'none' }}">
|
||||
<div class="section-title" style="margin-top:28px">Datos vehículo intervenido</div>
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="int_operacion">Operacion</label>
|
||||
<input class="form-input" id="int_operacion" name="int_operacion"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['int_operacion'] or '') if vehiculo else request.form.get('int_operacion','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="int_grupo">Grupo interviene</label>
|
||||
<input class="form-input" id="int_grupo" name="int_grupo"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['int_grupo'] or '') if vehiculo else request.form.get('int_grupo','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="int_procedimiento">Diligencias</label>
|
||||
<input class="form-input" id="int_procedimiento" name="int_procedimiento"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['int_procedimiento'] or '') if vehiculo else request.form.get('int_procedimiento','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="int_juzgado">Juzgado interviene</label>
|
||||
<input class="form-input" id="int_juzgado" name="int_juzgado"
|
||||
placeholder=""
|
||||
value="{{ (vehiculo['int_juzgado'] or '') if vehiculo else request.form.get('int_juzgado','') }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions" style="margin-top:28px">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ '💾 GUARDAR CAMBIOS' if vehiculo else '✚ Registrar vehículo' }}
|
||||
</button>
|
||||
|
||||
{# Cancelar #}
|
||||
{% if origen == 'viat' %}
|
||||
<a href="/viat?q={{ q_viat }}" class="btn btn-ghost">Cancelar</a>
|
||||
{% elif desde or hasta or q_itv %}
|
||||
<a href="/itv?desde={{ desde }}&hasta={{ hasta }}&q_itv={{ q_itv }}" class="btn btn-ghost">Cancelar</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('vehiculos_list', q=q) if q else url_for('vehiculos_list') }}" class="btn btn-ghost">Cancelar</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- <div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px;"> -->
|
||||
{% if vehiculo %}
|
||||
<button type="button" class="btn btn-ghost" onclick="imprimirFicha()" title="Imprimir ficha del vehículo">
|
||||
<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>
|
||||
Ficha - Imprimir/PDF
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- </div> -->
|
||||
|
||||
</div>
|
||||
|
||||
</form> {# ← CIERRE del formulario principal #}
|
||||
|
||||
<!-- {# Eliminar vehículo FUERA del form principal #}
|
||||
{% if vehiculo %}
|
||||
{% set nombre_vehiculo = vehiculo['m_oficial'] or vehiculo['tipo'] or '#' ~ vehiculo['id'] %} -->
|
||||
|
||||
<!-- QUITADO EL BOTON ELIMINAR -->
|
||||
<!-- <form method="POST"
|
||||
action="{{ url_for('vehiculos_eliminar', id=vehiculo['id']) }}"
|
||||
style="margin-top:0px;display:flex;justify-content:flex-end;">
|
||||
<input type="hidden" name="_q" value="{{ q|default('') }}">
|
||||
<button type="button" class="btn btn-danger"
|
||||
onclick="confirmarEliminarVehiculo(this)">🗑 Eliminar vehículo</button>
|
||||
</form> -->
|
||||
<!-- {% endif %} -->
|
||||
|
||||
|
||||
|
||||
{# ── HISTORIAL (fuera del form principal) ── #}
|
||||
{% if vehiculo %}
|
||||
<div class="section-title" style="margin-top:32px">Historial</div>
|
||||
|
||||
{% if historial %}
|
||||
<div class="table-wrap" style="margin-bottom:20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Fecha</th>
|
||||
<th>Matrícula</th>
|
||||
<th>Situación</th>
|
||||
<th>Poblacion</th>
|
||||
<th>Unidad</th>
|
||||
<th>Observaciones</th>
|
||||
<th style="text-align:right">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for h in historial %}
|
||||
<tr>
|
||||
<td class="td-id">{{ h['id'] or '—' }}</td>
|
||||
<td style="white-space:nowrap">{{ h['fecha'] | fecha_es }}</td>
|
||||
<!-- <td style="white-space:nowrap">{{ h['fecha'] or '—' }}</td> -->
|
||||
<td>{{ h['m_oficial'] or '—' }}</td>
|
||||
<td>{{ h['situacion'] or '—' }}</td>
|
||||
<td>{{ h['poblacion'] or '—' }}</td>
|
||||
<td>{{ h['unidad'] or '—' }}</td>
|
||||
<!-- <td>{{ h['grupo'] or '—' }}</td> -->
|
||||
<td style="color:var(--muted);font-size:12px">{{ h['observaciones'] or '—' }}</td>
|
||||
<td style="text-align:right">
|
||||
<form method="POST"
|
||||
action="{{ url_for('historial_eliminar', vid=vehiculo['id'], hid=h['id']) }}">
|
||||
<input type="hidden" name="_q" value="{{ q|default('') }}">
|
||||
<input type="hidden" name="_desde" value="{{ desde|default('') }}">
|
||||
<input type="hidden" name="_hasta" value="{{ hasta|default('') }}">
|
||||
<input type="hidden" name="_unidad_itv" value="{{ unidad_itv|default('') }}">
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
onclick="confirmarEliminar(this)">Eliminar</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="color:var(--muted);font-size:13px;margin-bottom:20px;">
|
||||
No hay entradas en el historial de este vehículo.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{# Formulario añadir entrada #}
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:20px;">
|
||||
<p style="font-size:11px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;
|
||||
color:var(--muted);margin-bottom:16px;">Añadir entrada al historial</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('historial_nuevo', vid=vehiculo['id']) }}">
|
||||
<input type="hidden" name="_q" value="{{ q|default('') }}">
|
||||
<input type="hidden" name="_desde" value="{{ desde|default('') }}">
|
||||
<input type="hidden" name="_hasta" value="{{ hasta|default('') }}">
|
||||
<input type="hidden" name="_unidad_itv" value="{{ unidad_itv|default('') }}">
|
||||
|
||||
<div class="form-grid" style="margin-bottom:12px;">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label class="form-label" for="h_fecha">Fecha</label>
|
||||
<input class="form-input" type="date" id="h_fecha" name="h_fecha" required>
|
||||
</div>
|
||||
|
||||
<input type='hidden' id="h_vehiculo_id" name="h_vehiculo_id" readonly
|
||||
placeholder="" value="{{ vehiculo['id'] if vehiculo else request.form.get('id','') }}">
|
||||
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label class="form-label" for="h_situacion">Situación</label>
|
||||
<div class="select-wrap">
|
||||
<select class="form-input form-select" id="h_situacion" name="h_situacion"
|
||||
onchange="onSituacionChange(this.value)" required>
|
||||
<option value="">— Selecciona situación —</option>
|
||||
<option value="ACTIVO">ACTIVO</option>
|
||||
<!-- <option value="EN REPARACIÓN">EN REPARACIÓN</option> -->
|
||||
<option value="ALTA">ALTA</option>
|
||||
<option value="BAJA">BAJA</option>
|
||||
</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>
|
||||
|
||||
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label class="form-label" for="h_poblacion">Población</label>
|
||||
<div class="select-wrap">
|
||||
<select class="form-input form-select" id="h_poblacion" name="h_poblacion"
|
||||
onchange="cargarUnidades(this.value)" disabled>
|
||||
<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>
|
||||
|
||||
<div class="form-group col-2" style="margin-bottom:0">
|
||||
<label class="form-label" for="h_unidad">Unidad</label>
|
||||
<div class="select-wrap">
|
||||
<select class="form-input form-select" id="h_unidad" name="h_unidad" disabled>
|
||||
<option value="">— Primero selecciona población —</option>
|
||||
</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>
|
||||
|
||||
<div class="form-group col-full" style="margin-bottom:0">
|
||||
<label class="form-label" for="h_obs">Observaciones</label>
|
||||
<input class="form-input" type="text" id="h_obs" name="h_observaciones"
|
||||
placeholder="Descripción…">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<svg width="12" height="12" 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>
|
||||
Añadir al historial
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div> {# ← cierre de form-card #}
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
function confirmarEliminar(btn) {
|
||||
if (confirm('¿Eliminar esta entrada del historial?')) {
|
||||
btn.closest('form').submit();
|
||||
}
|
||||
}
|
||||
|
||||
function formatFecha(f) {
|
||||
if (!f) return '';
|
||||
// Si ya está en formato DD-MM-YYYY lo devuelve tal cual
|
||||
if (/^\d{2}-\d{2}-\d{4}$/.test(f)) return f;
|
||||
// Convertir YYYY-MM-DD a DD-MM-YYYY
|
||||
var partes = f.substring(0, 10).split('-');
|
||||
if (partes.length === 3) return partes[2] + '-' + partes[1] + '-' + partes[0];
|
||||
return f;
|
||||
}
|
||||
|
||||
function fila(label, valor) {
|
||||
if (!valor) return '';
|
||||
return `<tr>
|
||||
<td style="font-weight:600;color:#555;padding:6px 12px;width:40%;border-bottom:1px solid #eee;font-size:11px;text-transform:uppercase;letter-spacing:.5px">${label}</td>
|
||||
<td style="padding:6px 12px;border-bottom:1px solid #eee;font-size:13px">${valor}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function onSituacionChange(valor) {
|
||||
var selectPoblacion = document.getElementById('h_poblacion');
|
||||
var selectUnidad = document.getElementById('h_unidad');
|
||||
var inputGrupo = document.getElementById('h_grupo');
|
||||
var campSituacion = document.getElementById('situacion');
|
||||
|
||||
// Resetear desplegables dependientes
|
||||
selectPoblacion.value = '';
|
||||
selectUnidad.innerHTML = '<option value="">— Primero selecciona población —</option>';
|
||||
selectUnidad.disabled = true;
|
||||
|
||||
if (valor === 'ACTIVO') {
|
||||
// Solo ACTIVO habilita población y unidad
|
||||
selectPoblacion.disabled = false;
|
||||
inputGrupo.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmarEliminarVehiculo(btn) {
|
||||
if (confirm('¿Eliminar el vehículo {{ nombre_vehiculo }}? Esta acción no se puede deshacer.')) {
|
||||
btn.closest('form').submit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function cargarUnidades(poblacionId) {
|
||||
var selectUnidad = document.getElementById('h_unidad');
|
||||
selectUnidad.innerHTML = '<option value="">— Cargando… —</option>';
|
||||
selectUnidad.disabled = true;
|
||||
|
||||
|
||||
if (!poblacionId) {
|
||||
selectUnidad.innerHTML = '<option value="">— Primero selecciona población —</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/unidades/' + poblacionId)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.length === 0) {
|
||||
selectUnidad.innerHTML = '<option value="">— Sin unidades disponibles —</option>';
|
||||
return;
|
||||
}
|
||||
var opciones = '<option value="">— Selecciona unidad —</option>';
|
||||
data.forEach(function(u) {
|
||||
opciones += '<option value="' + u.unidad + '">' + u.unidad + '</option>';
|
||||
});
|
||||
selectUnidad.innerHTML = opciones;
|
||||
selectUnidad.disabled = false;
|
||||
})
|
||||
.catch(function() {
|
||||
selectUnidad.innerHTML = '<option value="">— Error al cargar —</option>';
|
||||
});
|
||||
}
|
||||
|
||||
function checkMatricula(valor) {
|
||||
var bloque = document.getElementById('bloque-intervenido');
|
||||
if (valor && valor.trim().toUpperCase().endsWith('Q')) {
|
||||
bloque.style.display = 'block';
|
||||
} else {
|
||||
bloque.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function imprimirFicha() {
|
||||
var v = {
|
||||
id: '{{ vehiculo["id"] }}',
|
||||
m_oficial: '{{ vehiculo["m_oficial"] or "" }}',
|
||||
m_civil: '{{ vehiculo["m_civil"] or "" }}',
|
||||
m_propia: '{{ vehiculo["m_propìa"] or "" }}',
|
||||
denominacion: '{{ vehiculo["denominacion"] or "" }}',
|
||||
marca: '{{ vehiculo["marca"] or "" }}',
|
||||
modelo: '{{ vehiculo["modelo"] or "" }}',
|
||||
tipo: '{{ vehiculo["tipo"] or "" }}',
|
||||
color: '{{ vehiculo["color"] or "" }}',
|
||||
VIN: '{{ vehiculo["VIN"] or "" }}',
|
||||
matricula: '{{ vehiculo["numero_identificacion"] or "" }}',
|
||||
potencia: '{{ vehiculo["potencia"] or "" }}',
|
||||
cilindrada: '{{ vehiculo["cilindrada"] or "" }}',
|
||||
combustible: '{{ vehiculo["combustible"] or "" }}',
|
||||
neumaticos: '{{ vehiculo["neumaticos"] or "" }}',
|
||||
plazas: '{{ vehiculo["plazas"] or "" }}',
|
||||
km: '{{ vehiculo["km"] or "" }}',
|
||||
km_1: '{{ vehiculo["km_1"] or "" }}',
|
||||
itv_ultima: '{{ vehiculo["itv_ultima"] or "" }}',
|
||||
itv: '{{ vehiculo["itv"] or "" }}',
|
||||
situacion: '{{ vehiculo["situacion"] or "" }}',
|
||||
poblacion: '{{ vehiculo["poblacion"] or "" }}',
|
||||
unidad: '{{ vehiculo["unidad"] or "" }}',
|
||||
poliza: '{{ vehiculo["poliza"] or "" }}',
|
||||
viat: '{{ vehiculo["viat"] or "" }}',
|
||||
};
|
||||
|
||||
var fecha = new Date().toLocaleDateString('es-ES');
|
||||
|
||||
// Construir tabla de historial
|
||||
var historialRows = '';
|
||||
{% if historial %}
|
||||
{% for h in historial %}
|
||||
historialRows += `<tr>
|
||||
<td>${'{{ h["fecha"] | fecha_es }}' || '—'}</td>
|
||||
<td>${'{{ h["matricula"] or "—" }}'}</td>
|
||||
<td>${'{{ h["situacion"] or "—" }}'}</td>
|
||||
<td>${'{{ h["poblacion"] or "—" }}'}</td>
|
||||
<td>${'{{ h["unidad"] or "—" }}'}</td>
|
||||
<td>${'{{ h["grupo"] or "—" }}'}</td>
|
||||
<td style="color:#555">${'{{ h["observaciones"] or "—" }}'}</td>
|
||||
</tr>`;
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
var htmlHistorial = historialRows ? `
|
||||
<div class="section" style="margin-top:20px">
|
||||
<div class="section-title">Historial</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Matrícula</th>
|
||||
<th>Situación</th>
|
||||
<th>Población</th>
|
||||
<th>Unidad</th>
|
||||
<th>Grupo</th>
|
||||
<th>Observaciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${historialRows}</tbody>
|
||||
</table>
|
||||
</div>` : '';
|
||||
|
||||
var html = `<!DOCTYPE html><html><head><meta charset="UTF-8">
|
||||
<title>Ficha vehículo ${v.m_oficial}</title>
|
||||
<style>
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
body { font-family: Arial, sans-serif; color:#1a1d29; padding:24px; font-size:13px; }
|
||||
.header { display:flex; justify-content:space-between; align-items:flex-start;
|
||||
border-bottom:3px solid #5c6ef8; padding-bottom:16px; margin-bottom:20px; }
|
||||
.header h1 { font-size:20px; color:#5c6ef8; margin-bottom:4px; }
|
||||
.header .meta { font-size:11px; color:#888; }
|
||||
.badge { background:#5c6ef8; color:#fff; font-size:11px; font-weight:700;
|
||||
padding:3px 10px; border-radius:20px; display:inline-block; margin-top:6px; }
|
||||
.badge.baja { background:#ef4444; }
|
||||
.badge.activo { background:#10b981; }
|
||||
.section { margin-bottom:20px; }
|
||||
.section-title { font-size:10px; font-weight:700; letter-spacing:1px;
|
||||
text-transform:uppercase; color:#5c6ef8;
|
||||
border-bottom:1px solid #5c6ef8; padding-bottom:4px;
|
||||
margin-bottom:8px; }
|
||||
table { width:100%; border-collapse:collapse; }
|
||||
.grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
|
||||
/* Tabla historial ocupa ancho completo */
|
||||
.section table th {
|
||||
background:#5c6ef8; color:#fff; padding:5px 8px;
|
||||
text-align:left; font-size:9px; letter-spacing:.5px;
|
||||
}
|
||||
.section table td {
|
||||
padding:5px 8px; border-bottom:1px solid #eee; font-size:10px;
|
||||
}
|
||||
.section table tr:nth-child(even) td { background:#f5f6fa; }
|
||||
/* Filas datos vehículo */
|
||||
.data-table td:first-child {
|
||||
font-weight:600; color:#555; width:40%;
|
||||
font-size:11px; text-transform:uppercase; letter-spacing:.5px;
|
||||
}
|
||||
.data-table td { padding:6px 12px; border-bottom:1px solid #eee; font-size:13px; }
|
||||
.footer { margin-top:24px; font-size:10px; color:#aaa;
|
||||
text-align:center; border-top:1px solid #eee; padding-top:12px; }
|
||||
@media print {
|
||||
body { padding:12px; }
|
||||
@page { margin:1.5cm; size: A4; }
|
||||
}
|
||||
</style>
|
||||
</head><body>
|
||||
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="meta">Ficha de vehículo · ID #${v.id}</div>
|
||||
<h1>${v.m_oficial || v.denominacion || 'Sin matrícula'}</h1>
|
||||
<h3 class="badge ${v.situacion === 'ALTA' ? 'alta' : v.situacion === 'BAJA' ? 'baja' : v.situacion === 'ACTIVO' ? 'activo' : ''}">
|
||||
${v.situacion || 'Sin situación'}
|
||||
</h3>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="meta">Generado el ${fecha}</div>
|
||||
<div style="font-size:18px;font-weight:700;margin-top:8px">${v.marca} ${v.modelo}</div>
|
||||
<div class="meta">${v.denominacion}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="section">
|
||||
<div class="section-title">Identificación</div>
|
||||
<table class="data-table">
|
||||
${fila('M. oficial', v.m_oficial)}
|
||||
${fila('M. civil', v.m_civil)}
|
||||
${fila('M. propia', v.m_propia)}
|
||||
${fila('VIN', v.VIN)}
|
||||
${fila('Denominación', v.denominacion)}
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Características técnicas</div>
|
||||
<table class="data-table">
|
||||
${fila('Marca', v.marca)}
|
||||
${fila('Modelo', v.modelo)}
|
||||
${fila('Tipo', v.tipo)}
|
||||
${fila('Color', v.color)}
|
||||
${fila('Cilindrada', v.cilindrada)}
|
||||
${fila('Potencia', v.potencia ? v.potencia + ' CV' : '')}
|
||||
${fila('Combustible', v.combustible)}
|
||||
${fila('Neumáticos', v.neumaticos)}
|
||||
${fila('Plazas', v.plazas)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="section">
|
||||
<div class="section-title">Situación y destino</div>
|
||||
<table class="data-table">
|
||||
${fila('Situación', v.situacion)}
|
||||
${fila('Población', v.poblacion)}
|
||||
${fila('Unidad', v.unidad)}
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">ITV y kilometraje</div>
|
||||
<table class="data-table">
|
||||
${fila('Km actuales', v.km)}
|
||||
${fila('Próxima ITV', formatFecha(v.itv))}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Otros datos</div>
|
||||
<table class="data-table">
|
||||
${fila('Póliza', v.poliza)}
|
||||
${fila('Via-T', v.viat)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${htmlHistorial}
|
||||
|
||||
|
||||
</body></html>`;
|
||||
|
||||
var win = window.open('', '_blank', 'width=1000,height=750');
|
||||
win.document.write(html);
|
||||
win.document.close();
|
||||
win.focus();
|
||||
setTimeout(function() { win.print(); }, 400);
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Executable
+391
@@ -0,0 +1,391 @@
|
||||
{% 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 %}
|
||||
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
{% if rows %}
|
||||
{% for r in rows %}
|
||||
<tr data-id="{{ r['id'] }}"
|
||||
onclick="window.location='/vehiculos/{{ r['id'] }}/editar?q={{ q|urlencode }}'"
|
||||
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>{{ r['vin'] or '—' }}</td> -->
|
||||
<td><strong>{{ r['marca'] or '—' }}</strong></td>
|
||||
<td>{{ r['modelo'] or '—' }}</td>
|
||||
<td>{{ r['poblacion'] or '—' }}</td>
|
||||
<td>{{ r['unidad'] or '—' }}</td>
|
||||
<!-- <td>{{ r['grupo'] or '—' }}</td> -->
|
||||
<td>{{ r['tipo'] or '—' }}</td>
|
||||
<td>
|
||||
{% if r['situacion'] == 'BAJA' %}
|
||||
<span style="background:rgba(248,113,113,.12);color:#f87171;font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(248,113,113,.3)">
|
||||
BAJA
|
||||
</span>
|
||||
{% elif r['situacion'] %}
|
||||
<span style="background:rgba(52,211,153,.12);color:#34d399;font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(52,211,153,.3)">
|
||||
{{ r['situacion'] }}
|
||||
</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="10" rowspan="7">
|
||||
<div class="empty-state" style="color: rgb(202, 145, 145);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M5 17H3a2 2 0 01-2-2V9a2 2 0 012-2h11l4 4v4a2 2 0 01-2 2h-1"/>
|
||||
<circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/>
|
||||
</svg>
|
||||
<br>
|
||||
{% if q %}
|
||||
No se encontraron resultados para <strong>"{{ q }}"</strong>.
|
||||
{% else %}
|
||||
<!-- No hay vehículos registrados aún. -->
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- {% if rows %}
|
||||
{% for r in rows %}
|
||||
<tr data-id="{{ r['id'] }}">
|
||||
<td 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>
|
||||
{% if r['tipo'] %}
|
||||
<span style="background:rgba(92,110,248,.12);color:var(--accent);font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(92,110,248,.25)">
|
||||
{{ r['tipo'] }}
|
||||
</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if r['situacion'] == 'BAJA' %}
|
||||
<span style="background:rgba(248,113,113,.12);color:#f87171;font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(248,113,113,.3)">
|
||||
BAJA
|
||||
</span>
|
||||
{% elif r['situacion'] %}
|
||||
<span style="background:rgba(52,211,153,.12);color:#34d399;font-size:11px;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid rgba(52,211,153,.3)">
|
||||
{{ r['situacion'] }}
|
||||
</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="td-actions" style="justify-content:flex-end">
|
||||
<a href="/vehiculos/{{ r['id'] }}/editar?q={{ q|urlencode }}" class="btn btn-ghost btn-sm">Editar</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M5 17H3a2 2 0 01-2-2V9a2 2 0 012-2h11l4 4v4a2 2 0 01-2 2h-1"/>
|
||||
<circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/>
|
||||
</svg>
|
||||
{% if q %}
|
||||
No se encontraron resultados para <strong>"{{ q }}"</strong>.
|
||||
{% else %}
|
||||
No hay vehículos registrados aún.
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %} -->
|
||||
|
||||
|
||||
Executable
+230
@@ -0,0 +1,230 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Via-T{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div class="page-eyebrow">Gestión</div>
|
||||
<div class="page-title">Via-T</div>
|
||||
<div class="page-subtitle">Listado de vehículos con dispositivo Via-T. Búsqueda por Via-T, m.oficial, m.civil, marca, modelo, poblacion, unidad tipo.</div>
|
||||
</div>
|
||||
|
||||
<div class="crud-toolbar">
|
||||
<div class="search-wrap" style="max-width:420px;flex:1;">
|
||||
<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="searchInput"
|
||||
placeholder="Buscar… palabras separadas por comas"
|
||||
value="{{ q }}" autocomplete="off" style="padding-right:32px;" autofocus>
|
||||
<button type="button" id="clearBtn"
|
||||
style="display:{{ 'flex' if q else 'none' }};position:absolute;right:10px;top:50%;
|
||||
transform:translateY(-50%);background:none;border:none;cursor:pointer;
|
||||
color:var(--muted);padding:4px;align-items:center;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>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<a href="{{ url_for('viat_exportar', q=q) }}" class="btn btn-ghost" id="btnExcel" title="Exportar 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>
|
||||
</div>
|
||||
|
||||
<p id="counter" style="margin-bottom:10px;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 id="tablaViat">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Via-T</th>
|
||||
<th>M. Oficial</th>
|
||||
<th>M. Civil</th>
|
||||
<th>Marca</th>
|
||||
<th>Modelo</th>
|
||||
<th>Población</th>
|
||||
<th>Unidad</th>
|
||||
<th>Tipo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
{% include 'viat/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;
|
||||
|
||||
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 = '';
|
||||
this.style.display = 'none';
|
||||
input.focus();
|
||||
buscar('');
|
||||
});
|
||||
|
||||
function buscar(q) {
|
||||
var btnExcel = document.getElementById('btnExcel');
|
||||
if (btnExcel) {
|
||||
btnExcel.href = '{{ url_for("viat_exportar") }}?q=' + encodeURIComponent(q);
|
||||
}
|
||||
fetch('{{ url_for("viat_list") }}?q=' + encodeURIComponent(q) + '&solo_filas=1')
|
||||
.then(function(r) {
|
||||
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 + '»' : '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── PDF ── */
|
||||
document.getElementById('btnPDF').addEventListener('click', function() {
|
||||
var titulo = 'Listado Via-T';
|
||||
var fecha = new Date().toLocaleDateString('es-ES');
|
||||
var heads = ['Via-T','M. Oficial','M. Civil','Marca','Modelo','Población','Unidad','Tipo'];
|
||||
var filas = '';
|
||||
document.querySelectorAll('#tablaViat tbody tr[data-id]').forEach(function(tr) {
|
||||
filas += '<tr>';
|
||||
tr.querySelectorAll('td').forEach(function(td) {
|
||||
filas += '<td>' + td.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}
|
||||
@media print{body{margin:10px}}
|
||||
</style></head><body>
|
||||
<h2>${titulo}</h2>
|
||||
<div class="meta">Generado el ${fecha} · ${document.querySelectorAll('#tablaViat tbody 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=1000,height=700');
|
||||
win.document.write(html); win.document.close(); win.focus();
|
||||
setTimeout(function(){ win.print(); }, 400);
|
||||
});
|
||||
|
||||
/* ── COPIAR ── */
|
||||
document.getElementById('btnCopiar').addEventListener('click', function() {
|
||||
var btn = this;
|
||||
var heads = ['Via-T','M. Oficial','M. Civil','Marca','Modelo','Población','Unidad','Tipo'];
|
||||
var lineas = [heads.join('\t')];
|
||||
document.querySelectorAll('#tablaViat 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);
|
||||
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.'); }
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* ── ORDENACIÓN ── */
|
||||
(function() {
|
||||
var estado = { col: -1, asc: true };
|
||||
document.querySelectorAll('#tablaViat thead th').forEach(function(th, idx) {
|
||||
th.style.cursor = 'pointer'; th.style.userSelect = 'none';
|
||||
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 filas = Array.from(document.querySelectorAll('#tablaViat tbody tr[data-id]'));
|
||||
if (!filas.length) return;
|
||||
var mismaCol = estado.col === idx;
|
||||
estado.asc = mismaCol ? !estado.asc : true; estado.col = idx;
|
||||
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 valA = (a.querySelectorAll('td')[idx]||{innerText:''}).innerText.trim().toLowerCase();
|
||||
var valB = (b.querySelectorAll('td')[idx]||{innerText:''}).innerText.trim().toLowerCase();
|
||||
var numA = parseFloat(valA.replace(/[^0-9.-]/g,'')), numB = parseFloat(valB.replace(/[^0-9.-]/g,''));
|
||||
var r = (!isNaN(numA)&&!isNaN(numB)) ? numA-numB : valA.localeCompare(valB,'es');
|
||||
return estado.asc ? r : -r;
|
||||
});
|
||||
var tb = document.querySelector('#tablaViat tbody');
|
||||
filas.forEach(function(f){ tb.appendChild(f); });
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
{% if rows %}
|
||||
{% for r in rows %}
|
||||
<tr data-id="{{ r['id'] }}"
|
||||
onclick="window.location='/vehiculos/{{ r['id'] }}/editar?origen=viat&q_viat={{ q|urlencode }}'"∫
|
||||
style="cursor:pointer;"
|
||||
onmouseover="this.style.background='var(--pill-bg)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<td><strong>{{ r['viat'] or '—' }}</strong></td>
|
||||
<td>{{ r['m_oficial'] or '—' }}</td>
|
||||
<td>{{ r['m_civil'] or '—' }}</td>
|
||||
<td>{{ r['marca'] or '—' }}</td>
|
||||
<td>{{ r['modelo'] or '—' }}</td>
|
||||
<td>{{ r['poblacion'] or '—' }}</td>
|
||||
<td>{{ r['unidad'] or '—' }}</td>
|
||||
<td>
|
||||
{% if r['tipo'] %}
|
||||
<span style="background:rgba(92,110,248,.12);color:var(--accent);font-size:11px;
|
||||
font-weight:600;padding:3px 9px;border-radius:20px;
|
||||
border:1px solid rgba(92,110,248,.25)">{{ r['tipo'] }}</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
|
||||
style="width:36px;height:36px;margin-bottom:10px;opacity:.3">
|
||||
<rect x="1" y="3" width="15" height="13" rx="2"/>
|
||||
<path d="M16 8h4l3 3v5h-7V8z"/>
|
||||
<circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/>
|
||||
</svg>
|
||||
{% if q %}
|
||||
No se encontraron resultados para <strong>"{{ q }}"</strong>.
|
||||
{% else %}
|
||||
No hay vehículos con Via-T registrado.
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user