fcefd22334
Primera version estable para monitorear viales en diferentes planta. Cada planta se configura con el archivo configurador:monitor
1081 lines
36 KiB
HTML
1081 lines
36 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Configurador JSON v11 - Monitor de viales</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f172a;
|
|
--bg2: #12243a;
|
|
--panel: #172033;
|
|
--panel2: #111827;
|
|
--line: #334155;
|
|
--line2: #38bdf8;
|
|
--text: #e5edf7;
|
|
--muted: #94a3b8;
|
|
--accent: #f59e0b;
|
|
--ok: #22c55e;
|
|
--bad: #ef4444;
|
|
--input: #0b1220;
|
|
--button: #1e3a5f;
|
|
--button2: #1f2937;
|
|
}
|
|
|
|
body.light {
|
|
--bg: #eef3f8;
|
|
--bg2: #d8e6f2;
|
|
--panel: #ffffff;
|
|
--panel2: #f8fafc;
|
|
--line: #cbd5e1;
|
|
--line2: #0284c7;
|
|
--text: #0f172a;
|
|
--muted: #475569;
|
|
--accent: #b45309;
|
|
--ok: #15803d;
|
|
--bad: #b91c1c;
|
|
--input: #ffffff;
|
|
--button: #2563eb;
|
|
--button2: #e2e8f0;
|
|
}
|
|
|
|
body[data-style="salvia"] {
|
|
--bg: #101b18;
|
|
--bg2: #182721;
|
|
--panel: #17241f;
|
|
--panel2: #1d3029;
|
|
--line: #36564c;
|
|
--line2: #7bc4a8;
|
|
--text: #edf8f2;
|
|
--muted: #9ebaae;
|
|
--accent: #8fd6b8;
|
|
--button: #21463b;
|
|
--button2: #1a3029;
|
|
}
|
|
|
|
body.light[data-style="salvia"] {
|
|
--bg: #eef7f1;
|
|
--bg2: #dfeee6;
|
|
--panel: #fbfffc;
|
|
--panel2: #eef8f2;
|
|
--line: #bdd6ca;
|
|
--line2: #4f9d7e;
|
|
--text: #13251f;
|
|
--muted: #547067;
|
|
--accent: #2f8f6f;
|
|
--button: #2f8f6f;
|
|
--button2: #e3f2ea;
|
|
}
|
|
|
|
body[data-style="bruma"] {
|
|
--bg: #101821;
|
|
--bg2: #192735;
|
|
--panel: #172331;
|
|
--panel2: #1d3041;
|
|
--line: #344f63;
|
|
--line2: #92b7c9;
|
|
--text: #edf5f8;
|
|
--muted: #9fb2bd;
|
|
--accent: #a8cfe0;
|
|
--button: #243f52;
|
|
--button2: #1b2d3a;
|
|
}
|
|
|
|
body.light[data-style="bruma"] {
|
|
--bg: #f1f6f8;
|
|
--bg2: #ddeaf0;
|
|
--panel: #ffffff;
|
|
--panel2: #f3f8fb;
|
|
--line: #c5d6df;
|
|
--line2: #6d9ab0;
|
|
--text: #14232d;
|
|
--muted: #5c707b;
|
|
--accent: #477f98;
|
|
--button: #477f98;
|
|
--button2: #e7f1f6;
|
|
}
|
|
|
|
body[data-style="calma"] {
|
|
--bg: #181716;
|
|
--bg2: #27231f;
|
|
--panel: #22201d;
|
|
--panel2: #302b26;
|
|
--line: #574b40;
|
|
--line2: #d2b48c;
|
|
--text: #f7f0e8;
|
|
--muted: #c4b6a6;
|
|
--accent: #e6c79c;
|
|
--button: #49392b;
|
|
--button2: #2e261f;
|
|
}
|
|
|
|
body.light[data-style="calma"] {
|
|
--bg: #f7f4ef;
|
|
--bg2: #ebe3d7;
|
|
--panel: #fffdf9;
|
|
--panel2: #f7f0e6;
|
|
--line: #d8c8b5;
|
|
--line2: #a47c54;
|
|
--text: #2a241e;
|
|
--muted: #756757;
|
|
--accent: #9a6a3d;
|
|
--button: #9a6a3d;
|
|
--button2: #efe4d5;
|
|
}
|
|
|
|
body[data-style="selva"] {
|
|
--bg: #071611;
|
|
--bg2: #0f2a1f;
|
|
--panel: #10251d;
|
|
--panel2: #17372a;
|
|
--line: #285f49;
|
|
--line2: #34d399;
|
|
--text: #edfff6;
|
|
--muted: #a5cdbd;
|
|
--accent: #5ee0aa;
|
|
--button: #14533f;
|
|
--button2: #0e2f25;
|
|
}
|
|
|
|
body.light[data-style="selva"] {
|
|
--bg: #ecfdf3;
|
|
--bg2: #d4f5e2;
|
|
--panel: #fbfffc;
|
|
--panel2: #eaf9f0;
|
|
--line: #a7d8bf;
|
|
--line2: #10b981;
|
|
--text: #0f281f;
|
|
--muted: #4d6f61;
|
|
--accent: #047857;
|
|
--button: #047857;
|
|
--button2: #d8f3e5;
|
|
}
|
|
|
|
body[data-style="aurora"] {
|
|
--bg: #1b1118;
|
|
--bg2: #2a1724;
|
|
--panel: #241620;
|
|
--panel2: #341d2c;
|
|
--line: #614258;
|
|
--line2: #f0a6c8;
|
|
--text: #fff1f7;
|
|
--muted: #d2a9bc;
|
|
--accent: #f2b6d0;
|
|
--button: #553044;
|
|
--button2: #33202c;
|
|
}
|
|
|
|
body.light[data-style="aurora"] {
|
|
--bg: #fff1f6;
|
|
--bg2: #f8dce8;
|
|
--panel: #fffafd;
|
|
--panel2: #fbedf3;
|
|
--line: #e5b9cc;
|
|
--line2: #d96c9f;
|
|
--text: #321d29;
|
|
--muted: #7a5a69;
|
|
--accent: #b83273;
|
|
--button: #b83273;
|
|
--button2: #f5d8e5;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: "Segoe UI", Roboto, Arial, sans-serif;
|
|
background: linear-gradient(135deg, var(--bg), var(--bg2));
|
|
color: var(--text);
|
|
}
|
|
|
|
.page {
|
|
width: min(1500px, calc(100% - 28px));
|
|
margin: 0 auto;
|
|
padding: 16px 0 24px;
|
|
}
|
|
|
|
.topbar, .panel {
|
|
border: 1px solid var(--line);
|
|
border-radius: 14px;
|
|
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
|
box-shadow: 0 14px 34px rgba(0,0,0,0.28);
|
|
}
|
|
|
|
.topbar {
|
|
display: grid;
|
|
grid-template-columns: minmax(280px, 1fr) auto;
|
|
gap: 14px;
|
|
align-items: center;
|
|
padding: 16px;
|
|
margin-bottom: 14px;
|
|
position: relative;
|
|
z-index: 50;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 1.35rem;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.subtitle {
|
|
margin-top: 4px;
|
|
color: var(--muted);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
}
|
|
|
|
button {
|
|
height: 36px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 9px;
|
|
padding: 0 12px;
|
|
background: var(--button);
|
|
color: var(--text);
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
button:hover { border-color: var(--line2); }
|
|
button.secondary { background: var(--button2); }
|
|
body.light button.secondary { color: #0f172a; }
|
|
button.danger { background: rgba(127,29,29,0.75); }
|
|
button.small { height: 30px; padding: 0 9px; font-size: 0.78rem; }
|
|
|
|
.iconBtn {
|
|
width: 38px;
|
|
min-width: 38px;
|
|
padding: 0;
|
|
display: inline-grid;
|
|
place-items: center;
|
|
border-radius: 10px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.styleTools { position: relative; display: inline-flex; gap: 8px; }
|
|
.styleMenu {
|
|
position: absolute;
|
|
top: 43px;
|
|
left: 0;
|
|
z-index: 5000;
|
|
width: 200px;
|
|
display: none;
|
|
gap: 7px;
|
|
padding: 10px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 14px;
|
|
background: color-mix(in srgb, var(--panel) 96%, transparent);
|
|
box-shadow: 0 16px 34px rgba(0,0,0,0.24);
|
|
}
|
|
.styleMenu.open { display: grid; }
|
|
.styleOption { width: 100%; text-align: left; }
|
|
.styleOption.active {
|
|
border-color: var(--accent);
|
|
background: color-mix(in srgb, var(--accent) 18%, var(--button));
|
|
}
|
|
|
|
input, select, textarea {
|
|
width: 100%;
|
|
min-width: 0;
|
|
height: 36px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 0 10px;
|
|
background: var(--input);
|
|
color: var(--text);
|
|
outline: none;
|
|
font: inherit;
|
|
}
|
|
|
|
input:focus, select:focus, textarea:focus {
|
|
border-color: var(--line2);
|
|
box-shadow: 0 0 0 3px rgba(56,189,248,0.14);
|
|
}
|
|
|
|
textarea {
|
|
height: 88px;
|
|
padding: 9px 10px;
|
|
resize: vertical;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
color: var(--muted);
|
|
font-size: 0.76rem;
|
|
font-weight: 800;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: 310px minmax(0, 1fr);
|
|
gap: 14px;
|
|
align-items: start;
|
|
}
|
|
|
|
.panel { padding: 14px; }
|
|
|
|
.panelTitle {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.panelTitle h2 {
|
|
margin: 0;
|
|
color: var(--accent);
|
|
font-size: 1rem;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.plantList {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.plantBtn {
|
|
width: 100%;
|
|
height: auto;
|
|
min-height: 48px;
|
|
text-align: left;
|
|
background: color-mix(in srgb, var(--panel2) 72%, transparent);
|
|
}
|
|
|
|
.plantBtn.active {
|
|
border-color: var(--accent);
|
|
background: rgba(245,158,11,0.14);
|
|
}
|
|
|
|
.plantBtn span:first-child {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 28px;
|
|
height: 24px;
|
|
padding: 0 8px;
|
|
border-radius: 999px;
|
|
background: rgba(56,189,248,0.14);
|
|
color: #bae6fd;
|
|
font-size: 0.78rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
body.light .badge { color: #075985; }
|
|
|
|
.formGrid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.wide2 { grid-column: span 2; }
|
|
.wide4 { grid-column: 1 / -1; }
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
margin-top: 8px;
|
|
overflow: hidden;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
th, td {
|
|
border-bottom: 1px solid var(--line);
|
|
padding: 8px;
|
|
vertical-align: middle;
|
|
background: color-mix(in srgb, var(--panel2) 70%, transparent);
|
|
}
|
|
|
|
th {
|
|
color: var(--muted);
|
|
font-size: 0.74rem;
|
|
text-align: left;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
td.actions {
|
|
width: 90px;
|
|
text-align: right;
|
|
}
|
|
|
|
.sectionHead {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin-top: 14px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.sectionHead h3 {
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
letter-spacing: 0.06em;
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.status {
|
|
min-height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 10px;
|
|
margin-top: 12px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
color: var(--muted);
|
|
background: color-mix(in srgb, var(--panel2) 72%, transparent);
|
|
font-size: 0.86rem;
|
|
}
|
|
|
|
.status.ok { color: #bbf7d0; border-color: rgba(34,197,94,0.45); }
|
|
.status.bad { color: #fecaca; border-color: rgba(239,68,68,0.55); }
|
|
|
|
.jsonPreview {
|
|
height: 360px;
|
|
font-family: Consolas, "Courier New", monospace;
|
|
font-size: 0.82rem;
|
|
white-space: pre;
|
|
}
|
|
|
|
.fileInput { display: none; }
|
|
|
|
@media (max-width: 1100px) {
|
|
.topbar, .grid, .formGrid { grid-template-columns: 1fr; }
|
|
.wide2, .wide4 { grid-column: auto; }
|
|
.toolbar { justify-content: flex-start; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<input id="fileInput" class="fileInput" type="file" accept="application/json,.json" />
|
|
<div class="page">
|
|
<header class="topbar">
|
|
<div>
|
|
<h1>Configurador JSON v11</h1>
|
|
<div class="subtitle">Editor de plantas, viales, tótems, tablas y tarjetas para el monitor.</div>
|
|
</div>
|
|
<div class="toolbar">
|
|
<div class="styleTools">
|
|
<button id="styleBtn" class="secondary iconBtn" type="button" title="Cambiar estilo" aria-label="Cambiar estilo">◈</button>
|
|
<div id="styleMenu" class="styleMenu" aria-label="Selector de estilo">
|
|
<button class="styleOption" type="button" data-style-option="base">Azul sereno</button>
|
|
<button class="styleOption" type="button" data-style-option="salvia">Salvia técnica</button>
|
|
<button class="styleOption" type="button" data-style-option="bruma">Bruma gris</button>
|
|
<button class="styleOption" type="button" data-style-option="calma">Calma cálida</button>
|
|
<button class="styleOption" type="button" data-style-option="selva">Selva tropical</button>
|
|
<button class="styleOption" type="button" data-style-option="aurora">Aurora rosa</button>
|
|
</div>
|
|
<button id="themeBtn" class="secondary iconBtn" type="button" title="Cambiar claro/oscuro" aria-label="Cambiar claro/oscuro">☼</button>
|
|
</div>
|
|
<button id="loadFileBtn" type="button">Cargar configuración JSON</button>
|
|
<button id="downloadBtn" type="button">Exportar configuración JSON</button>
|
|
<button id="copyBtn" class="secondary" type="button">Copiar JSON</button>
|
|
<button id="resetBtn" class="danger" type="button">Restaurar base</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="grid">
|
|
<aside class="panel">
|
|
<div class="panelTitle">
|
|
<h2>PLANTAS</h2>
|
|
<button id="addPlantBtn" class="small" type="button">Añadir</button>
|
|
</div>
|
|
<div id="plantList" class="plantList"></div>
|
|
</aside>
|
|
|
|
<section class="panel">
|
|
<div class="panelTitle">
|
|
<h2>DATOS DE PLANTA</h2>
|
|
<button id="deletePlantBtn" class="small danger" type="button">Eliminar planta</button>
|
|
</div>
|
|
|
|
<div class="formGrid">
|
|
<div>
|
|
<label for="plantName">Nombre planta</label>
|
|
<input id="plantName" autocomplete="off" />
|
|
</div>
|
|
<div class="wide2">
|
|
<label for="plantGuid">IdCentroProveedor / GUID</label>
|
|
<input id="plantGuid" autocomplete="off" />
|
|
</div>
|
|
<div>
|
|
<label for="plantVials">Viales</label>
|
|
<input id="plantVials" type="number" min="1" step="1" />
|
|
</div>
|
|
<div>
|
|
<label for="supervisorName">Nombre supervisor</label>
|
|
<input id="supervisorName" autocomplete="off" />
|
|
</div>
|
|
<div class="wide2">
|
|
<label for="websocketUrl">WebSocket supervisor</label>
|
|
<input id="websocketUrl" autocomplete="off" />
|
|
</div>
|
|
<div>
|
|
<label for="initialPlant">Planta inicial</label>
|
|
<select id="initialPlant"></select>
|
|
</div>
|
|
<div class="wide4">
|
|
<label for="plantTables">Tablas de esta planta, una por línea</label>
|
|
<textarea id="plantTables" spellcheck="false"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sectionHead">
|
|
<h3>TÓTEMS DE LA PLANTA</h3>
|
|
<button id="addTotemBtn" class="small" type="button">Añadir tótem</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 16%;">ID</th>
|
|
<th style="width: 16%;">Nombre</th>
|
|
<th style="width: 20%;">IP</th>
|
|
<th>URL pantalla</th>
|
|
<th style="width: 92px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="totemRows"></tbody>
|
|
</table>
|
|
|
|
<div class="sectionHead">
|
|
<h3>TARJETAS INICIALES</h3>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 20%;">Tarjeta</th>
|
|
<th style="width: 14%;">Vial</th>
|
|
<th>Tótem 1</th>
|
|
<th>Tótem 2</th>
|
|
<th style="width: 18%;">Pantalla</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="cardRows"></tbody>
|
|
</table>
|
|
|
|
<div class="sectionHead">
|
|
<h3>JSON GENERADO</h3>
|
|
<button id="refreshPreviewBtn" class="small secondary" type="button">Actualizar vista</button>
|
|
</div>
|
|
<textarea id="jsonPreview" class="jsonPreview" spellcheck="false" readonly></textarea>
|
|
<div id="status" class="status">Listo.</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
const BASE_CONFIG = {
|
|
"version": 10,
|
|
"plantaInicial": "02C9381D-D4A0-4503-B9A4-890BFABD9608",
|
|
"plantas": [
|
|
{
|
|
"nombre": "CANTABRIA",
|
|
"idCentroProveedor": "02C9381D-D4A0-4503-B9A4-890BFABD9608",
|
|
"supervisor": {
|
|
"nombre": "Supervisor LOCAL",
|
|
"websocketUrl": "ws://127.0.0.1:25009/"
|
|
},
|
|
"viales": 4,
|
|
"totems": [
|
|
{ "id": "totem_1", "nombre": "entrada", "ip": "192.168.0.29", "url": "http://192.168.0.29:1880/ui?monitor=1" },
|
|
{ "id": "totem_2", "nombre": "salida", "ip": "127.0.0.1:25054", "url": "http://127.0.0.1:25054/ui?monitor=1" },
|
|
{ "id": "totem_3", "nombre": "entrada", "ip": "127.0.0.1:25059", "url": "http://127.0.0.1:25059/ui?monitor=1" },
|
|
{ "id": "totem_4", "nombre": "salida", "ip": "192.168.0.29", "url": "http://192.168.0.29:1880/ui?monitor=1" },
|
|
{ "id": "totem_5", "nombre": "entrada", "ip": "192.168.0.29", "url": "http://192.168.0.29:1880/ui?monitor=1" }
|
|
],
|
|
"tablas": ["Matriculas", "Identificadores", "MatriculasIdentificadores", "UbicacionesInternas"]
|
|
},
|
|
{
|
|
"nombre": "ZARAGOZA",
|
|
"idCentroProveedor": "02C9381D-D4A0-4503-B9A4-890BFABD9608",
|
|
"supervisor": {
|
|
"nombre": "Supervisor",
|
|
"websocketUrl": "ws://127.0.0.1:25009/"
|
|
},
|
|
"viales": 3,
|
|
"totems": [
|
|
{ "id": "totem_1", "nombre": "entrada", "ip": "192.168.0.29", "url": "http://192.168.0.29:1880/ui?monitor=1" },
|
|
{ "id": "totem_2", "nombre": "salida", "ip": "127.0.0.1:25054", "url": "http://127.0.0.1:25054/ui?monitor=1" },
|
|
{ "id": "totem_3", "nombre": "entrada", "ip": "127.0.0.1:25059", "url": "http://127.0.0.1:25059/ui?monitor=1" },
|
|
{ "id": "totem_4", "nombre": "salida", "ip": "192.168.0.29", "url": "http://192.168.0.29:1880/ui?monitor=1" }
|
|
],
|
|
"tablas": ["Matriculas", "Identificadores", "MatriculasIdentificadores", "UbicacionesInternas"]
|
|
}
|
|
],
|
|
"pantalla": {
|
|
"tarjetas": [
|
|
{ "id": "card_1", "vialNumero": 1, "totemId": "totem_1", "totemScreenEnabled": true },
|
|
{ "id": "card_2", "vialNumero": 2, "totemId": "totem_2", "totemScreenEnabled": true },
|
|
{ "id": "card_3", "vialNumero": 3, "totemIds": ["totem_3", "totem_4"], "totemScreenEnabled": true }
|
|
]
|
|
}
|
|
};
|
|
|
|
const $ = selector => document.querySelector(selector);
|
|
const els = {
|
|
fileInput: $('#fileInput'),
|
|
styleBtn: $('#styleBtn'),
|
|
styleMenu: $('#styleMenu'),
|
|
themeBtn: $('#themeBtn'),
|
|
loadFileBtn: $('#loadFileBtn'),
|
|
downloadBtn: $('#downloadBtn'),
|
|
copyBtn: $('#copyBtn'),
|
|
resetBtn: $('#resetBtn'),
|
|
addPlantBtn: $('#addPlantBtn'),
|
|
deletePlantBtn: $('#deletePlantBtn'),
|
|
plantList: $('#plantList'),
|
|
plantName: $('#plantName'),
|
|
plantGuid: $('#plantGuid'),
|
|
plantVials: $('#plantVials'),
|
|
supervisorName: $('#supervisorName'),
|
|
websocketUrl: $('#websocketUrl'),
|
|
initialPlant: $('#initialPlant'),
|
|
plantTables: $('#plantTables'),
|
|
addTotemBtn: $('#addTotemBtn'),
|
|
totemRows: $('#totemRows'),
|
|
cardRows: $('#cardRows'),
|
|
jsonPreview: $('#jsonPreview'),
|
|
refreshPreviewBtn: $('#refreshPreviewBtn'),
|
|
status: $('#status')
|
|
};
|
|
|
|
let config = clone(BASE_CONFIG);
|
|
let selectedPlantIndex = 0;
|
|
let theme = localStorage.getItem('configuradorMonitorV11.theme') || 'dark';
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function setStatus(message, type = '') {
|
|
els.status.textContent = message;
|
|
els.status.className = 'status' + (type ? ' ' + type : '');
|
|
}
|
|
|
|
const VISUAL_STYLES = new Set(['base', 'salvia', 'bruma', 'calma', 'selva', 'aurora']);
|
|
|
|
function applyVisualStyle(style) {
|
|
const next = VISUAL_STYLES.has(style) ? style : 'base';
|
|
document.body.dataset.style = next;
|
|
localStorage.setItem('configuradorMonitorV11.visualStyle', next);
|
|
document.querySelectorAll('[data-style-option]').forEach(btn => {
|
|
const active = btn.dataset.styleOption === next;
|
|
btn.classList.toggle('active', active);
|
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
});
|
|
}
|
|
|
|
function applyTheme(nextTheme) {
|
|
theme = nextTheme === 'light' ? 'light' : 'dark';
|
|
document.body.classList.toggle('light', theme === 'light');
|
|
els.themeBtn.textContent = theme === 'light' ? '☾' : '☼';
|
|
els.themeBtn.title = theme === 'light' ? 'Cambiar a modo oscuro' : 'Cambiar a modo claro';
|
|
els.themeBtn.setAttribute('aria-label', els.themeBtn.title);
|
|
localStorage.setItem('configuradorMonitorV11.theme', theme);
|
|
}
|
|
|
|
function plantKey(plant, index) {
|
|
return `${plant.nombre || 'PLANTA'}__${plant.idCentroProveedor || ''}__${index}`;
|
|
}
|
|
|
|
function selectedPlant() {
|
|
return config.plantas[selectedPlantIndex] || null;
|
|
}
|
|
|
|
function normalizeConfig(raw) {
|
|
const cfg = raw && typeof raw === 'object' ? raw : {};
|
|
const plants = Array.isArray(cfg.plantas) ? cfg.plantas : [];
|
|
return {
|
|
version: 10,
|
|
plantaInicial: String(cfg.plantaInicial || plants[0]?.idCentroProveedor || '').trim(),
|
|
plantas: plants.map((plant, index) => ({
|
|
nombre: String(plant.nombre || `PLANTA ${index + 1}`).trim(),
|
|
idCentroProveedor: String(plant.idCentroProveedor || '').trim(),
|
|
supervisor: {
|
|
nombre: String(plant.supervisor?.nombre || '').trim(),
|
|
websocketUrl: String(plant.supervisor?.websocketUrl || '').trim()
|
|
},
|
|
viales: Math.max(1, Number(plant.viales) || 1),
|
|
totems: Array.isArray(plant.totems) ? plant.totems.map((totem, totemIndex) => ({
|
|
id: String(totem.id || `totem_${totemIndex + 1}`).trim(),
|
|
nombre: String(totem.nombre || '').trim(),
|
|
ip: String(totem.ip || '').trim(),
|
|
url: String(totem.url || '').trim()
|
|
})) : [],
|
|
tablas: Array.isArray(plant.tablas) ? plant.tablas.map(String).filter(Boolean) : []
|
|
})),
|
|
pantalla: {
|
|
tarjetas: Array.isArray(cfg.pantalla?.tarjetas) ? cfg.pantalla.tarjetas.map((card, index) => ({
|
|
id: String(card.id || `card_${index + 1}`).trim(),
|
|
vialNumero: String(card.vialNumero ?? '').trim() === '' ? '' : Math.max(1, Number(card.vialNumero) || 1),
|
|
totemScreenEnabled: card.totemScreenEnabled !== false,
|
|
...(Array.isArray(card.totemIds)
|
|
? { totemIds: card.totemIds.map(String).filter(Boolean) }
|
|
: { totemId: String(card.totemId || '').trim() })
|
|
})) : clone(BASE_CONFIG.pantalla.tarjetas)
|
|
}
|
|
};
|
|
}
|
|
|
|
function buildDerivedConfig() {
|
|
const cfg = normalizeConfig(config);
|
|
const firstPlant = cfg.plantas[0];
|
|
cfg.websocketUrls = cfg.plantas
|
|
.filter(plant => plant.supervisor.websocketUrl)
|
|
.map(plant => ({ nombre: plant.supervisor.nombre || plant.nombre, url: plant.supervisor.websocketUrl }));
|
|
cfg.centrosProveedor = cfg.plantas
|
|
.filter(plant => plant.idCentroProveedor)
|
|
.map(plant => ({ nombre: plant.nombre, id: plant.idCentroProveedor }));
|
|
cfg.totemUrls = firstPlant
|
|
? firstPlant.totems.filter(totem => totem.url).map(totem => ({
|
|
nombre: `${totem.id}${totem.nombre ? ' - ' + totem.nombre : ''}`,
|
|
url: totem.url
|
|
}))
|
|
: [];
|
|
cfg.tablas = firstPlant ? [...firstPlant.tablas] : [];
|
|
return cfg;
|
|
}
|
|
|
|
function renderPlantList() {
|
|
els.plantList.innerHTML = '';
|
|
config.plantas.forEach((plant, index) => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'plantBtn' + (index === selectedPlantIndex ? ' active' : '');
|
|
button.innerHTML = `<span>${plant.nombre || 'Sin nombre'}</span><span class="badge">${plant.viales || 1}</span>`;
|
|
button.addEventListener('click', () => {
|
|
savePlantForm();
|
|
selectedPlantIndex = index;
|
|
renderAll();
|
|
});
|
|
els.plantList.appendChild(button);
|
|
});
|
|
}
|
|
|
|
function renderInitialPlantSelect() {
|
|
const current = config.plantaInicial || config.plantas[0]?.idCentroProveedor || '';
|
|
els.initialPlant.innerHTML = '';
|
|
config.plantas.forEach((plant, index) => {
|
|
const option = document.createElement('option');
|
|
option.value = String(index);
|
|
option.textContent = `${plant.nombre || 'PLANTA'} - ${plant.idCentroProveedor || 'sin GUID'}`;
|
|
els.initialPlant.appendChild(option);
|
|
});
|
|
const match = config.plantas.findIndex(plant => plant.idCentroProveedor === current);
|
|
els.initialPlant.value = String(match >= 0 ? match : 0);
|
|
}
|
|
|
|
function renderPlantForm() {
|
|
const plant = selectedPlant();
|
|
if (!plant) return;
|
|
els.plantName.value = plant.nombre || '';
|
|
els.plantGuid.value = plant.idCentroProveedor || '';
|
|
els.plantVials.value = plant.viales || 1;
|
|
els.supervisorName.value = plant.supervisor?.nombre || '';
|
|
els.websocketUrl.value = plant.supervisor?.websocketUrl || '';
|
|
els.plantTables.value = (plant.tablas || []).join('\n');
|
|
renderInitialPlantSelect();
|
|
renderTotems();
|
|
renderCards();
|
|
}
|
|
|
|
function savePlantForm() {
|
|
const plant = selectedPlant();
|
|
if (!plant) return;
|
|
plant.nombre = els.plantName.value.trim();
|
|
plant.idCentroProveedor = els.plantGuid.value.trim();
|
|
plant.viales = Math.max(1, Number(els.plantVials.value) || 1);
|
|
plant.supervisor = {
|
|
nombre: els.supervisorName.value.trim(),
|
|
websocketUrl: els.websocketUrl.value.trim()
|
|
};
|
|
plant.tablas = els.plantTables.value.split(/\r?\n/).map(x => x.trim()).filter(Boolean);
|
|
const initial = config.plantas[Number(els.initialPlant.value)] || config.plantas[0];
|
|
config.plantaInicial = initial ? initial.idCentroProveedor : '';
|
|
}
|
|
|
|
function renderTotems() {
|
|
const plant = selectedPlant();
|
|
els.totemRows.innerHTML = '';
|
|
(plant.totems || []).forEach((totem, index) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td><input data-totem-field="id" data-index="${index}" value="${escapeAttr(totem.id)}" /></td>
|
|
<td><input data-totem-field="nombre" data-index="${index}" value="${escapeAttr(totem.nombre)}" /></td>
|
|
<td><input data-totem-field="ip" data-index="${index}" value="${escapeAttr(totem.ip)}" /></td>
|
|
<td><input data-totem-field="url" data-index="${index}" value="${escapeAttr(totem.url)}" /></td>
|
|
<td class="actions"><button class="small danger" type="button" data-delete-totem="${index}">Eliminar</button></td>
|
|
`;
|
|
els.totemRows.appendChild(tr);
|
|
});
|
|
els.totemRows.querySelectorAll('input').forEach(input => input.addEventListener('input', updateTotemFromInput));
|
|
els.totemRows.querySelectorAll('[data-delete-totem]').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
plant.totems.splice(Number(button.dataset.deleteTotem), 1);
|
|
renderTotems();
|
|
renderCards();
|
|
updatePreview();
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateTotemFromInput(event) {
|
|
const plant = selectedPlant();
|
|
const index = Number(event.target.dataset.index);
|
|
const field = event.target.dataset.totemField;
|
|
if (!plant || !plant.totems[index]) return;
|
|
plant.totems[index][field] = event.target.value.trim();
|
|
renderCards();
|
|
updatePreview();
|
|
}
|
|
|
|
function renderCards() {
|
|
const plant = selectedPlant();
|
|
const totems = plant ? plant.totems : [];
|
|
els.cardRows.innerHTML = '';
|
|
config.pantalla.tarjetas.forEach((card, index) => {
|
|
const ids = Array.isArray(card.totemIds) ? card.totemIds : [card.totemId || ''];
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${escapeHtml(card.id || `card_${index + 1}`)}</td>
|
|
<td>${vialSelectHtml(index, card.vialNumero, plant?.viales || 1)}</td>
|
|
<td>${totemSelectHtml(index, 0, ids[0] || '', totems)}</td>
|
|
<td>${index === 2 ? totemSelectHtml(index, 1, ids[1] || '', totems) : ''}</td>
|
|
<td>${screenPowerSelectHtml(index, card.totemScreenEnabled !== false)}</td>
|
|
`;
|
|
els.cardRows.appendChild(tr);
|
|
});
|
|
els.cardRows.querySelectorAll('select').forEach(select => select.addEventListener('change', updateCardFromSelect));
|
|
}
|
|
|
|
function vialSelectHtml(cardIndex, value, max) {
|
|
const selectedValue = String(value ?? '').trim();
|
|
const options = [`<option value=""${selectedValue === '' ? ' selected' : ''}></option>`];
|
|
for (let i = 1; i <= Math.max(1, Number(max) || 1); i += 1) {
|
|
options.push(`<option value="${i}"${selectedValue !== '' && Number(selectedValue) === i ? ' selected' : ''}>${i}</option>`);
|
|
}
|
|
return `<select data-card="${cardIndex}" data-card-field="vialNumero">${options.join('')}</select>`;
|
|
}
|
|
|
|
function totemSelectHtml(cardIndex, slot, value, totems) {
|
|
const options = ['<option value=""></option>'].concat(totems.map(totem => (
|
|
`<option value="${escapeAttr(totem.id)}"${totem.id === value ? ' selected' : ''}>${escapeHtml(totem.id)}</option>`
|
|
)));
|
|
return `<select data-card="${cardIndex}" data-card-field="totem" data-slot="${slot}">${options.join('')}</select>`;
|
|
}
|
|
|
|
function screenPowerSelectHtml(cardIndex, enabled) {
|
|
return `<select data-card="${cardIndex}" data-card-field="totemScreenEnabled">
|
|
<option value="true"${enabled ? ' selected' : ''}>ON</option>
|
|
<option value="false"${!enabled ? ' selected' : ''}>OFF</option>
|
|
</select>`;
|
|
}
|
|
|
|
function updateCardFromSelect(event) {
|
|
const card = config.pantalla.tarjetas[Number(event.target.dataset.card)];
|
|
if (!card) return;
|
|
if (event.target.dataset.cardField === 'vialNumero') {
|
|
card.vialNumero = event.target.value === '' ? '' : Number(event.target.value);
|
|
} else if (event.target.dataset.cardField === 'totemScreenEnabled') {
|
|
card.totemScreenEnabled = event.target.value === 'true';
|
|
} else {
|
|
const slot = Number(event.target.dataset.slot);
|
|
if (Number(event.target.dataset.card) === 2) {
|
|
const ids = Array.isArray(card.totemIds) ? [...card.totemIds] : [card.totemId || '', ''];
|
|
ids[slot] = event.target.value;
|
|
delete card.totemId;
|
|
card.totemIds = ids.filter(Boolean);
|
|
} else {
|
|
delete card.totemIds;
|
|
card.totemId = event.target.value;
|
|
}
|
|
}
|
|
updatePreview();
|
|
}
|
|
|
|
function validateConfig(cfg) {
|
|
const errors = [];
|
|
if (!cfg.plantas.length) errors.push('Debe existir al menos una planta.');
|
|
cfg.plantas.forEach(plant => {
|
|
if (!plant.nombre) errors.push('Hay una planta sin nombre.');
|
|
if (!plant.idCentroProveedor) errors.push(`La planta ${plant.nombre || '?'} no tiene GUID.`);
|
|
if (!plant.supervisor.websocketUrl) errors.push(`La planta ${plant.nombre || '?'} no tiene WebSocket.`);
|
|
if (!plant.totems.length) errors.push(`La planta ${plant.nombre || '?'} no tiene tótems.`);
|
|
});
|
|
const allTotems = new Set(cfg.plantas.flatMap(plant => plant.totems.map(totem => totem.id)));
|
|
cfg.pantalla.tarjetas.forEach(card => {
|
|
const ids = Array.isArray(card.totemIds) ? card.totemIds : [card.totemId];
|
|
ids.filter(Boolean).forEach(id => {
|
|
if (!allTotems.has(id)) errors.push(`${card.id} referencia un tótem inexistente: ${id}`);
|
|
});
|
|
});
|
|
return errors;
|
|
}
|
|
|
|
function updatePreview() {
|
|
savePlantForm();
|
|
const cfg = buildDerivedConfig();
|
|
const errors = validateConfig(cfg);
|
|
els.jsonPreview.value = JSON.stringify(cfg, null, 2);
|
|
setStatus(errors.length ? errors.join(' ') : 'JSON v11 válido para exportar.', errors.length ? 'bad' : 'ok');
|
|
return { cfg, errors };
|
|
}
|
|
|
|
function addPlant() {
|
|
savePlantForm();
|
|
config.plantas.push({
|
|
nombre: `PLANTA ${config.plantas.length + 1}`,
|
|
idCentroProveedor: '',
|
|
supervisor: { nombre: 'Supervisor', websocketUrl: 'ws://127.0.0.1:25009/' },
|
|
viales: 1,
|
|
totems: [],
|
|
tablas: ['Matriculas', 'Identificadores', 'MatriculasIdentificadores', 'UbicacionesInternas']
|
|
});
|
|
selectedPlantIndex = config.plantas.length - 1;
|
|
renderAll();
|
|
}
|
|
|
|
function deletePlant() {
|
|
if (config.plantas.length <= 1) {
|
|
setStatus('No se puede eliminar la última planta.', 'bad');
|
|
return;
|
|
}
|
|
const plant = selectedPlant();
|
|
if (!confirm(`¿Eliminar la planta ${plant?.nombre || selectedPlantIndex + 1}?`)) return;
|
|
config.plantas.splice(selectedPlantIndex, 1);
|
|
selectedPlantIndex = Math.max(0, selectedPlantIndex - 1);
|
|
renderAll();
|
|
}
|
|
|
|
function addTotem() {
|
|
const plant = selectedPlant();
|
|
const next = (plant.totems || []).length + 1;
|
|
plant.totems.push({ id: `totem_${next}`, nombre: '', ip: '', url: '' });
|
|
renderTotems();
|
|
renderCards();
|
|
updatePreview();
|
|
}
|
|
|
|
function downloadConfig() {
|
|
const { cfg, errors } = updatePreview();
|
|
if (errors.length && !confirm('Hay avisos de validación. ¿Descargar igualmente?')) return;
|
|
const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'monitor_config.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
async function copyConfig() {
|
|
const { cfg } = updatePreview();
|
|
await navigator.clipboard.writeText(JSON.stringify(cfg, null, 2));
|
|
setStatus('JSON copiado al portapapeles.', 'ok');
|
|
}
|
|
|
|
function loadFile(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
try {
|
|
config = normalizeConfig(JSON.parse(String(reader.result || '{}')));
|
|
selectedPlantIndex = 0;
|
|
renderAll();
|
|
setStatus(`Archivo cargado: ${file.name}`, 'ok');
|
|
} catch (err) {
|
|
setStatus(`No se pudo cargar el JSON: ${err.message}`, 'bad');
|
|
}
|
|
};
|
|
reader.readAsText(file, 'utf-8');
|
|
}
|
|
|
|
function renderAll() {
|
|
config = normalizeConfig(config);
|
|
selectedPlantIndex = Math.min(selectedPlantIndex, config.plantas.length - 1);
|
|
renderPlantList();
|
|
renderPlantForm();
|
|
updatePreview();
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return escapeHtml(value);
|
|
}
|
|
|
|
els.styleBtn.addEventListener('click', event => {
|
|
event.stopPropagation();
|
|
els.styleMenu.classList.toggle('open');
|
|
});
|
|
document.querySelectorAll('[data-style-option]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
applyVisualStyle(btn.dataset.styleOption);
|
|
els.styleMenu.classList.remove('open');
|
|
});
|
|
});
|
|
document.addEventListener('click', event => {
|
|
if (!els.styleMenu.contains(event.target) && event.target !== els.styleBtn) els.styleMenu.classList.remove('open');
|
|
});
|
|
els.themeBtn.addEventListener('click', () => applyTheme(theme === 'light' ? 'dark' : 'light'));
|
|
els.loadFileBtn.addEventListener('click', () => els.fileInput.click());
|
|
els.fileInput.addEventListener('change', event => {
|
|
const file = event.target.files && event.target.files[0];
|
|
if (file) loadFile(file);
|
|
event.target.value = '';
|
|
});
|
|
els.downloadBtn.addEventListener('click', downloadConfig);
|
|
els.copyBtn.addEventListener('click', copyConfig);
|
|
els.resetBtn.addEventListener('click', () => {
|
|
if (!confirm('¿Restaurar la configuración base v11?')) return;
|
|
config = clone(BASE_CONFIG);
|
|
selectedPlantIndex = 0;
|
|
renderAll();
|
|
});
|
|
els.addPlantBtn.addEventListener('click', addPlant);
|
|
els.deletePlantBtn.addEventListener('click', deletePlant);
|
|
els.addTotemBtn.addEventListener('click', addTotem);
|
|
els.refreshPreviewBtn.addEventListener('click', updatePreview);
|
|
[els.plantName, els.plantGuid, els.plantVials, els.supervisorName, els.websocketUrl, els.initialPlant, els.plantTables]
|
|
.forEach(input => input.addEventListener('input', () => {
|
|
savePlantForm();
|
|
renderPlantList();
|
|
renderInitialPlantSelect();
|
|
renderCards();
|
|
updatePreview();
|
|
}));
|
|
els.initialPlant.addEventListener('change', updatePreview);
|
|
|
|
applyVisualStyle(localStorage.getItem('configuradorMonitorV11.visualStyle') || 'base');
|
|
applyTheme(theme);
|
|
renderAll();
|
|
</script>
|
|
</body>
|
|
</html>
|