Corrección gestión de notificaciones

This commit is contained in:
2025-05-29 09:01:50 -06:00
parent 79c75ef011
commit e08a813802
3 changed files with 577 additions and 214 deletions

View File

@@ -64,7 +64,7 @@ if (!isset($_SESSION['usuario_id']) || $_SESSION['tipo_usuario'] !== 'importador
// Detecta si estamos en alguna parta de seguridad
$esVistaSeguridad = str_contains($_SERVER['REQUEST_URI'], '/seguridad');
// Detecta si estamos en alguna parte de preferencias
$esVistaPreferencias = str_contains($_SERVER['REQUEST_URI'], '/preferencias')
$esVistaPreferencias = str_contains($_SERVER['REQUEST_URI'], '/preferencias');
?>
<!-- INFORMACIÓN GENERAL -->

View File

@@ -9,6 +9,7 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
font-family: 'Segoe UI', sans-serif;
@@ -54,7 +55,6 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
margin-left: 250px; /* Ancho del sidebar */
}
}
/* En móviles, sin margen lateral */
@media (max-width: 767.98px) {
.content {
@@ -74,81 +74,86 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
.card {
border-radius: 12px;
}
.section-divider {
border-left: 4px solid #007bff;
padding-left: 15px;
margin: 20px 0;
}
.fade-transition {
transition: opacity 0.3s ease-in-out;
transition: all 0.3s ease-in-out;
}
.correo-extra-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
.section-divider {
border-bottom: 1px solid #e9ecef;
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
}
.success-message {
background-color: #d4edda;
color: #155724;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid #c3e6cb;
.section-divider:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid #f5c6cb;
}
/* Estilos para indicadores de guardado */
.save-indicator {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 15px;
border-radius: 8px;
font-weight: 500;
z-index: 9999;
transition: all 0.3s ease;
background: #28a745;
color: white;
padding: 10px 20px;
border-radius: 25px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1050;
display: flex;
align-items: center;
font-size: 14px;
transition: all 0.3s ease;
}
.save-indicator.saving {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
.success-message {
background: #d4edda;
color: #155724;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #c3e6cb;
margin-bottom: 20px;
}
.save-indicator.saved {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.save-indicator.error {
background-color: #f8d7da;
.error-message {
background: #f8d7da;
color: #721c24;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #f5c6cb;
margin-bottom: 20px;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
.resumen-config {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-top: 15px;
}
/* Estilo para switches deshabilitados temporalmente */
.form-check-input:disabled {
opacity: 0.6;
.time-input {
max-width: 120px;
}
/* Animación sutil para switches */
.form-check-input {
transition: all 0.2s ease;
.days-selector {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.day-option {
flex: 1;
min-width: 45px;
text-align: center;
}
.day-preset {
margin-bottom: 15px;
}
.day-preset .btn {
margin-right: 10px;
margin-bottom: 5px;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-outline-primary.active {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
}
</style>
</head>
@@ -187,62 +192,227 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
</div>
<!-- Tipos de notificaciones -->
<div id="tipos-notificaciones" class="fade-transition"
<?= !$notificaciones['notificaciones'] ? 'style="display:none; opacity:0;"' : 'style="opacity:1;"' ?>>
<div id="tipos-notificaciones" class="fade-transition" style="opacity:1;">
<div class="section-divider">
<h6 class="mb-3">📋 Tipos de Notificaciones</h6>
<div class="row">
<?php foreach ($tipos as $clave => $descripcion): ?>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="nuevas_solicitudes" id="nuevas_solicitudes"
data-tipo="tipo_notificacion" data-nombre="nuevas_solicitudes"
<?= !empty($preferencias['nuevas_solicitudes']) ? 'checked' : '' ?>>
<label class="form-check-label" for="nuevas_solicitudes">
<strong>Nuevas Solicitudes</strong>
<div class="form-text">Creación de nuevas solicitudes de importación</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="registro_solicitud" id="registro_solicitud"
data-tipo="tipo_notificacion" data-nombre="registro_solicitud"
<?= !empty($preferencias['registro_solicitud']) ? 'checked' : '' ?>>
<label class="form-check-label" for="registro_solicitud">
<strong>Registro Solicitud</strong>
<div class="form-text">Notificación inmediata al registrar una solicitud</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="cambio_estado" id="cambio_estado"
data-tipo="tipo_notificacion" data-nombre="cambio_estado"
<?= !empty($preferencias['cambio_estado']) ? 'checked' : '' ?>>
<label class="form-check-label" for="cambio_estado">
<strong>Cambio Estado</strong>
<div class="form-text">Cambio de estado de solicitudes de importación</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="resumen_diario" id="resumen_diario"
data-tipo="tipo_notificacion" data-nombre="resumen_diario"
<?= !empty($preferencias['resumen_diario']) ? 'checked' : '' ?>>
<label class="form-check-label" for="resumen_diario">
<strong>Resumen Diario</strong>
<div class="form-text">Resumen diario de solicitudes de importación</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="alertas_tiempo" id="alertas_tiempo"
data-tipo="tipo_notificacion" data-nombre="alertas_tiempo"
<?= !empty($preferencias['alertas_tiempo']) ? 'checked' : '' ?>>
<label class="form-check-label" for="alertas_tiempo">
<strong>Alertas Tiempo</strong>
<div class="form-text">Alertas por tiempo excedido en estados críticos</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="documentos_expediente" id="documentos_expediente"
data-tipo="tipo_notificacion" data-nombre="documentos_expediente"
<?= !empty($preferencias['documentos_expediente']) ? 'checked' : '' ?>>
<label class="form-check-label" for="documentos_expediente">
<strong>Documentos Expediente</strong>
<div class="form-text">Incorporación de documentos al expediente electrónico</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="nuevos_archivos" id="nuevos_archivos"
data-tipo="tipo_notificacion" data-nombre="nuevos_archivos"
<?= !empty($preferencias['nuevos_archivos']) ? 'checked' : '' ?>>
<label class="form-check-label" for="nuevos_archivos">
<strong>Nuevos Archivos</strong>
<div class="form-text">Aviso al agregarse nuevos archivos o documentos</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="intentos_fallidos" id="intentos_fallidos"
data-tipo="tipo_notificacion" data-nombre="intentos_fallidos"
<?= !empty($preferencias['intentos_fallidos']) ? 'checked' : '' ?>>
<label class="form-check-label" for="intentos_fallidos">
<strong>Intentos Fallidos</strong>
<div class="form-text">Intentos fallidos de acceso</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="bloqueo_cuenta" id="bloqueo_cuenta"
data-tipo="tipo_notificacion" data-nombre="bloqueo_cuenta"
<?= !empty($preferencias['bloqueo_cuenta']) ? 'checked' : '' ?>>
<label class="form-check-label" for="bloqueo_cuenta">
<strong>Bloqueo Cuenta</strong>
<div class="form-text">Bloqueo de cuenta</div>
</label>
</div>
</div>
</div>
</div>
<!-- Configuración específica del resumen diario -->
<div id="resumen-diario-config" class="fade-transition" style="display: none; opacity: 0;">
<div class="resumen-config">
<h6 class="mb-3">
<i class="fas fa-clock me-2"></i>
⏰ Configuración del Resumen Diario
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input tipo-notificacion" type="checkbox"
name="<?= $clave ?>" id="<?= $clave ?>"
data-tipo="tipo_notificacion" data-nombre="<?= $clave ?>"
<?= ($preferencias[$clave] ?? 0) ? 'checked' : '' ?>>
<label class="form-check-label" for="<?= $clave ?>">
<strong><?= ucwords(str_replace('_', ' ', $clave)) ?></strong>
<div class="form-text"><?= htmlspecialchars($descripcion) ?></div>
</label>
</div>
<label for="resumen_hora" class="form-label fw-bold">Hora de envío</label>
<input type="time" class="form-control time-input" id="resumen_hora"
name="resumen_hora" value="08:00">
<div class="form-text">Hora en que se enviará el resumen diario</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Días de envío</label>
<!-- Configuración específica para resumen diario -->
<?php if ($clave === 'resumen_diario'): ?>
<div id="resumen-diario-config" class="mt-3 ps-4 fade-transition"
style="<?= ($preferencias['resumen_diario'] ?? 0) ? 'display:block; opacity:1;' : 'display:none; opacity:0;' ?>">
<div class="card border-0 bg-light p-3">
<h6 class="mb-3 text-primary">⚙️ Configuración de Horario</h6>
<div class="row">
<div class="col-md-6 mb-2">
<label for="resumen_hora" class="form-label small fw-bold">🕐 Hora de envío</label>
<input type="time" class="form-control form-control-sm"
id="resumen_hora" name="resumen_hora"
value="<?= date('H:i', strtotime($preferencias['resumen_diario_hora'] ?? '08:00:00')) ?>">
</div>
<div class="col-md-6 mb-2">
<label for="resumen_dias" class="form-label small fw-bold">📅 Días de envío</label>
<select class="form-select form-select-sm" id="resumen_dias" name="resumen_dias">
<option value="L-V" <?= ($preferencias['resumen_diario_dias'] ?? 'L-V') === 'L-V' ? 'selected' : '' ?>>
Lunes a Viernes
</option>
<option value="L-S" <?= ($preferencias['resumen_diario_dias'] ?? 'L-V') === 'L-S' ? 'selected' : '' ?>>
Lunes a Sábado
</option>
<option value="TODOS" <?= ($preferencias['resumen_diario_dias'] ?? 'L-V') === 'TODOS' ? 'selected' : '' ?>>
Todos los días
</option>
</select>
</div>
</div>
<div class="form-text mt-2">
<i class="fas fa-info-circle text-info"></i>
Recibirás un resumen con las actividades del día anterior
</div>
<!-- Botones de preselección -->
<div class="day-preset">
<button type="button" class="btn btn-outline-primary btn-sm preset-btn active" data-preset="L-V">
Lunes a Viernes
</button>
<button type="button" class="btn btn-outline-primary btn-sm preset-btn" data-preset="TODOS">
Todos los días
</button>
<button type="button" class="btn btn-outline-primary btn-sm preset-btn" data-preset="PERSONALIZADO">
Personalizado
</button>
</div>
<!-- Selector individual de días -->
<div class="days-selector" id="days-selector" style="display: none;">
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="L" id="day-L" name="dias[]">
<label class="form-check-label" for="day-L">L</label>
</div>
</div>
<?php endif; ?>
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="M" id="day-M" name="dias[]">
<label class="form-check-label" for="day-M">M</label>
</div>
</div>
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="X" id="day-X" name="dias[]">
<label class="form-check-label" for="day-X">X</label>
</div>
</div>
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="J" id="day-J" name="dias[]">
<label class="form-check-label" for="day-J">J</label>
</div>
</div>
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="V" id="day-V" name="dias[]">
<label class="form-check-label" for="day-V">V</label>
</div>
</div>
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="S" id="day-S" name="dias[]">
<label class="form-check-label" for="day-S">S</label>
</div>
</div>
<div class="day-option">
<div class="form-check">
<input class="form-check-input day-checkbox" type="checkbox"
value="D" id="day-D" name="dias[]">
<label class="form-check-label" for="day-D">D</label>
</div>
</div>
</div>
<input type="hidden" id="resumen_dias" name="resumen_dias" value="L-V">
<div class="form-text">Días en que se enviará el resumen diario</div>
</div>
<?php endforeach; ?>
</div>
<div class="alert alert-info d-flex align-items-center mt-3">
<i class="fas fa-info-circle me-2"></i>
<small>
El resumen diario se enviará automáticamente a la hora configurada,
únicamente en los días seleccionados y solo si hay actividad de solicitudes.
</small>
</div>
</div>
</div>
</div>
@@ -400,6 +570,18 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
}
}
// Función para debounce (evitar múltiples guardados rápidos)
function debounce(func, wait) {
return function executedFunction(...args) {
const later = () => {
clearTimeout(saveTimeout);
func(...args);
};
clearTimeout(saveTimeout);
saveTimeout = setTimeout(later, wait);
};
}
// Event listener para switch general de notificaciones
document.getElementById('notificaciones').addEventListener('change', function() {
const isChecked = this.checked;
@@ -429,6 +611,15 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
document.querySelectorAll('.tipo-notificacion').forEach(checkbox => {
checkbox.checked = false;
});
// Ocultar configuración de resumen diario
const resumenDiarioConfig = document.getElementById('resumen-diario-config');
if (resumenDiarioConfig) {
resumenDiarioConfig.style.opacity = '0';
setTimeout(() => {
resumenDiarioConfig.style.display = 'none';
}, 300);
}
}
});
@@ -483,10 +674,147 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
}).finally(() => {
this.disabled = false;
});
// Si es el resumen diario, mostrar/ocultar configuración
if (tipoNombre === 'resumen_diario') {
toggleSection(this, 'resumen-diario-config');
}
});
});
// Prevenir envío accidental del formulario (aunque ya no lo necesitamos)
// === FUNCIONALIDAD DE RESUMEN DIARIO ===
// Referencias específicas para resumen diario
const resumenDiario = document.getElementById('resumen_diario');
const resumenDiarioConfig = document.getElementById('resumen-diario-config');
const presetButtons = document.querySelectorAll('.preset-btn');
const daysSelector = document.getElementById('days-selector');
const dayCheckboxes = document.querySelectorAll('.day-checkbox');
const resumenDiasInput = document.getElementById('resumen_dias');
const resumenHoraInput = document.getElementById('resumen_hora');
// Función para actualizar el valor de días seleccionados
function updateResumenDias() {
const selectedDays = Array.from(dayCheckboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
if (selectedDays.length === 7) {
resumenDiasInput.value = 'TODOS';
} else if (selectedDays.length === 5 &&
['L', 'M', 'X', 'J', 'V'].every(day => selectedDays.includes(day))) {
resumenDiasInput.value = 'L-V';
} else {
resumenDiasInput.value = selectedDays.join(',');
}
}
// Función para cargar preset de días
function loadDaysPreset(preset) {
// Desmarcar todos los checkboxes primero
dayCheckboxes.forEach(cb => cb.checked = false);
// Marcar según el preset
if (preset === 'L-V') {
['L', 'M', 'X', 'J', 'V'].forEach(day => {
const checkbox = document.getElementById(`day-${day}`);
if (checkbox) checkbox.checked = true;
});
} else if (preset === 'TODOS') {
dayCheckboxes.forEach(cb => cb.checked = true);
} else if (preset.includes(',')) {
// Preset personalizado
const days = preset.split(',');
days.forEach(day => {
const checkbox = document.getElementById(`day-${day.trim()}`);
if (checkbox) checkbox.checked = true;
});
}
updateResumenDias();
}
// Función para actualizar botones de preset según selección actual
function updatePresetButtons() {
const currentValue = resumenDiasInput.value;
presetButtons.forEach(btn => btn.classList.remove('active'));
const activeButton = document.querySelector(`[data-preset="${currentValue}"]`);
if (activeButton) {
activeButton.classList.add('active');
daysSelector.style.display = 'none';
} else {
// Es personalizado
const customButton = document.querySelector('[data-preset="PERSONALIZADO"]');
if (customButton) {
customButton.classList.add('active');
daysSelector.style.display = 'flex';
}
}
}
// Función debounced para guardar configuración de resumen
const saveResumenConfig = debounce(async function() {
const hora = resumenHoraInput.value;
const dias = resumenDiasInput.value;
await autoSave('resumen_config', {
hora: hora,
dias: dias
});
}, 1000);
// Event listeners para botones de preset de días
if (presetButtons.length > 0) {
presetButtons.forEach(button => {
button.addEventListener('click', function() {
// Remover clase active de todos los botones
presetButtons.forEach(btn => btn.classList.remove('active'));
// Agregar clase active al botón clickeado
this.classList.add('active');
const preset = this.dataset.preset;
if (preset === 'PERSONALIZADO') {
daysSelector.style.display = 'flex';
} else {
daysSelector.style.display = 'none';
resumenDiasInput.value = preset;
loadDaysPreset(preset);
saveResumenConfig();
}
});
});
}
// Event listeners para checkboxes individuales de días
if (dayCheckboxes.length > 0) {
dayCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateResumenDias();
updatePresetButtons();
saveResumenConfig();
});
});
}
// Event listener para cambio de hora
if (resumenHoraInput) {
resumenHoraInput.addEventListener('change', function() {
saveResumenConfig();
});
}
// Inicialización del estado de los días al cargar la página
document.addEventListener('DOMContentLoaded', function() {
if (resumenDiasInput && resumenDiasInput.value) {
loadDaysPreset(resumenDiasInput.value);
updatePresetButtons();
}
});
// === FIN FUNCIONALIDAD DE RESUMEN DIARIO ===
// Prevenir envío accidental del formulario
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.type !== 'submit') {
e.preventDefault();
@@ -501,80 +829,6 @@ include __DIR__ . '/../partials/sidebar_configuracion.php';
return e.returnValue;
}
});
// Event listener específico para el switch de resumen diario
document.getElementById('resumen_diario').addEventListener('change', function() {
const isChecked = this.checked;
const configSection = document.getElementById('resumen-diario-config');
// Mostrar/ocultar configuración de horario con animación
if (isChecked) {
configSection.style.display = 'block';
setTimeout(() => {
configSection.style.opacity = '1';
}, 10);
} else {
configSection.style.opacity = '0';
setTimeout(() => {
configSection.style.display = 'none';
}, 300);
}
});
// Event listeners para los controles de horario
document.getElementById('resumen_hora').addEventListener('change', function() {
const hora = this.value;
const dias = document.getElementById('resumen_dias').value;
// Validar que resumen_diario esté activado
const resumenDiarioCheckbox = document.getElementById('resumen_diario');
if (!resumenDiarioCheckbox.checked) {
showSaveIndicator('error', 'El resumen diario debe estar activado');
return;
}
// Deshabilitar controles temporalmente
const horaInput = this;
const diasSelect = document.getElementById('resumen_dias');
horaInput.disabled = true;
diasSelect.disabled = true;
// Guardar configuración
autoSave('resumen_diario_config', {
hora: hora,
dias: dias
}).finally(() => {
horaInput.disabled = false;
diasSelect.disabled = false;
});
});
document.getElementById('resumen_dias').addEventListener('change', function() {
const hora = document.getElementById('resumen_hora').value;
const dias = this.value;
// Validar que resumen_diario esté activado
const resumenDiarioCheckbox = document.getElementById('resumen_diario');
if (!resumenDiarioCheckbox.checked) {
showSaveIndicator('error', 'El resumen diario debe estar activado');
return;
}
// Deshabilitar controles temporalmente
const horaInput = document.getElementById('resumen_hora');
const diasSelect = this;
horaInput.disabled = true;
diasSelect.disabled = true;
// Guardar configuración
autoSave('resumen_diario_config', {
hora: hora,
dias: dias
}).finally(() => {
horaInput.disabled = false;
diasSelect.disabled = false;
});
});
</script>
</body>