491 lines
21 KiB
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';
|
|
}
|