academy/views/grupos.php

631 lines
20 KiB
PHP

<?php
require __DIR__ . '/../config/app.php';
require __DIR__ . '/../config/app.php';
if (is_logged_in()) {
header('Location: ' . BASE_URL);
exit;
}
require __DIR__ . '/../partials/header.php';
?>
<link rel="stylesheet" href="../css/views/grupos.css">
<div class="container mt-4">
<div class="row"> <!-- Grilla -->
<div class="col-12">
<div id="pantallaGrilla" class="card shadow-sm rounded-3 overflow-hidden"
data-aos="fade-up"
data-aos-duration="600"
data-aos-easing="ease-out-cubic"
data-aos-delay="60"
data-aos-once="false">
<div class="card-header bg-light text-body border-bottom">
<h5 class="text-primary fw-semibold mb-0">
<i class="bi bi-grid-3x3-gap me-2"></i> Grupos
</h5>
</div>
<div class="card-body p-0">
<div id="lstTabla"></div>
</div>
</div>
</div>
</div> <!-- Grilla -->
<div class="row"> <!-- Formulario -->
<div class="col-12">
<div id="pantallaFormulario" class="card shadow-sm rounded-3 overflow-hidden d-none"
data-aos="fade-right"
data-aos-duration="650"
data-aos-easing="ease-out-cubic"
data-aos-delay="80"
data-aos-once="false">
<div class="card-header bg-light text-body border-bottom">
<h5 class="text-primary fw-semibold mb-0">
<i class="bi bi-grid-3x3-gap me-2"></i> Nuevo Grupo
</h5>
</div>
<div class="card-body">
<form id="miFormulario" class="row needs-validation" novalidate> <!-- Formulario -->
<input type="hidden" id="iptId" name="id">
<div class="row"> <!-- ROW: Nombre Activo -->
<div class="col-md-6"> <!-- Nombre -->
<div class="form-group mb-2">
<label class="col-form-label" for="iptNombre">
<span class="small">Nombre</span><span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-sm" id="iptNombre" name="iptNombre"
placeholder="Ingrese el nombre" autocomplete="off" required>
<div class="invalid-feedback">Debe ingresar el nombre!</div>
</div>
</div> <!-- Nombre -->
<div class="col-md-6"> <!-- Activo -->
<div class="form-group mb-2">
<label class="col-form-label" for="iptActivo">
<span class="small">¿Habilitado?</span><span class="text-danger">*</span>
</label>
<select class="form-select form-select-sm" id="iptActivo" name="iptActivo" required>
<option value="">--Seleccione si está habilitado--</option>
<option value="1">Si</option>
<option value="0">No</option>
</select>
</div>
</div> <!-- Activo -->
</div> <!-- ROW: Nombre Activo -->
<div class="row"> <!-- ROW: Descripcion -->
<div class="col-md-12">
<div class="form-group mb-2">
<label class="col-form-label" for="iptDescripcion">
<span class="small">Descripción</span><span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-sm" id="iptDescripcion" name="iptDescripcion"
placeholder="Ingrese el nombre" autocomplete="off" required>
<div class="invalid-feedback">Debe ingresar la descripción!</div>
</div>
</div>
</div> <!-- ROW: Descripcion -->
<div class="row"> <!-- ROW: Integrantes -->
<div class="col-12 mt-3">
<label class="col-form-label d-block">
<span class="small">Miembros del grupo</span>
</label>
<div class="row g-3">
<div class="col-md-6">
<div id="lbDisponibles"></div>
</div>
<div class="col-md-6">
<div id="lbMiembros"></div>
</div>
</div>
<input type="hidden" id="iptMiembros" name="miembros">
</div>
</div> <!-- ROW: Integrantes -->
<div class="row"> <!-- ROW: Botones -->
<div class="mt-4">
<button type="submit" class="btn btn-success me-2" id="btnGuardar">
<i class="bi bi-save2-fill me-1"></i> Guardar
</button>
<button type="button" class="btn btn-secondary" id="btnCancelar">
<i class="bi bi-arrow-left-short me-1"></i> Cancelar
</button>
</div>
</div> <!-- ROW: Botones -->
</form> <!-- Formulario -->
</div>
</div>
</div>
</div>
</div>
<?php
require __DIR__ . '/../partials/footer.php';
?>
<script src="../js/cargar_datos.js"></script>
<script src="../js/convertir_form.js"></script>
<script>
AOS.init({
duration: 600, // velocidad “cómoda”
easing: 'ease-out-cubic',
once: false,
offset: 0 // que no espere scroll extra
});
let lbDisponibles, lbMiembros;
function initDualListBox() {
lbDisponibles = new ej.dropdowns.ListBox({
dataSource: [], // se carga luego por AJAX
fields: {
text: 'nombre',
value: 'id'
},
allowDragAndDrop: true,
scope: '#lbMiembros',
toolbarSettings: {
items: ['moveTo', 'moveFrom', 'moveAllTo', 'moveAllFrom']
},
locale: 'es-ES',
});
lbDisponibles.appendTo('#lbDisponibles');
lbMiembros = new ej.dropdowns.ListBox({
dataSource: [],
fields: {
text: 'nombre',
value: 'id'
},
allowDragAndDrop: true,
scope: '#lbDisponibles',
locale: 'es-ES',
});
lbMiembros.appendTo('#lbMiembros');
}
function cargarUsuariosDisponibles() {
return $.ajax({
url: "../ajax/usuarios.ajax.php",
dataSrc: "",
data: {
"accion": "LOV"
},
type: "POST",
dataType: "json"
}).done(function(respuesta) {
lbDisponibles.dataSource = respuesta;
lbDisponibles.refresh();
});
}
$(document).ready(function() {
initDualListBox();
cargarUsuariosDisponibles()
});
// Cargar miembros del grupo si estamos editando (ipasado por iptId)
function cargarMiembrosDelGrupo(idGrupo) {
if (!idGrupo) return;
// url: "",
return $.getJSON('../ajax/grupos.ajax.php', {
accion: 'MIEMBROS',
id: idGrupo
})
.then(function(miembros) {
// Asignar a la lista derecha
lbMiembros.dataSource = miembros;
lbMiembros.refresh();
// Remover de “Disponibles” los que ya están seleccionados
const elegidos = new Set(miembros.map(m => String(m.id)));
const restantes = lbDisponibles.dataSource.filter(u => !elegidos.has(String(u.id)));
lbDisponibles.dataSource = restantes;
lbDisponibles.refresh();
});
}
function inicializarFormularioNuevo() {
$('#miFormulario')[0].reset();
$('#miFormulario').removeClass('was-validated');
$('#iptId').val('');
$('#pantallaFormulario h5')
.html('<i class="bi bi-grid-3x3-gap me-2"></i>Nuevo Grupo')
.removeClass('text-warning')
.addClass('text-success');
$('#iptId').hide();
}
document.getElementById("btnCancelar").addEventListener("click", function() {
cambiarVista('#pantallaGrilla', '#pantallaFormulario');
});
function cargarFormularioParaEditar(rowData) {
const $form = $('#miFormulario');
$form[0].reset();
$form.removeClass('was-validated');
var datos = new FormData();
datos.append("id", rowData.id);
datos.append("accion", "DATOS");
$.ajax({
url: "../ajax/grupos.ajax.php",
type: "POST",
data: datos,
cache: false,
contentType: false,
processData: false,
dataType: 'json',
success: function(r) {
if (!r || r.respuesta !== "OK") {
console.warn("Respuesta no OK:", r);
return;
}
const grupo = Array.isArray(r.grupo) ? (r.grupo[0] || {}) : (r.grupo || {});
const personasIds = Array.isArray(r.personas) ?
r.personas.map(p => (p?.id_usuario ?? p)).filter(v => v != null) : [];
fillFormFromData($form, grupo);
actualizarMiembrosGrupo(personasIds);
// Ajusta encabezado
$('#pantallaFormulario h5')
.html('<i class="bi bi-grid-3x3-gap me-2"></i>Editar Grupo')
.removeClass('text-success')
.addClass('text-warning');
cambiarVista('#pantallaFormulario', '#pantallaGrilla');
}
});
}
function getListBoxValues(listBoxInstance) {
return Array.from(listBoxInstance.getItems())
.map(li => li.getAttribute('data-value'))
.filter(v => v !== null && v !== undefined);
}
function actualizarMiembrosGrupo(ids) {
const idsSet = new Set(ids.map(String)); // para comparar rápido
// dataSource original (donde están todos los usuarios posibles)
const todos = lbDisponibles.dataSource.concat(lbMiembros.dataSource);
// separar disponibles y miembros
const miembros = todos.filter(u => idsSet.has(String(u.id)));
const disponibles = todos.filter(u => !idsSet.has(String(u.id)));
// reasignar dataSource a cada ListBox
lbMiembros.dataSource = miembros;
lbMiembros.refresh();
lbDisponibles.dataSource = disponibles;
lbDisponibles.refresh();
}
function eliminarRegistro(rowData) {
Swal.fire({
icon: 'question',
title: '¿Está seguro?',
html: `Se eliminará el grupo <b>"${rowData.nombre}"</b>`,
showCancelButton: true,
confirmButtonText: 'Sí, borrar',
cancelButtonText: 'Cancelar',
buttonsStyling: false, // importante para usar Bootstrap
customClass: {
popup: 'my-swal-popup',
actions: 'my-swal-actions',
confirmButton: 'btn btn-sm btn-outline-danger', // igual al botón borrar
cancelButton: 'btn btn-sm btn-outline-primary', // igual al botón editar/cancelar
icon: 'my-swal-icon'
}
}).then((result) => {
if (result.isConfirmed) {
const datos = new FormData();
datos.append('accion', 'BORRAR');
datos.append('id', rowData.id);
$.ajax({
url: "../ajax/grupos.ajax.php",
method: 'POST',
data: datos,
cache: false,
contentType: false,
processData: false,
dataType: 'json',
success: function(respuesta) {
if (respuesta.respuesta === 'OK') {
Swal.fire({
toast: true,
position: 'top',
icon: 'success',
title: 'El registro fue eliminado',
showConfirmButton: false,
timer: 2500,
timerProgressBar: true
});
// Recargar grilla manteniendo estado
const estadoGrid = grid.getPersistData();
$.ajax({
url: "../ajax/grupos.ajax.php",
method: 'POST',
data: {
accion: 'LISTAR'
},
dataType: 'json',
success: function(data) {
grid.dataSource = data;
const estado = JSON.parse(estadoGrid);
if (estado.filterSettings) grid.filterSettings = estado.filterSettings;
if (estado.searchSettings) grid.searchSettings = estado.searchSettings;
}
});
} else {
Swal.fire('Error', respuesta.mensaje, 'error');
}
}
});
}
});
}
$('#lstTabla').on('click', '.btnEliminar', function() {
const row = $(this).closest('tr');
const rowData = grid.getRowObjectFromUID(row.attr('data-uid')).data;
eliminarRegistro(rowData);
});
$('#lstTabla').on('click', '.btnEditar', function() {
const row = $(this).closest('tr');
const rowData = grid.getRowObjectFromUID(row.attr('data-uid')).data;
cargarFormularioParaEditar(rowData);
});
document.getElementById("miFormulario").addEventListener("submit", function(e) {
e.preventDefault();
if (!this.checkValidity()) {
this.classList.add('was-validated');
return;
}
const datos = buildPayload(this);
// para no salirme del estandar
datos.delete('lbdisponibles');
datos.delete('lbmiembros');
const grupos = getListBoxValues(lbMiembros);
datos.append('miembros', JSON.stringify(grupos));
$.ajax({
url: "../ajax/grupos.ajax.php",
method: "POST",
data: datos,
cache: false,
contentType: false,
processData: false,
dataType: "json",
success: function(resultado) {
if (resultado.respuesta === "OK") {
const estadoGrid = grid.getPersistData(); // Guarda el estado
Swal.fire({
position: 'top', // Parte superior, centrado horizontal
icon: 'success',
title: 'El registro fue guardado',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
toast: true, // sigue siendo un toast, pero en 'top' se centra
customClass: {
popup: 'swal2-toast swal2-top-center'
}
});
cambiarVista('#pantallaGrilla', '#pantallaFormulario');
// Recargar grilla manteniendo estado
$.ajax({
url: "../ajax/grupos.ajax.php",
method: 'POST',
data: {
accion: 'LISTAR'
},
dataType: 'json',
success: function(data) {
grid.dataSource = data;
const estado = JSON.parse(estadoGrid);
if (estado.filterSettings) grid.filterSettings = estado.filterSettings;
if (estado.searchSettings) grid.searchSettings = estado.searchSettings;
}
});
} else {
Swal.fire({
icon: 'error',
title: 'Error al guardar',
text: resultado.mensaje,
confirmButtonText: 'Aceptar',
customClass: {
confirmButton: 'btn btn-danger',
popup: 'rounded'
},
buttonsStyling: false
});
}
}
});
});
function cambiarVista(vistaMostrar, vistaOcultar) {
const $show = $(vistaMostrar).hasClass('card') ? $(vistaMostrar) : $(vistaMostrar).closest('.card');
const $hide = $(vistaOcultar).hasClass('card') ? $(vistaOcultar) : $(vistaOcultar).closest('.card');
// Elegir clase de salida según el efecto de AOS del que se va
const hideEffect = ($hide.attr('data-aos') || '').toLowerCase();
const exitClass =
hideEffect.includes('fade-right') ? 'aos-exit-right' : 'aos-exit-up'; // por defecto "up"
const exitMs = parseInt($hide.attr('data-aos-duration'), 10) || 600;
// 1) Animación de salida
$hide.addClass(exitClass).one('animationend', function() {
// 2) Tras salir, oculto y limpio
$hide.removeClass(exitClass + ' aos-animate').addClass('d-none');
// 3) Preparar y animar la entrada con AOS
$show.removeClass('d-none aos-animate');
(AOS.refreshHard || AOS.refresh).call(AOS);
requestAnimationFrame(() => $show.addClass('aos-animate'));
});
// Fallback por si el evento no dispara (raro, pero seguro)
setTimeout(() => {
if (!$hide.hasClass('d-none')) {
$hide.removeClass(exitClass + ' aos-animate').addClass('d-none');
$show.removeClass('d-none aos-animate');
(AOS.refreshHard || AOS.refresh).call(AOS);
requestAnimationFrame(() => $show.addClass('aos-animate'));
}
}, exitMs + 50);
}
let grid;
$.ajax({ // Solicitud ajax para cargar los usuarios en la grilla
url: '../ajax/grupos.ajax.php',
method: 'POST',
data: {
accion: 'LISTAR'
},
dataType: 'json',
success: function(data) {
ej.grids.Grid.Inject(ej.grids.Sort, ej.grids.Search, ej.grids.ColumnChooser, ej.grids.Filter, ej.grids.ExcelExport);
grid = new ej.grids.Grid({
id: 'lstTabla',
dataSource: data,
locale: 'es-ES',
allowPaging: true,
allowSorting: true,
allowFiltering: true,
allowExcelExport: true,
showColumnChooser: true,
filterSettings: {
type: 'Excel',
operators: {
stringOperator: [{
value: 'contains',
text: 'Contiene'
},
{
value: 'equal',
text: 'Igual'
},
{
value: 'startswith',
text: 'Empieza con'
},
{
value: 'endswith',
text: 'Termina con'
}
]
}
},
toolbar: [
'Search',
{
text: '<i class="bi bi-layout-three-columns"></i> Columnas',
tooltipText: 'Mostrar/ocultar columnas',
id: 'btnColumnas'
},
{
text: '<i class="bi bi-person-fill-add"></i> Nuevo',
tooltipText: 'Crear nuevo grupo',
id: 'btnNuevo'
},
{
text: '<i class="bi bi-file-earmark-excel-fill"></i> Excel',
tooltipText: 'Exportar a Excel',
id: 'btnExcel'
}
],
toolbarClick: function(args) {
if (args.item.id === 'btnExcel') {
const hoy = new Date().toISOString().slice(0, 10);
grid.excelExport({
fileName: `grupos_${hoy}.xlsx`
});
} else if (args.item.id === 'btnNuevo') {
inicializarFormularioNuevo();
cambiarVista('#pantallaFormulario', '#pantallaGrilla');
} else if (args.item.id === 'btnColumnas') {
grid.columnChooserModule.openColumnChooser(0, 0); // puedes ajustar posición x, y
}
},
disableHtmlEncode: true,
pageSettings: {
pageSize: 10
},
locale: 'es-ES',
columns: [{
headerText: '',
width: 100,
template: function() {
return `
<div class="text-center d-flex justify-content-center gap-2">
<button class="btn btn-sm btn-outline-primary btn-icon-sm btnEditar" title="Editar">
<i class="bi bi-pencil-fill"></i>
</button>
<button class="btn btn-sm btn-outline-danger btn-icon-sm btnEliminar" title="Eliminar">
<i class="bi bi-trash-fill"></i>
</button>
</div>
`;
},
allowSorting: false
},
{
field: 'id',
headerText: '#',
width: 50
},
{
field: 'nombre',
headerText: 'Nombre',
width: 100,
clipMode: 'EllipsisWithTooltip'
},
{
field: 'activo',
headerText: '¿Activo?',
visible: false,
width: 50
},
{
field: 'integrantes',
headerText: 'Integrantes',
width: 160,
clipMode: 'EllipsisWithTooltip'
}
],
});
grid.locale = 'es-ES';
grid.appendTo('#lstTabla');
$('#lstTabla').on('click', '.btnEditar', function() {
const row = $(this).closest('tr');
const rowData = grid.getRowObjectFromUID(row.attr('data-uid')).data;
// cargarFormularioParaEditar(rowData);
});
$('#lstTabla').on('click', '.btnEliminar', function() {
const row = $(this).closest('tr');
const rowData = grid.getRowObjectFromUID(row.attr('data-uid')).data;
});
},
error: function(xhr, status, error) {
console.error('Error cargando datos:', error);
}
});
</script>