clean($_POST['numero_oficio'] ?? ''), 'tipo' => clean($_POST['tipo'] ?? 'recibido'), 'remitente' => clean($_POST['remitente'] ?? ''), 'destinatario' => clean($_POST['destinatario'] ?? ''), 'asunto' => clean($_POST['asunto'] ?? ''), 'descripcion' => clean($_POST['descripcion'] ?? ''), 'fecha_recepcion' => clean($_POST['fecha_recepcion'] ?? ''), 'fecha_vencimiento' => clean($_POST['fecha_vencimiento'] ?? ''), 'prioridad' => clean($_POST['prioridad'] ?? 'media'), 'estado' => clean($_POST['estado'] ?? 'recibido'), 'responsable_id' => (int)($_POST['responsable_id'] ?? 0), 'es_confidencial' => isset($_POST['es_confidencial']) ? 1 : 0, 'etiquetas' => $_POST['etiquetas'] ?? [], ]; $errores = validarOficio($datos, $model); if (!empty($errores)) { $_SESSION['errores_form'] = $errores; $_SESSION['datos_form'] = $datos; redirect(APP_URL.'/views/oficios/crear.php'); } $id = $model->crear($datos, $userId); // Subir adjuntos subirAdjuntos($id, $userId); // Crear notificación al responsable if ($datos['responsable_id']) { crearNotificacion($datos['responsable_id'], $id, 'sistema', 'Nuevo oficio asignado', "Se te ha asignado el oficio {$datos['numero_oficio']}: {$datos['asunto']}"); } logActividad($userId, 'crear_oficio', 'oficios', "Oficio #{$datos['numero_oficio']} creado."); redirect(APP_URL.'/views/oficios/detalle.php?id='.$id.'&success='.urlencode('Oficio creado exitosamente.')); // ── ACTUALIZAR ───────────────────────────────────────────────────────── case 'actualizar': if ($_SERVER['REQUEST_METHOD'] !== 'POST') redirect(APP_URL.'/views/oficios/lista.php'); verificarCsrf(); $id = (int)($_POST['id'] ?? 0); if (!$id) redirect(APP_URL.'/views/oficios/lista.php'); $datos = [ 'numero_oficio' => clean($_POST['numero_oficio'] ?? ''), 'tipo' => clean($_POST['tipo'] ?? 'recibido'), 'remitente' => clean($_POST['remitente'] ?? ''), 'destinatario' => clean($_POST['destinatario'] ?? ''), 'asunto' => clean($_POST['asunto'] ?? ''), 'descripcion' => clean($_POST['descripcion'] ?? ''), 'fecha_recepcion' => clean($_POST['fecha_recepcion'] ?? ''), 'fecha_vencimiento' => clean($_POST['fecha_vencimiento'] ?? ''), 'prioridad' => clean($_POST['prioridad'] ?? 'media'), 'estado' => clean($_POST['estado'] ?? 'recibido'), 'responsable_id' => (int)($_POST['responsable_id'] ?? 0), 'es_confidencial' => isset($_POST['es_confidencial']) ? 1 : 0, 'etiquetas' => $_POST['etiquetas'] ?? [], ]; $errores = validarOficio($datos, $model, $id); if (!empty($errores)) { $_SESSION['errores_form'] = $errores; $_SESSION['datos_form'] = $datos; redirect(APP_URL.'/views/oficios/editar.php?id='.$id); } $model->actualizar($id, $datos, $userId); subirAdjuntos($id, $userId); logActividad($userId, 'editar_oficio', 'oficios', "Oficio #$id actualizado."); redirect(APP_URL.'/views/oficios/detalle.php?id='.$id.'&success='.urlencode('Oficio actualizado exitosamente.')); // ── ELIMINAR (lógico) ────────────────────────────────────────────────── case 'eliminar': verificarCsrf(); $id = (int)($_GET['id'] ?? $_POST['id'] ?? 0); if (!$id) redirect(APP_URL.'/views/oficios/lista.php'); $model->eliminarLogico($id, $userId); logActividad($userId, 'eliminar_oficio', 'oficios', "Oficio #$id movido a papelera."); redirect(APP_URL.'/views/oficios/lista.php?success='.urlencode('Oficio movido a la papelera.')); // ── ELIMINAR FÍSICO (solo admin) ─────────────────────────────────────── case 'eliminar_fisico': AuthController::requerirAdmin(); verificarCsrf(); $id = (int)($_GET['id'] ?? $_POST['id'] ?? 0); if (!$id) redirect(APP_URL.'/views/oficios/papelera.php'); $model->eliminarFisico($id); logActividad($userId, 'eliminar_fisico_oficio', 'oficios', "Oficio #$id eliminado permanentemente."); redirect(APP_URL.'/views/oficios/papelera.php?success='.urlencode('Oficio eliminado permanentemente.')); // ── RESTAURAR ───────────────────────────────────────────────────────── case 'restaurar': AuthController::requerirAdmin(); verificarCsrf(); $id = (int)($_GET['id'] ?? $_POST['id'] ?? 0); if (!$id) redirect(APP_URL.'/views/oficios/papelera.php'); $model->restaurar($id, $userId); logActividad($userId, 'restaurar_oficio', 'oficios', "Oficio #$id restaurado."); redirect(APP_URL.'/views/oficios/papelera.php?success='.urlencode('Oficio restaurado exitosamente.')); // ── DERIVAR ─────────────────────────────────────────────────────────── case 'derivar': if ($_SERVER['REQUEST_METHOD'] !== 'POST') redirect(APP_URL.'/views/oficios/lista.php'); verificarCsrf(); $id = (int)($_POST['id'] ?? 0); $nuevoResponsable= (int)($_POST['nuevo_responsable'] ?? 0); $comentario = clean($_POST['comentario_derivacion'] ?? ''); if (!$id || !$nuevoResponsable) redirect(APP_URL.'/views/oficios/detalle.php?id='.$id.'&error='.urlencode('Datos incompletos para la derivación.')); $model->derivar($id, $nuevoResponsable, $userId, $comentario); crearNotificacion($nuevoResponsable, $id, 'derivacion', 'Oficio derivado hacia ti', "Se te ha derivado el oficio: $comentario"); logActividad($userId, 'derivar_oficio', 'oficios', "Oficio #$id derivado a usuario #$nuevoResponsable."); redirect(APP_URL.'/views/oficios/detalle.php?id='.$id.'&success='.urlencode('Oficio derivado exitosamente.')); // ── COMENTARIO ──────────────────────────────────────────────────────── case 'comentar': if ($_SERVER['REQUEST_METHOD'] !== 'POST') redirect(APP_URL.'/views/oficios/lista.php'); verificarCsrf(); $id = (int)($_POST['oficio_id'] ?? 0); $texto = clean($_POST['comentario'] ?? ''); $privado = isset($_POST['es_privado']); if (!$id || strlen($texto) < 2) redirect(APP_URL.'/views/oficios/detalle.php?id='.$id.'&error='.urlencode('El comentario no puede estar vacío.')); $model->agregarComentario($id, $userId, $texto, $privado); redirect(APP_URL.'/views/oficios/detalle.php?id='.$id.'&success='.urlencode('Comentario agregado.')); // ── GENERAR NÚMERO ──────────────────────────────────────────────────── case 'gen_numero': $tipo = clean($_GET['tipo'] ?? 'recibido'); jsonResponse(['numero' => $model->generarNumero($tipo)]); // ── MARCAR NOTIFICACIÓN LEÍDA ───────────────────────────────────────── case 'marcar_notif': $nId = (int)($_GET['id'] ?? 0); if ($nId) { $db = getDB(); $db->prepare("UPDATE notificaciones SET leida=1,leida_at=NOW() WHERE id=? AND usuario_id=?") ->execute([$nId, $userId]); } redirect(APP_URL.'/views/oficios/detalle.php?id='.($_GET['oficio'] ?? 0)); // ── PDF ─────────────────────────────────────────────────────────────── case 'pdf': $id = (int)($_GET['id'] ?? 0); $datos = $model->buscarPorId($id); if (!$dados = $datos) redirect(APP_URL.'/views/oficios/lista.php'); require_once __DIR__ . '/../controllers/ReporteController.php'; ReporteController::generarPdfOficio($datos); break; default: redirect(APP_URL.'/views/oficios/lista.php'); } // ── Helpers ────────────────────────────────────────────────────────────────── function validarOficio(array $datos, OficioModel $model, ?int $excepto = null): array { $errores = []; if (empty($datos['numero_oficio'])) $errores[] = 'El número de oficio es requerido.'; elseif ($model->existeNumero($datos['numero_oficio'], $excepto)) $errores[] = 'El número de oficio ya existe en el sistema.'; if (empty($datos['remitente'])) $errores[] = 'El remitente es requerido.'; if (empty($datos['destinatario'])) $errores[] = 'El destinatario es requerido.'; if (empty($datos['asunto'])) $errores[] = 'El asunto es requerido.'; if (empty($datos['fecha_recepcion'])) $errores[] = 'La fecha de recepción es requerida.'; if (!in_array($datos['tipo'], ['recibido','enviado'])) $errores[] = 'Tipo inválido.'; if (!in_array($datos['prioridad'], ['alta','media','baja'])) $errores[] = 'Prioridad inválida.'; if (!in_array($datos['estado'], ['recibido','en_proceso','respondido','vencido','archivado'])) $errores[] = 'Estado inválido.'; return $errores; } function subirAdjuntos(int $oficioId, int $userId): void { if (empty($_FILES['adjuntos']['name'][0])) return; $db = getDB(); $count = count($_FILES['adjuntos']['name']); for ($i = 0; $i < $count; $i++) { if ($_FILES['adjuntos']['error'][$i] !== UPLOAD_ERR_OK) continue; $originalName = $_FILES['adjuntos']['name'][$i]; $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); if (!in_array($ext, UPLOAD_ALLOWED_TYPES)) continue; if ($_FILES['adjuntos']['size'][$i] > UPLOAD_MAX_SIZE) continue; $safeName = date('Ymd_His') . '_' . bin2hex(random_bytes(6)) . '.' . $ext; $destDir = UPLOAD_PATH . 'oficios/' . $oficioId . '/'; if (!is_dir($destDir)) mkdir($destDir, 0755, true); if (move_uploaded_file($_FILES['adjuntos']['tmp_name'][$i], $destDir . $safeName)) { $db->prepare( "INSERT INTO documentos_adjuntos (oficio_id,nombre_original,nombre_archivo,ruta,tipo_mime,tamanio,subido_por) VALUES (?,?,?,?,?,?,?)" )->execute([$oficioId, $originalName, $safeName, 'oficios/'.$oficioId.'/'.$safeName, $_FILES['adjuntos']['type'][$i], $_FILES['adjuntos']['size'][$i], $userId]); } } } function crearNotificacion(int $usuarioId, ?int $oficioId, string $tipo, string $titulo, string $mensaje): void { $db = getDB(); $db->prepare( "INSERT INTO notificaciones (usuario_id,oficio_id,tipo,titulo,mensaje) VALUES (?,?,?,?,?)" )->execute([$usuarioId, $oficioId, $tipo, $titulo, $mensaje]); } function logActividad(int $userId, string $accion, string $modulo, string $desc = ''): void { $db = getDB(); $db->prepare( "INSERT INTO log_actividad (usuario_id,accion,modulo,descripcion,ip_address,user_agent) VALUES (?,?,?,?,?,?)" )->execute([$userId, $accion, $modulo, $desc, $_SERVER['REMOTE_ADDR']??null, $_SERVER['HTTP_USER_AGENT']??null]); }