esp/paginas/puertos.php

491 lines
21 KiB
PHP

<?php
$title = 'Puertos de Dispositivo';
ob_start();
?>
<div class="bg-white shadow-sm rounded-md overflow-hidden">
<div class="px-4 py-3 bg-blue-600 text-white">
<h5 class="m-0 font-semibold">Puertos del Dispositivo</h5>
</div>
<div class="px-4 py-4">
<h5 id="chipid-header" class="mb-4 font-semibold text-gray-800"></h5>
<!-- Acciones -->
<div class="flex flex-wrap items-center gap-2 mb-4">
<a href="/" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded border border-gray-300 text-gray-700 hover:bg-gray-100"><span class="icon icon-back" aria-hidden="true"></span><span>Volver</span></a>
<button id="btnAgregarEntidad" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded bg-cyan-600 text-white hover:bg-cyan-700"><span class="icon icon-add" aria-hidden="true"></span><span>Agregar Entidad</span></button>
<button id="btnAsignar" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded bg-blue-600 text-white hover:bg-blue-700"><span class="icon icon-assign" aria-hidden="true"></span><span>Asignar</span></button>
<span id="update-label" class="inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full bg-slate-600 text-white ring-2 ring-slate-300/50 ml-auto"></span>
</div>
<div id="lista-puertos" class="overflow-x-auto"></div>
</div>
</div>
<!-- Modal de Selección de Placa (Tailwind) -->
<div id="modalSeleccionPlaca" class="fixed inset-0 z-50 hidden">
<div data-modal-overlay class="absolute inset-0 bg-black/40"></div>
<div class="relative mx-auto mt-20 max-w-lg rounded-md bg-white shadow-lg">
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
<h5 class="m-0 font-semibold">Seleccionar Placa</h5>
<button type="button" class="text-gray-500 hover:text-gray-700" data-modal-close aria-label="Cerrar">✕</button>
</div>
<div class="p-4 space-y-3">
<label for="placaSelect" class="block text-sm text-gray-700">Selecciona el tipo de placa:</label>
<select id="placaSelect" class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-200"></select>
<div id="infoPlacaSeleccionada" class="mt-2 p-2 border border-gray-200 bg-gray-50 rounded"></div>
</div>
<div class="px-4 py-3 border-t border-gray-200 text-right space-x-2">
<button type="button" class="inline-flex items-center px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-700 hover:bg-gray-100" data-modal-close>Cerrar</button>
<button type="button" class="inline-flex items-center px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" id="btnSeleccionarPlaca">Asignar placa</button>
</div>
</div>
</div>
<!-- Modal para Agregar Nueva Entidad (Tailwind) -->
<div id="modalAgregarEntidad" class="fixed inset-0 z-50 hidden">
<div data-modal-overlay class="absolute inset-0 bg-black/40"></div>
<div class="relative mx-auto mt-20 max-w-md rounded-md bg-white shadow-lg">
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
<h5 class="m-0 font-semibold">Agregar Nueva Entidad</h5>
<button type="button" class="text-gray-500 hover:text-gray-700" data-modal-close aria-label="Cerrar">✕</button>
</div>
<div class="p-4 space-y-2">
<label for="nuevaEntidad" class="block text-sm text-gray-700">Nombre de la nueva entidad:</label>
<input type="text" id="nuevaEntidad" class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-200" placeholder="Ej. Sensor A">
</div>
<div class="px-4 py-3 border-t border-gray-200 text-right space-x-2">
<button type="button" class="inline-flex items-center px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-700 hover:bg-gray-100" data-modal-close>Cancelar</button>
<button type="button" class="inline-flex items-center px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" id="btnGuardarEntidad">Guardar</button>
</div>
</div>
</div>
<script>
// mostrarToast provisto por assets/js/toast.js
const urlParams = new URLSearchParams(window.location.search);
let chipid = urlParams.get('chipid');
if (!chipid) {
const listaPuertos = document.getElementById('lista-puertos');
if (listaPuertos) listaPuertos.innerHTML = '<div class="rounded-md border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-sm">Error: No se especificó un ChipID en la URL.</div>';
throw new Error('Falta el parámetro chipid');
}
const chipidHeader = document.getElementById('chipid-header');
if (chipidHeader) chipidHeader.textContent = `Puertos para el ChipID: ${chipid}`;
let puertosData = {};
let listaEntidades = [];
function renderTablaPuertos() {
let html = `
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Puerto</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">GPIO</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Entidad</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Modo</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Notas</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">`;
for (let puerto in puertosData) {
if (puerto === 'update') continue;
const p = puertosData[puerto];
const modoOptions = ['INPUT', 'OUTPUT', 'INPUT_PULLUP'];
let modoSelect = `<select class="modo-select w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-200">`;
modoOptions.forEach(modo => {
modoSelect += `<option value="${modo}" ${p.modo === modo ? 'selected' : ''}>${modo}</option>`;
});
modoSelect += `</select>`;
let entidadSelect = `<select class="entidad-select w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-200" data-puerto="${puerto}">`;
listaEntidades.forEach(entidad => {
const selected = (p.disp === entidad) ? 'selected' : '';
entidadSelect += `<option value="${entidad}" ${selected}>${entidad}</option>`;
});
entidadSelect += `</select>`;
html += `
<tr class="hover:bg-gray-50">
<td class="px-3 py-2">${puerto}</td>
<td class="px-3 py-2">${p.gpio}</td>
<td class="px-3 py-2">${entidadSelect}</td>
<td class="px-3 py-2">${modoSelect}</td>
<td class="px-3 py-2 editable" contenteditable="true" data-puerto="${puerto}" data-field="notas">${p.notas || ''}</td>
</tr>`;
}
html += '</tbody></table>';
const listaPuertos = document.getElementById('lista-puertos');
if (listaPuertos) listaPuertos.innerHTML = html;
try { if (window.applyIcons) window.applyIcons('#lista-puertos'); } catch {}
// Mostrar fecha de última actualización
mostrarUpdate(puertosData.update);
}
// Función para cargar las entidades desde el archivo entidades.json
function cargarEntidades(callback) {
fetch('/data/entidades.json')
.then(r => r.json())
.then(data => {
listaEntidades = data.entidades || [];
if (typeof callback === 'function') callback();
})
.catch(() => {
mostrarToast('No se pudieron cargar las entidades.');
listaEntidades = []; // fallback vacío
if (typeof callback === 'function') callback();
});
}
function cargarPlacas() {
fetch('/data/placas.json')
.then(r => r.json())
.then(data => {
const placas = Object.keys(data);
const select = document.getElementById('placaSelect');
if (!select) return;
select.innerHTML = '';
if (placas.length === 0) {
select.innerHTML = '<option disabled selected>No hay placas disponibles</option>';
return;
}
select.innerHTML = '<option disabled selected>-- Selecciona una placa --</option>';
placas.forEach(placa => {
const option = document.createElement('option');
option.value = placa;
option.textContent = placa;
select.appendChild(option);
});
})
.catch(() => {
mostrarToast('Error al cargar las placas disponibles.');
});
}
// Cuando se confirma la placa seleccionada:
const btnSeleccionarPlaca = document.getElementById('btnSeleccionarPlaca');
if (btnSeleccionarPlaca) {
btnSeleccionarPlaca.addEventListener('click', function () {
const placaSelect = document.getElementById('placaSelect');
const placa = placaSelect ? placaSelect.value : '';
if (!placa) return mostrarToast('Selecciona una placa.');
fetch('/data/placas.json')
.then(r => r.json())
.then(data => {
const pines = data[placa];
if (!pines) return mostrarToast('Placa no encontrada');
// ✅ Establecer un chipid único si aún no hay uno
if (!chipid) {
chipid = generarChipID();
window.history.replaceState({}, '', `?chipid=${chipid}`);
}
// Construir estructura
puertosData = {};
for (const pin in pines) {
puertosData[pin] = {
gpio: pines[pin],
disp: null,
modo: 'INPUT_PULLUP',
notas: ''
};
}
puertosData.update = new Date().toISOString();
asignaPuertos({ crear: true });
renderTablaPuertos();
if (window.closeModal) window.closeModal('modalSeleccionPlaca');
})
.catch(() => {
mostrarToast('Error al cargar datos de placas.');
});
});
}
// Función para cargar puertos desde API
function cargarPuertos() {
api.get('/api/get_puertos', { chipid: chipid, _: Date.now() }).then(data => {
if (data && typeof data === 'object' && !data.error) {
puertosData = data;
let html = `
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Puerto</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">GPIO</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Entidad</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Modo</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Notas</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase tracking-wider update-column">Actualización</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">`;
for (let puerto in puertosData) {
if (puerto === 'update') continue;
const p = puertosData[puerto];
const modoOptions = ['INPUT', 'OUTPUT', 'INPUT_PULLUP'];
let modoSelect = '<select class="modo-select w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-200">';
modoOptions.forEach(modo => {
modoSelect += `<option value="${modo}" ${p.modo === modo ? 'selected' : ''}>${modo}</option>`;
});
modoSelect += '</select>';
let entidadSelect = `<select class="entidad-select w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-200" data-puerto="${puerto}">`;
listaEntidades.forEach(entidad => {
const selected = (p.disp === entidad) ? 'selected' : '';
entidadSelect += `<option value="${entidad}" ${selected}>${entidad}</option>`;
});
entidadSelect += '</select>';
html += `
<tr class="hover:bg-gray-50">
<td class="px-3 py-2">${puerto}</td>
<td class="px-3 py-2">${p.gpio}</td>
<td class="px-3 py-2">${entidadSelect}</td>
<td class="px-3 py-2">${modoSelect}</td>
<td class="px-3 py-2 editable" contenteditable="true" data-puerto="${puerto}" data-field="notas">${p.notas || ''}</td>
<td class="px-3 py-2 update-column"><span class="inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full bg-indigo-600 text-white ring-2 ring-indigo-300/50">${p.update || 'N/A'}</span></td>
</tr>`;
}
html += '</tbody></table>';
const listaPuertos = document.getElementById('lista-puertos');
if (listaPuertos) listaPuertos.innerHTML = html;
try { if (window.applyIcons) window.applyIcons('#lista-puertos'); } catch {}
// Mostrar la fecha de la última actualización
mostrarUpdate(puertosData.update);
const btnAsignar = document.getElementById('btnAsignar');
if (btnAsignar) btnAsignar.style.display = 'none';
} else {
const listaPuertos = document.getElementById('lista-puertos');
if (listaPuertos) listaPuertos.innerHTML = '<div class="text-sm text-gray-700">No se encontraron puertos para este ChipID. Se debe seleccionar una placa.</div>';
cargarPlacas();
if (window.openModal) window.openModal('modalSeleccionPlaca');
}
}).catch(err => {
console.error('Error cargando puertos:', err);
const listaPuertos = document.getElementById('lista-puertos');
if (listaPuertos) listaPuertos.innerHTML = '<div class="text-sm text-red-700">Error al cargar puertos.</div>';
});
}
// Mostrar la fecha de la última actualización
function mostrarUpdate(updateDate) {
const updateLabel = document.getElementById('update-label');
if (!updateLabel) return;
if (updateDate) {
const date = new Date(updateDate);
if (isNaN(date.getTime())) {
updateLabel.textContent = 'Última actualización: Fecha no válida';
return;
}
const formattedDate = date.toLocaleString('es-ES', {
weekday: 'short',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
updateLabel.textContent = `Última actualización: ${formattedDate}`;
} else {
updateLabel.textContent = 'Última actualización: Nunca';
}
}
// Manejo de campos editables (delegación de eventos)
document.addEventListener('blur', function(e) {
const editable = e.target.closest('.editable');
if (!editable) return;
const puerto = editable.dataset.puerto;
const campo = editable.dataset.field;
const nuevoValor = editable.textContent.trim();
if (nuevoValor || nuevoValor === '') {
if (campo === 'notas') puertosData[puerto].notas = nuevoValor || null;
asignaPuertos();
}
}, true);
// Manejo de cambios en modo-select y entidad-select (delegación)
document.addEventListener('change', function(e) {
const modoSelect = e.target.closest('.modo-select');
if (modoSelect) {
const tr = modoSelect.closest('tr');
const puerto = tr ? tr.querySelector('td:first-child').textContent : '';
const nuevoModo = modoSelect.value;
if (puerto && puertosData[puerto]) {
puertosData[puerto].modo = nuevoModo;
asignaPuertos();
}
return;
}
const entidadSelect = e.target.closest('.entidad-select');
if (entidadSelect) {
const puerto = entidadSelect.dataset.puerto;
const nuevaEntidad = entidadSelect.value;
if (puerto && puertosData[puerto]) {
puertosData[puerto].disp = nuevaEntidad;
asignaPuertos();
}
}
});
// Actualizar puertos en el servidor
function buildPuertosPayload() {
const clean = {};
for (const p in puertosData) {
if (p === 'update') continue;
const puerto = puertosData[p] || {};
clean[p] = {
gpio: puerto.gpio,
disp: puerto.disp ?? null,
notas: puerto.notas ?? null,
modo: puerto.modo ?? 'INPUT_PULLUP'
};
}
return { [chipid]: clean };
}
function asignaPuertos(options) {
const opts = options || {};
for (const p in puertosData) {
if (p === 'update') continue;
puertosData[p].disp = puertosData[p].disp ?? null;
puertosData[p].notas = puertosData[p].notas ?? null;
puertosData[p].modo = puertosData[p].modo ?? 'INPUT_PULLUP';
}
// Actualizar el campo de fecha de la última actualización
puertosData.update = new Date().toISOString();
const payload = buildPuertosPayload();
const endpoint = opts.crear ? '/api/index.php?r=asignar_puertos' : '/api/index.php?r=update_puertos';
api.postJSON(endpoint, payload)
.then(response => {
if (!response || response.success === false) {
mostrarToast('Error: ' + ((response && response.message) || 'No se pudo guardar'), { type: 'danger' });
return;
}
mostrarToast('Puerto actualizado correctamente', { type: 'success' });
// Recargar desde servidor para reflejar persistencia y evitar cache local
cargarPuertos();
})
.catch(err => {
console.error('Error:', err);
const msg = (err.data && (err.data.message || err.data.msg)) || err.message || 'Error al conectar con el servidor';
mostrarToast(msg, { type: 'danger' });
});
}
// Abrir modal para agregar nueva entidad
const btnAgregarEntidad = document.getElementById('btnAgregarEntidad');
if (btnAgregarEntidad) {
btnAgregarEntidad.addEventListener('click', function () {
if (window.openModal) window.openModal('modalAgregarEntidad');
});
}
// Guardar nueva entidad
const btnGuardarEntidad = document.getElementById('btnGuardarEntidad');
if (btnGuardarEntidad) {
btnGuardarEntidad.addEventListener('click', function () {
const inputNuevaEntidad = document.getElementById('nuevaEntidad');
const nuevaEntidad = inputNuevaEntidad ? inputNuevaEntidad.value.trim() : '';
if (!nuevaEntidad) {
mostrarToast('Debe ingresar un nombre para la entidad.');
return;
}
// Añadir la nueva entidad a la lista
listaEntidades.push(nuevaEntidad);
api.postJSON('/api/add_entidad', { entidad: nuevaEntidad })
.then(() => {
mostrarToast('Entidad agregada correctamente.');
if (window.closeModal) window.closeModal('modalAgregarEntidad');
if (inputNuevaEntidad) inputNuevaEntidad.value = '';
cargarPuertos();
})
.catch(() => {
mostrarToast('Error al agregar la entidad.');
});
});
}
// Cargar entidades y puertos al cargar la página
cargarEntidades(cargarPuertos);
// Abrir el modal de selección de placa
const btnAsignar = document.getElementById('btnAsignar');
if (btnAsignar) {
btnAsignar.addEventListener('click', function () {
cargarPlacas();
if (window.openModal) window.openModal('modalSeleccionPlaca');
});
}
const placaSelectElem = document.getElementById('placaSelect');
if (placaSelectElem) {
placaSelectElem.addEventListener('change', function () {
const placaSeleccionada = this.value;
const infoDiv = document.getElementById('infoPlacaSeleccionada');
if (!placaSeleccionada) {
if (infoDiv) infoDiv.innerHTML = '';
return;
}
fetch('/data/placas.json')
.then(r => r.json())
.then(placas => {
const pines = placas[placaSeleccionada];
if (!pines) {
if (infoDiv) infoDiv.innerHTML = '<div class="text-red-600">Placa no encontrada.</div>';
return;
}
let html = '<h6>Pines disponibles:</h6>';
html += '<table class="min-w-full divide-y divide-gray-200 bg-white text-sm"><thead class="bg-gray-50"><tr><th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase">Pin</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-700 uppercase">GPIO</th></tr></thead><tbody class="divide-y divide-gray-100">';
for (const pin in pines) {
html += `<tr><td>${pin}</td><td>${pines[pin]}</td></tr>`;
}
html += '</tbody></table>';
if (infoDiv) infoDiv.innerHTML = html;
})
.catch(() => {
if (infoDiv) infoDiv.innerHTML = '<div class="text-red-600">Error al cargar placas.</div>';
});
});
}
</script>
<?php
$content = ob_get_clean();
// Si no se está renderizando como parcial, incluir el layout principal
if (!defined('RENDER_PARTIAL')) {
include __DIR__ . '/../layouts/layout.php';
}