From e2827786469e076e1b7f4f89a5a9a50ffe9072a6 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 24 Jun 2026 21:44:23 +0000 Subject: [PATCH] Autosave: 20260624-214502 --- .../080_create_hr_reclutamiento_table.sql | 26 + .../081_create_hr_colaboradores_table.sql | 24 + .../082_create_hr_manuales_table.sql | 29 + .../083_create_hr_evaluaciones_table.sql | 18 + ...d_hr_reclutamiento_indicadores_columns.sql | 10 + ...r_reclutamiento_indicadores_to_varchar.sql | 9 + download_ruta_contraentrega.php | 36 +- hr_colaboradores.php | 356 +++++++++++ hr_evaluaciones.php | 331 ++++++++++ hr_manuales.php | 461 +++++++++++++ hr_reclutamiento.php | 604 ++++++++++++++++++ includes/hr_bootstrap.php | 145 +++++ layout_header.php | 32 + 13 files changed, 2078 insertions(+), 3 deletions(-) create mode 100644 db/migrations/080_create_hr_reclutamiento_table.sql create mode 100644 db/migrations/081_create_hr_colaboradores_table.sql create mode 100644 db/migrations/082_create_hr_manuales_table.sql create mode 100644 db/migrations/083_create_hr_evaluaciones_table.sql create mode 100644 db/migrations/084_add_hr_reclutamiento_indicadores_columns.sql create mode 100644 db/migrations/085_convert_hr_reclutamiento_indicadores_to_varchar.sql create mode 100644 hr_colaboradores.php create mode 100644 hr_evaluaciones.php create mode 100644 hr_manuales.php create mode 100644 hr_reclutamiento.php create mode 100644 includes/hr_bootstrap.php diff --git a/db/migrations/080_create_hr_reclutamiento_table.sql b/db/migrations/080_create_hr_reclutamiento_table.sql new file mode 100644 index 00000000..84c5bcdd --- /dev/null +++ b/db/migrations/080_create_hr_reclutamiento_table.sql @@ -0,0 +1,26 @@ +-- HR: Reclutamiento (postulantes) +CREATE TABLE IF NOT EXISTS `hr_reclutamiento` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `fecha_registro` DATE NOT NULL, + `nombre_completo` VARCHAR(255) NOT NULL, + `dni` VARCHAR(20) NOT NULL, + `celular` VARCHAR(20) NOT NULL, + `correo` VARCHAR(255) NOT NULL, + `ciudad` VARCHAR(100) NOT NULL, + `puesto_postulado` VARCHAR(150) NOT NULL, + `estado` ENUM( + 'Postulación Recibida', + 'Entrevista Pendiente', + 'Entrevista Realizada', + 'Aprobado', + 'Contratado', + 'No Seleccionado' + ) NOT NULL DEFAULT 'Postulación Recibida', + `observaciones` TEXT, + `cv_pdf_path` TEXT, + `portafolio_path` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + KEY `idx_hr_reclutamiento_estado` (`estado`), + KEY `idx_hr_reclutamiento_fecha_registro` (`fecha_registro`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/081_create_hr_colaboradores_table.sql b/db/migrations/081_create_hr_colaboradores_table.sql new file mode 100644 index 00000000..81206824 --- /dev/null +++ b/db/migrations/081_create_hr_colaboradores_table.sql @@ -0,0 +1,24 @@ +-- HR: Colaboradores (personal activo) +CREATE TABLE IF NOT EXISTS `hr_colaboradores` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `nombre_completo` VARCHAR(255) NOT NULL, + `dni` VARCHAR(20) NOT NULL, + `celular` VARCHAR(20) NOT NULL, + `correo` VARCHAR(255) NOT NULL, + `area` ENUM( + 'Ventas', + 'Almacén', + 'Marketing', + 'Administración', + 'Finanzas', + 'Gerencia' + ) NOT NULL, + `cargo` VARCHAR(150) NOT NULL, + `fecha_ingreso` DATE NOT NULL, + `estado` ENUM('Activo', 'Inactivo', 'Suspendido') NOT NULL DEFAULT 'Activo', + `observaciones` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + KEY `idx_hr_colaboradores_area` (`area`), + KEY `idx_hr_colaboradores_estado` (`estado`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/082_create_hr_manuales_table.sql b/db/migrations/082_create_hr_manuales_table.sql new file mode 100644 index 00000000..69d23b7c --- /dev/null +++ b/db/migrations/082_create_hr_manuales_table.sql @@ -0,0 +1,29 @@ +-- HR: Manuales y Capacitación +CREATE TABLE IF NOT EXISTS `hr_manuales` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `titulo` VARCHAR(255) NOT NULL, + `categoria` ENUM( + 'Introducción a la Empresa', + 'Manual de Ventas', + 'Guía de Objeciones', + 'Script Comercial', + 'Atención al Cliente', + 'Manual de Almacén', + 'Manual de Inventario', + 'Procedimientos Operativos', + 'Políticas Internas', + 'Marketing', + 'Otros' + ) NOT NULL, + `area` ENUM('General', 'Ventas', 'Almacén', 'Marketing', 'Administración') NOT NULL DEFAULT 'General', + `descripcion` TEXT, + `archivo_pdf_path` TEXT, + `archivo_word_path` TEXT, + `fecha_creacion` DATE NOT NULL, + `estado` ENUM('Activo', 'Inactivo') NOT NULL DEFAULT 'Activo', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + KEY `idx_hr_manuales_categoria` (`categoria`), + KEY `idx_hr_manuales_area` (`area`), + KEY `idx_hr_manuales_estado` (`estado`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/083_create_hr_evaluaciones_table.sql b/db/migrations/083_create_hr_evaluaciones_table.sql new file mode 100644 index 00000000..9673eed6 --- /dev/null +++ b/db/migrations/083_create_hr_evaluaciones_table.sql @@ -0,0 +1,18 @@ +-- HR: Evaluaciones de desempeño +CREATE TABLE IF NOT EXISTS `hr_evaluaciones` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `colaborador_id` INT NOT NULL, + `area` ENUM('Ventas', 'Almacén', 'Marketing', 'Administración', 'Finanzas', 'Gerencia') NOT NULL, + `fecha` DATE NOT NULL, + `tipo_evaluacion` VARCHAR(150) NOT NULL, + `puntaje` INT NOT NULL DEFAULT 0, + `resultado` ENUM('Aprobado', 'En Capacitación', 'Requiere Seguimiento') NOT NULL, + `observaciones` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `fk_hr_evaluaciones_colaborador` + FOREIGN KEY (`colaborador_id`) REFERENCES `hr_colaboradores`(`id`) + ON DELETE CASCADE, + KEY `idx_hr_evaluaciones_fecha` (`fecha`), + KEY `idx_hr_evaluaciones_resultado` (`resultado`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/084_add_hr_reclutamiento_indicadores_columns.sql b/db/migrations/084_add_hr_reclutamiento_indicadores_columns.sql new file mode 100644 index 00000000..6c0e689d --- /dev/null +++ b/db/migrations/084_add_hr_reclutamiento_indicadores_columns.sql @@ -0,0 +1,10 @@ +-- HR: Reclutamiento - indicadores (rubrica) y datos de soporte +-- Agrega columnas para mostrar y guardar los 7 indicadores definidos en Reclutamiento. + +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `edad_hijos_estudia_trabaja` VARCHAR(255) NULL; +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `organizacion_personal` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `claridad_expresarse` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `manejo_objeciones` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `experiencia_otros_trabajos` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `experiencia_relevante_ventas` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` ADD COLUMN IF NOT EXISTS `empatia_trato` VARCHAR(50) NULL; diff --git a/db/migrations/085_convert_hr_reclutamiento_indicadores_to_varchar.sql b/db/migrations/085_convert_hr_reclutamiento_indicadores_to_varchar.sql new file mode 100644 index 00000000..f92224dd --- /dev/null +++ b/db/migrations/085_convert_hr_reclutamiento_indicadores_to_varchar.sql @@ -0,0 +1,9 @@ +-- HR: Reclutamiento - indicadores (rubrica) en LETRAS (A/B/C...) en vez de números +-- Convierte columnas previamente creadas como INT a VARCHAR para permitir guardar letras. + +ALTER TABLE `hr_reclutamiento` MODIFY COLUMN `organizacion_personal` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` MODIFY COLUMN `claridad_expresarse` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` MODIFY COLUMN `manejo_objeciones` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` MODIFY COLUMN `experiencia_otros_trabajos` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` MODIFY COLUMN `experiencia_relevante_ventas` VARCHAR(50) NULL; +ALTER TABLE `hr_reclutamiento` MODIFY COLUMN `empatia_trato` VARCHAR(50) NULL; diff --git a/download_ruta_contraentrega.php b/download_ruta_contraentrega.php index e7e64152..44aec406 100644 --- a/download_ruta_contraentrega.php +++ b/download_ruta_contraentrega.php @@ -85,6 +85,26 @@ function formatPriceSegment(float $value): string return $s; } + +function countryToInitials(string $value): string +{ + $value = trim($value); + if ($value === '') { + return ''; + } + + // Normalize accents to ASCII (e.g. 'Perú' -> 'Peru') + $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value); + if ($ascii === false || $ascii === null) { + $ascii = $value; + } + + $ascii = (string)$ascii; + $ascii = preg_replace('/[^A-Za-z]/', '', $ascii); + $initials = substr($ascii, 0, 3); + return strtoupper($initials); +} + function extractProductNames(array $pedido): array { $names = []; @@ -283,6 +303,9 @@ try { $stmt->execute($params); $pedidos = $stmt->fetchAll(PDO::FETCH_ASSOC); + $courier_default = 1; + $fecha_despacho = date('d/m/Y'); + $rows = []; $rows[] = [ 'Nombre y apellido', @@ -298,12 +321,17 @@ try { 'Cantidad', 'Precio', 'Total', - 'DE DEDICATORIA / OBS.' + 'COURIER', + 'DE DEDICATORIA / OBS.', + 'F.DESPACHO' ]; foreach ($pedidos as $pedido) { [$provincia, $distrito] = splitProvinciaDistrito($pedido['codigo_rastreo'] ?? ''); + $pais_raw = (string)($pedido['pais'] ?? 'Perú'); + $pais_code = countryToInitials($pais_raw); + $nota_adicional = trim((string)($pedido['nota_adicional'] ?? '')); if ($nota_adicional === '') { $nota_adicional = trim((string)($pedido['descargo'] ?? '')); @@ -375,7 +403,7 @@ try { $rows[] = [ (string)($pedido['nombre_completo'] ?? ''), (string)($pedido['celular'] ?? ''), - 'Perú', + $pais_code, (string)($pedido['sede_envio'] ?? ''), $provincia, $distrito, @@ -386,7 +414,9 @@ try { $cantidadOut, $precioOut, $total, - $nota_adicional_display + $courier_default, + $nota_adicional_display, + $fecha_despacho ]; } diff --git a/hr_colaboradores.php b/hr_colaboradores.php new file mode 100644 index 00000000..c468c9b9 --- /dev/null +++ b/hr_colaboradores.php @@ -0,0 +1,356 @@ +getMessage(); +} + +$pageTitle = "Recursos Humanos - Colaboradores"; +$pageDescription = "Gestión del personal: registro, área, cargo y estado."; + +$areasValidas = ['Ventas', 'Almacén', 'Marketing', 'Administración', 'Finanzas', 'Gerencia']; +$estadosValidos = ['Activo', 'Inactivo', 'Suspendido']; + +$notice = null; +$noticeType = 'success'; + +if (!empty($schemaInitError)) { + $notice = 'Error al preparar las tablas de Recursos Humanos.'; + $noticeType = 'danger'; + error_log('HR schema init error (colaboradores): ' . $schemaInitError); +} + +if (isset($_GET['success']) && $_GET['success'] === '1') { + $notice = 'Colaborador guardado correctamente.'; + $noticeType = 'success'; +} +if (isset($_GET['estado_updated']) && $_GET['estado_updated'] === '1') { + $notice = 'Estado actualizado correctamente.'; + $noticeType = 'success'; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + + if ($action === 'add_colaborador') { + try { + $nombre_completo = trim((string)($_POST['nombre_completo'] ?? '')); + $dni = trim((string)($_POST['dni'] ?? '')); + $celular = trim((string)($_POST['celular'] ?? '')); + $correo = trim((string)($_POST['correo'] ?? '')); + $area = (string)($_POST['area'] ?? ''); + $cargo = trim((string)($_POST['cargo'] ?? '')); + $fecha_ingreso = $_POST['fecha_ingreso'] ?? date('Y-m-d'); + $estado = (string)($_POST['estado'] ?? 'Activo'); + $observaciones = trim((string)($_POST['observaciones'] ?? '')); + + if ($nombre_completo === '' || $dni === '' || $celular === '' || $correo === '' || $cargo === '' || $area === '') { + throw new RuntimeException('Faltan campos obligatorios.'); + } + if (!filter_var($correo, FILTER_VALIDATE_EMAIL)) { + throw new RuntimeException('Correo inválido.'); + } + if (!in_array($area, $areasValidas, true)) { + throw new RuntimeException('Área inválida.'); + } + if (!in_array($estado, $estadosValidos, true)) { + throw new RuntimeException('Estado inválido.'); + } + + $stmt = $pdo->prepare('INSERT INTO hr_colaboradores (nombre_completo, dni, celular, correo, area, cargo, fecha_ingreso, estado, observaciones) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'); + $stmt->execute([ + $nombre_completo, + $dni, + $celular, + $correo, + $area, + $cargo, + $fecha_ingreso, + $estado, + $observaciones, + ]); + + header('Location: hr_colaboradores.php?success=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo guardar el colaborador: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); + $noticeType = 'danger'; + } + } + + if ($action === 'update_colaborador_estado') { + try { + $id = (int)($_POST['id'] ?? 0); + $nuevoEstado = (string)($_POST['estado'] ?? ''); + + if ($id <= 0) { + throw new RuntimeException('ID inválido.'); + } + if (!in_array($nuevoEstado, $estadosValidos, true)) { + throw new RuntimeException('Estado inválido.'); + } + + $stmt = $pdo->prepare('UPDATE hr_colaboradores SET estado = ? WHERE id = ?'); + $stmt->execute([$nuevoEstado, $id]); + + header('Location: hr_colaboradores.php?estado_updated=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo actualizar el estado.'; + $noticeType = 'danger'; + } + } +} + +// --- Filtros --- +$filter_area = (string)($_GET['area'] ?? 'Todas'); +$filter_estado = (string)($_GET['estado'] ?? 'Todos'); +$search = trim((string)($_GET['q'] ?? '')); + +if ($filter_area !== 'Todas' && !in_array($filter_area, $areasValidas, true)) { + $filter_area = 'Todas'; +} + +if ($filter_estado !== 'Todos' && !in_array($filter_estado, $estadosValidos, true)) { + $filter_estado = 'Todos'; +} + +$where = []; +$params = []; + +if ($filter_area !== 'Todas') { + $where[] = 'area = ?'; + $params[] = $filter_area; +} + +if ($filter_estado !== 'Todos') { + $where[] = 'estado = ?'; + $params[] = $filter_estado; +} + +if ($search !== '') { + $where[] = '(nombre_completo LIKE ? OR dni LIKE ? OR cargo LIKE ?)'; + $like = '%' . $search . '%'; + $params[] = $like; + $params[] = $like; + $params[] = $like; +} + +$sql = 'SELECT id, nombre_completo, dni, celular, correo, area, cargo, fecha_ingreso, estado, observaciones FROM hr_colaboradores'; +if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); +} +$sql .= ' ORDER BY fecha_ingreso DESC, id DESC'; + +$colaboradores = []; +try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $colaboradores = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (PDOException $e) { + error_log('HR Colaboradores DB error: ' . $e->getMessage()); + $notice = 'No se pudo cargar la lista de colaboradores.'; + $noticeType = 'danger'; +} + + +$fechaHoy = date('Y-m-d'); + +include 'layout_header.php'; +?> + +
+ + + + +
+
+
+
+

Nuevo Colaborador

+
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+
+
+

Colaboradores

+ Total: +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ Limpiar + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNombre CompletoDNICelularCorreoÁreaCargoFecha IngresoEstadoObservacionesAcciones
No hay colaboradores para los filtros seleccionados.
+
+
+
+ + + + +
+
+
+
+
+
+ + diff --git a/hr_evaluaciones.php b/hr_evaluaciones.php new file mode 100644 index 00000000..206e5f1c --- /dev/null +++ b/hr_evaluaciones.php @@ -0,0 +1,331 @@ +getMessage(); +} + +$pageTitle = "Recursos Humanos - Evaluaciones"; +$pageDescription = "Registro de evaluaciones internas y seguimiento del desempeño del personal."; + +$areasValidas = ['Ventas', 'Almacén', 'Marketing', 'Administración', 'Finanzas', 'Gerencia']; +$resultadosValidos = ['Aprobado', 'En Capacitación', 'Requiere Seguimiento']; + +$notice = null; +$noticeType = 'success'; + +if (!empty($schemaInitError)) { + $notice = 'Error al preparar las tablas de Recursos Humanos.'; + $noticeType = 'danger'; + error_log('HR schema init error (evaluaciones): ' . $schemaInitError); +} + +if (isset($_GET['success']) && $_GET['success'] === '1') { + $notice = 'Evaluación guardada correctamente.'; + $noticeType = 'success'; +} + +// Lista de colaboradores para los selects +$colaboradores = []; +try { + $stmt = $pdo->query('SELECT id, nombre_completo, area FROM hr_colaboradores ORDER BY nombre_completo ASC'); + $colaboradores = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (Throwable $e) { + $colaboradores = []; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + + if ($action === 'add_evaluacion') { + try { + $colaborador_id = (int)($_POST['colaborador_id'] ?? 0); + $fecha = $_POST['fecha'] ?? date('Y-m-d'); + $tipo_evaluacion = trim((string)($_POST['tipo_evaluacion'] ?? '')); + $puntaje = (int)($_POST['puntaje'] ?? 0); + $resultado = (string)($_POST['resultado'] ?? ''); + $observaciones = trim((string)($_POST['observaciones'] ?? '')); + + if ($colaborador_id <= 0) { + throw new RuntimeException('Selecciona un colaborador.'); + } + if ($tipo_evaluacion === '') { + throw new RuntimeException('Tipo de evaluación es obligatorio.'); + } + if (!in_array($resultado, $resultadosValidos, true)) { + throw new RuntimeException('Resultado inválido.'); + } + + // Traer área del colaborador para guardarla en el registro + $stmtArea = $pdo->prepare('SELECT area FROM hr_colaboradores WHERE id = ? LIMIT 1'); + $stmtArea->execute([$colaborador_id]); + $areaRow = $stmtArea->fetch(PDO::FETCH_ASSOC); + if (!$areaRow || empty($areaRow['area'])) { + throw new RuntimeException('No se encontró el área del colaborador.'); + } + $area = (string)$areaRow['area']; + if (!in_array($area, $areasValidas, true)) { + // Seguridad extra + throw new RuntimeException('Área inválida en el colaborador.'); + } + + $stmt = $pdo->prepare('INSERT INTO hr_evaluaciones (colaborador_id, area, fecha, tipo_evaluacion, puntaje, resultado, observaciones) VALUES (?, ?, ?, ?, ?, ?, ?)'); + $stmt->execute([ + $colaborador_id, + $area, + $fecha, + $tipo_evaluacion, + $puntaje, + $resultado, + $observaciones, + ]); + + header('Location: hr_evaluaciones.php?success=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo guardar la evaluación: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); + $noticeType = 'danger'; + } + } +} + +// --- Filtros de listado --- +$f_colaborador = (int)($_GET['colaborador_id'] ?? 0); +$f_resultado = (string)($_GET['resultado'] ?? 'Todos'); +$f_fecha_desde = trim((string)($_GET['fecha_desde'] ?? '')); +$f_fecha_hasta = trim((string)($_GET['fecha_hasta'] ?? '')); + +if ($f_resultado !== 'Todos' && !in_array($f_resultado, $resultadosValidos, true)) { + $f_resultado = 'Todos'; +} + +$where = []; +$params = []; + +if ($f_colaborador > 0) { + $where[] = 'e.colaborador_id = ?'; + $params[] = $f_colaborador; +} +if ($f_resultado !== 'Todos') { + $where[] = 'e.resultado = ?'; + $params[] = $f_resultado; +} +if ($f_fecha_desde !== '') { + $where[] = 'e.fecha >= ?'; + $params[] = $f_fecha_desde; +} +if ($f_fecha_hasta !== '') { + $where[] = 'e.fecha <= ?'; + $params[] = $f_fecha_hasta; +} + +$sql = 'SELECT e.id, e.fecha, e.tipo_evaluacion, e.puntaje, e.resultado, e.area, e.observaciones, c.nombre_completo, c.dni FROM hr_evaluaciones e LEFT JOIN hr_colaboradores c ON c.id = e.colaborador_id'; +if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); +} +$sql .= ' ORDER BY e.fecha DESC, e.id DESC'; + +$evaluaciones = []; +try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $evaluaciones = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (PDOException $e) { + error_log('HR Evaluaciones DB error: ' . $e->getMessage()); + $notice = 'No se pudo cargar el listado de evaluaciones.'; + $noticeType = 'danger'; +} + + +$fechaHoy = date('Y-m-d'); + +include 'layout_header.php'; +?> + +
+ + + + +
+
+
+
+

Nueva Evaluación

+
+
+ +
+ Primero agrega colaboradores en Colaboradores. +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+
+ +
+
+
+

Evaluaciones

+ Total: +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Limpiar + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDColaboradorÁreaFechaTipoPuntajeResultadoObservaciones
No hay evaluaciones para los filtros seleccionados.
+ + +
DNI:
+ +
+ + + + +
+
+
+
+
+
+ + diff --git a/hr_manuales.php b/hr_manuales.php new file mode 100644 index 00000000..a3393dd1 --- /dev/null +++ b/hr_manuales.php @@ -0,0 +1,461 @@ +getMessage(); +} + +$pageTitle = "Recursos Humanos - Manuales y Capacitación"; +$pageDescription = "Repositorio interno de manuales, procedimientos y material de capacitación: PDF/Word, búsqueda y descargas."; + +$categoriasValidas = [ + 'Introducción a la Empresa', + 'Manual de Ventas', + 'Guía de Objeciones', + 'Script Comercial', + 'Atención al Cliente', + 'Manual de Almacén', + 'Manual de Inventario', + 'Procedimientos Operativos', + 'Políticas Internas', + 'Marketing', + 'Otros' +]; + +$areasValidas = ['General', 'Ventas', 'Almacén', 'Marketing', 'Administración']; +$estadosValidos = ['Activo', 'Inactivo']; + +function hr_move_uploaded_file(array $file, string $uploadDirAbs, array $allowedExts): string +{ + $error = $file['error'] ?? UPLOAD_ERR_NO_FILE; + if ($error !== UPLOAD_ERR_OK) { + throw new RuntimeException('Error al cargar el archivo.'); + } + + $size = (int)($file['size'] ?? 0); + $maxBytes = 8 * 1024 * 1024; + if ($size <= 0) { + throw new RuntimeException('Archivo vacío.'); + } + if ($size > $maxBytes) { + throw new RuntimeException('El archivo excede el tamaño máximo permitido (8MB).'); + } + + $origName = (string)($file['name'] ?? 'archivo'); + $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); + if ($ext === '' || !in_array($ext, $allowedExts, true)) { + throw new RuntimeException('Tipo de archivo no permitido.'); + } + + if (!is_dir($uploadDirAbs)) { + mkdir($uploadDirAbs, 0775, true); + } + + $base = pathinfo($origName, PATHINFO_FILENAME); + $base = preg_replace('/[^A-Za-z0-9._-]+/', '_', $base); + if (!$base) { + $base = 'archivo'; + } + + $newName = time() . '_' . $base . '.' . $ext; + $targetAbs = rtrim($uploadDirAbs, '/\\') . DIRECTORY_SEPARATOR . $newName; + + if (!move_uploaded_file($file['tmp_name'], $targetAbs)) { + throw new RuntimeException('No se pudo guardar el archivo en el servidor.'); + } + + $uploadDirRel = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', rtrim($uploadDirAbs, '/\\')); + return $uploadDirRel . '/' . $newName; +} + +$notice = null; +$noticeType = 'success'; + +if (!empty($schemaInitError)) { + $notice = 'Error al preparar las tablas de Recursos Humanos.'; + $noticeType = 'danger'; + error_log('HR schema init error (manuales): ' . $schemaInitError); +} + +if (isset($_GET['success']) && $_GET['success'] === '1') { + $notice = 'Manual guardado correctamente.'; + $noticeType = 'success'; +} +if (isset($_GET['estado_updated']) && $_GET['estado_updated'] === '1') { + $notice = 'Estado del manual actualizado correctamente.'; + $noticeType = 'success'; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + + if ($action === 'add_manual') { + try { + $titulo = trim((string)($_POST['titulo'] ?? '')); + $categoria = (string)($_POST['categoria'] ?? ''); + $area = (string)($_POST['area'] ?? 'General'); + $descripcion = trim((string)($_POST['descripcion'] ?? '')); + $fecha_creacion = $_POST['fecha_creacion'] ?? date('Y-m-d'); + $estado = (string)($_POST['estado'] ?? 'Activo'); + + if ($titulo === '' || $categoria === '' || $area === '') { + throw new RuntimeException('Faltan campos obligatorios.'); + } + if (!in_array($categoria, $categoriasValidas, true)) { + throw new RuntimeException('Categoría inválida.'); + } + if (!in_array($area, $areasValidas, true)) { + throw new RuntimeException('Área inválida.'); + } + if (!in_array($estado, $estadosValidos, true)) { + throw new RuntimeException('Estado inválido.'); + } + + $pdfPath = null; + $wordPath = null; + + $pdfFile = $_FILES['archivo_pdf'] ?? null; + if ($pdfFile && !empty($pdfFile['name']) && ($pdfFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) { + $pdfUploadDirAbs = __DIR__ . '/assets/uploads/hr/manuales/pdf/'; + $pdfPath = hr_move_uploaded_file($pdfFile, $pdfUploadDirAbs, ['pdf']); + } + + $wordFile = $_FILES['archivo_word'] ?? null; + if ($wordFile && !empty($wordFile['name']) && ($wordFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) { + $wordUploadDirAbs = __DIR__ . '/assets/uploads/hr/manuales/word/'; + $wordPath = hr_move_uploaded_file($wordFile, $wordUploadDirAbs, ['doc', 'docx']); + } + + if (empty($pdfPath) && empty($wordPath)) { + throw new RuntimeException('Sube al menos un archivo: PDF y/o Word.'); + } + + $stmt = $pdo->prepare('INSERT INTO hr_manuales (titulo, categoria, area, descripcion, archivo_pdf_path, archivo_word_path, fecha_creacion, estado) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'); + $stmt->execute([ + $titulo, + $categoria, + $area, + $descripcion, + $pdfPath, + $wordPath, + $fecha_creacion, + $estado, + ]); + + header('Location: hr_manuales.php?success=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo guardar el manual: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); + $noticeType = 'danger'; + } + } + + if ($action === 'update_manual_estado') { + try { + $id = (int)($_POST['id'] ?? 0); + $nuevoEstado = (string)($_POST['estado'] ?? ''); + + if ($id <= 0) { + throw new RuntimeException('ID inválido.'); + } + if (!in_array($nuevoEstado, $estadosValidos, true)) { + throw new RuntimeException('Estado inválido.'); + } + + $stmt = $pdo->prepare('UPDATE hr_manuales SET estado = ? WHERE id = ?'); + $stmt->execute([$nuevoEstado, $id]); + + header('Location: hr_manuales.php?estado_updated=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo actualizar el estado del manual.'; + $noticeType = 'danger'; + } + } +} + +// --- Filtros --- +$q = trim((string)($_GET['q'] ?? '')); +$fCategoria = (string)($_GET['categoria'] ?? 'Todas'); +$fArea = (string)($_GET['area'] ?? 'Todas'); +$fEstado = (string)($_GET['estado'] ?? 'Activo'); + +if ($fCategoria !== 'Todas' && !in_array($fCategoria, $categoriasValidas, true)) { + $fCategoria = 'Todas'; +} +if ($fArea !== 'Todas' && !in_array($fArea, $areasValidas, true)) { + $fArea = 'Todas'; +} +if (!in_array($fEstado, array_merge(['Todos'], $estadosValidos), true)) { + $fEstado = 'Activo'; +} + +$where = []; +$params = []; + +if ($q !== '') { + $where[] = 'titulo LIKE ?'; + $params[] = '%' . $q . '%'; +} +if ($fCategoria !== 'Todas') { + $where[] = 'categoria = ?'; + $params[] = $fCategoria; +} +if ($fArea !== 'Todas') { + $where[] = 'area = ?'; + $params[] = $fArea; +} +if ($fEstado !== 'Todos') { + $where[] = 'estado = ?'; + $params[] = $fEstado; +} + +$sql = 'SELECT id, titulo, categoria, area, descripcion, archivo_pdf_path, archivo_word_path, fecha_creacion, estado FROM hr_manuales'; +if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); +} +$sql .= ' ORDER BY fecha_creacion DESC, id DESC'; + +$manuales = []; +try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $manuales = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (PDOException $e) { + error_log('HR Manuales DB error: ' . $e->getMessage()); + $notice = 'No se pudo cargar el repositorio de manuales.'; + $noticeType = 'danger'; +} + + +$fechaHoy = date('Y-m-d'); + +include 'layout_header.php'; +?> + +
+ + + + +
+
+
+
+

Subir Manual

+
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+
+
+

Manuales y Capacitación

+ Total: +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Limpiar + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDTítuloCategoríaÁreaFechaEstadoArchivoDescripciónAcciones
No hay manuales con los filtros seleccionados.
+ + + + + + + + +
+ + + PDF + + + + + Word + + + + + +
+
+ +
+
+
+ + + + +
+
+
+
+
+
+ + diff --git a/hr_reclutamiento.php b/hr_reclutamiento.php new file mode 100644 index 00000000..a3edf198 --- /dev/null +++ b/hr_reclutamiento.php @@ -0,0 +1,604 @@ +getMessage(); +} + +$pageTitle = "Recursos Humanos - Reclutamiento"; +$pageDescription = "Gestión de postulantes: CV, portafolio, indicadores y estado del proceso de reclutamiento."; + +$estadosValidos = [ + 'Postulación Recibida', + 'Entrevista Pendiente', + 'Entrevista Realizada', + 'Aprobado', + 'Contratado', + 'No Seleccionado', +]; + +function hr_move_uploaded_file(array $file, string $uploadDirAbs, array $allowedExts): string +{ + $error = $file['error'] ?? UPLOAD_ERR_NO_FILE; + if ($error !== UPLOAD_ERR_OK) { + throw new RuntimeException('Error al cargar el archivo.'); + } + + $size = (int)($file['size'] ?? 0); + $maxBytes = 8 * 1024 * 1024; + if ($size <= 0) { + throw new RuntimeException('Archivo vacío.'); + } + if ($size > $maxBytes) { + throw new RuntimeException('El archivo excede el tamaño máximo permitido (8MB).'); + } + + $origName = (string)($file['name'] ?? 'archivo'); + $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); + if ($ext === '' || !in_array($ext, $allowedExts, true)) { + throw new RuntimeException('Tipo de archivo no permitido.'); + } + + if (!is_dir($uploadDirAbs)) { + mkdir($uploadDirAbs, 0775, true); + } + + $base = pathinfo($origName, PATHINFO_FILENAME); + $base = preg_replace('/[^A-Za-z0-9._-]+/', '_', $base); + if (!$base) { + $base = 'archivo'; + } + + $newName = time() . '_' . $base . '.' . $ext; + $targetAbs = rtrim($uploadDirAbs, '/\\') . DIRECTORY_SEPARATOR . $newName; + + if (!move_uploaded_file($file['tmp_name'], $targetAbs)) { + throw new RuntimeException('No se pudo guardar el archivo en el servidor.'); + } + + $uploadDirRel = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', rtrim($uploadDirAbs, '/\\')); + return $uploadDirRel . '/' . $newName; +} + +$notice = null; +$noticeType = 'success'; + +if (!empty($schemaInitError)) { + $notice = 'Error al preparar las tablas de Recursos Humanos.'; + $noticeType = 'danger'; + error_log('HR schema init error (reclutamiento): ' . $schemaInitError); +} + +if (isset($_GET['success']) && $_GET['success'] === '1') { + $notice = 'Postulante guardado correctamente.'; + $noticeType = 'success'; +} +if (isset($_GET['estado_updated']) && $_GET['estado_updated'] === '1') { + $notice = 'Estado actualizado correctamente.'; + $noticeType = 'success'; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + + if ($action === 'add_reclutamiento') { + try { + $fecha_registro = $_POST['fecha_registro'] ?? date('Y-m-d'); + $nombre_completo = trim((string)($_POST['nombre_completo'] ?? '')); + $dni = trim((string)($_POST['dni'] ?? '')); + $celular = trim((string)($_POST['celular'] ?? '')); + $correo = trim((string)($_POST['correo'] ?? '')); + // Truncamos para evitar errores de longitud en BD (cuando el campo no es obligatorio). + $dni = $dni === '' ? '' : substr($dni, 0, 20); + $correo = $correo === '' ? '' : substr($correo, 0, 255); + $ciudad = trim((string)($_POST['ciudad'] ?? '')); + $puesto_postulado = trim((string)($_POST['puesto_postulado'] ?? '')); + $estado = (string)($_POST['estado'] ?? 'Postulación Recibida'); + $observaciones = trim((string)($_POST['observaciones'] ?? '')); + + // Indicadores (opcionales) + $edad_hijos_estudia_trabaja = trim((string)($_POST['edad_hijos_estudia_trabaja'] ?? '')); + if ($edad_hijos_estudia_trabaja === '') { + $edad_hijos_estudia_trabaja = null; + } + + // Indicadores (opcionales) - se guardan como LETRAS (A/B/C...). + $organizacion_personal = strtoupper(trim((string)($_POST['organizacion_personal'] ?? ''))); + $claridad_expresarse = strtoupper(trim((string)($_POST['claridad_expresarse'] ?? ''))); + $manejo_objeciones = strtoupper(trim((string)($_POST['manejo_objeciones'] ?? ''))); + $experiencia_otros_trabajos = strtoupper(trim((string)($_POST['experiencia_otros_trabajos'] ?? ''))); + $experiencia_relevante_ventas = strtoupper(trim((string)($_POST['experiencia_relevante_ventas'] ?? ''))); + $empatia_trato = strtoupper(trim((string)($_POST['empatia_trato'] ?? ''))); + + $organizacion_personal = $organizacion_personal === '' ? null : substr($organizacion_personal, 0, 50); + $claridad_expresarse = $claridad_expresarse === '' ? null : substr($claridad_expresarse, 0, 50); + $manejo_objeciones = $manejo_objeciones === '' ? null : substr($manejo_objeciones, 0, 50); + $experiencia_otros_trabajos = $experiencia_otros_trabajos === '' ? null : substr($experiencia_otros_trabajos, 0, 50); + $experiencia_relevante_ventas = $experiencia_relevante_ventas === '' ? null : substr($experiencia_relevante_ventas, 0, 50); + $empatia_trato = $empatia_trato === '' ? null : substr($empatia_trato, 0, 50); + + if ($nombre_completo === '' || $celular === '' || $ciudad === '' || $puesto_postulado === '') { + throw new RuntimeException('Faltan campos obligatorios.'); + } + if ($correo !== '' && !filter_var($correo, FILTER_VALIDATE_EMAIL)) { + throw new RuntimeException('Correo inválido.'); + } + if (!in_array($estado, $estadosValidos, true)) { + throw new RuntimeException('Estado inválido.'); + } + + $cvPdfFile = $_FILES['cv_pdf'] ?? null; + if (!$cvPdfFile || ($cvPdfFile['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + throw new RuntimeException('El CV (PDF) es obligatorio.'); + } + + $cvUploadDirAbs = __DIR__ . '/assets/uploads/hr/reclutamiento/cv/'; + $cv_pdf_path = hr_move_uploaded_file($cvPdfFile, $cvUploadDirAbs, ['pdf']); + + $portafolio_path = null; + $portFile = $_FILES['portafolio'] ?? null; + if ($portFile && !empty($portFile['name']) && ($portFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) { + $portUploadDirAbs = __DIR__ . '/assets/uploads/hr/reclutamiento/portafolio/'; + // Portafolio opcional: permitimos PDF o documentos comunes. + $portafolio_path = hr_move_uploaded_file($portFile, $portUploadDirAbs, ['pdf', 'doc', 'docx']); + } + + $stmt = $pdo->prepare('INSERT INTO hr_reclutamiento (fecha_registro, edad_hijos_estudia_trabaja, organizacion_personal, claridad_expresarse, manejo_objeciones, experiencia_otros_trabajos, experiencia_relevante_ventas, empatia_trato, nombre_completo, dni, celular, correo, ciudad, puesto_postulado, estado, observaciones, cv_pdf_path, portafolio_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); + $stmt->execute([ + $fecha_registro, + $edad_hijos_estudia_trabaja, + $organizacion_personal, + $claridad_expresarse, + $manejo_objeciones, + $experiencia_otros_trabajos, + $experiencia_relevante_ventas, + $empatia_trato, + $nombre_completo, + $dni, + $celular, + $correo, + $ciudad, + $puesto_postulado, + $estado, + $observaciones, + $cv_pdf_path, + $portafolio_path, + ]); + + header('Location: hr_reclutamiento.php?success=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo guardar el postulante: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); + $noticeType = 'danger'; + } + } + + if ($action === 'update_reclutamiento_estado') { + try { + $id = (int)($_POST['id'] ?? 0); + $nuevoEstado = (string)($_POST['estado'] ?? ''); + + if ($id <= 0) { + throw new RuntimeException('ID inválido.'); + } + if (!in_array($nuevoEstado, $estadosValidos, true)) { + throw new RuntimeException('Estado inválido.'); + } + + $stmt = $pdo->prepare('UPDATE hr_reclutamiento SET estado = ? WHERE id = ?'); + $stmt->execute([$nuevoEstado, $id]); + + header('Location: hr_reclutamiento.php?estado_updated=1'); + exit; + } catch (Throwable $e) { + $notice = 'No se pudo actualizar el estado.'; + $noticeType = 'danger'; + } + } +} + +// --- Filtros --- +$filter_puesto = trim((string)($_GET['puesto'] ?? '')); +$filter_estado = (string)($_GET['estado'] ?? 'Todos'); +$fecha_desde = trim((string)($_GET['fecha_desde'] ?? '')); +$fecha_hasta = trim((string)($_GET['fecha_hasta'] ?? '')); + +if ($filter_estado !== 'Todos' && !in_array($filter_estado, $estadosValidos, true)) { + $filter_estado = 'Todos'; +} + +$where = []; +$params = []; + +if ($filter_puesto !== '') { + $where[] = 'puesto_postulado LIKE ?'; + $params[] = '%' . $filter_puesto . '%'; +} +if ($filter_estado !== 'Todos') { + $where[] = 'estado = ?'; + $params[] = $filter_estado; +} +if ($fecha_desde !== '') { + $where[] = 'fecha_registro >= ?'; + $params[] = $fecha_desde; +} +if ($fecha_hasta !== '') { + $where[] = 'fecha_registro <= ?'; + $params[] = $fecha_hasta; +} + +$sql = 'SELECT id, fecha_registro, edad_hijos_estudia_trabaja, organizacion_personal, claridad_expresarse, manejo_objeciones, experiencia_otros_trabajos, experiencia_relevante_ventas, empatia_trato, nombre_completo, dni, celular, correo, ciudad, puesto_postulado, estado, observaciones, cv_pdf_path, portafolio_path FROM hr_reclutamiento'; +if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); +} +$sql .= ' ORDER BY fecha_registro DESC, id DESC'; + +$candidatos = []; +try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $candidatos = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (PDOException $e) { + error_log('HR Reclutamiento DB error: ' . $e->getMessage()); + $notice = 'No se pudo cargar la lista de postulantes.'; + $noticeType = 'danger'; +} + +$fechaHoy = date('Y-m-d'); + +include 'layout_header.php'; +?> + +
+ + + + +
+ +
+
+
+

Nuevo Postulante

+
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
Sube el PDF del CV.
+
+
+ +
+
+ + +
PDF o documento Word (si aplica).
+
+ +
+ + +
+
+ + +
+
+
+
+ + +
+
+
+

Postulantes

+ Total: +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Limpiar + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDFecha RegistroEdad, Hijos/Estudia TrabajaOCUPACIONClaridad en ExpresarseManejo de ObjecionesEXPERIENCIA LABORALRESPONSABILIDAD/COMPROMISODISPONIBILIDAD INMEDIATANombre CompletoDNICelularCorreoCiudadPuestoEstadoCVPortafolioObservacionesAcciones
No hay postulantes para los filtros seleccionados.
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+
+
+
+
+
+ + diff --git a/includes/hr_bootstrap.php b/includes/hr_bootstrap.php new file mode 100644 index 00000000..fff7985d --- /dev/null +++ b/includes/hr_bootstrap.php @@ -0,0 +1,145 @@ +prepare( + 'SELECT COUNT(*) AS c ' + . 'FROM information_schema.tables ' + . 'WHERE table_schema = DATABASE() AND table_name = ?' + ); + + foreach ($requiredTables as $t) { + $checkTableStmt->execute([$t]); + $row = $checkTableStmt->fetch(PDO::FETCH_ASSOC); + $count = (int)($row['c'] ?? 0); + if ($count === 0) { + $missingTables[] = $t; + } + } + + if (!empty($missingTables)) { + // Ejecutamos las migraciones HR en orden para que existan claves. + $migrationFiles = [ + __DIR__ . '/../db/migrations/080_create_hr_reclutamiento_table.sql', + __DIR__ . '/../db/migrations/081_create_hr_colaboradores_table.sql', + __DIR__ . '/../db/migrations/082_create_hr_manuales_table.sql', + __DIR__ . '/../db/migrations/083_create_hr_evaluaciones_table.sql', + ]; + + foreach ($migrationFiles as $file) { + if (!is_file($file)) { + throw new RuntimeException('No se encontró la migración: ' . basename($file)); + } + $sql = file_get_contents($file); + if ($sql === false) { + throw new RuntimeException('No se pudo leer la migración: ' . basename($file)); + } + $pdo->exec($sql); + } + } + + // 2) Asegurar columnas necesarias en hr_reclutamiento + $requiredColumns = [ + 'hr_reclutamiento' => [ + 'edad_hijos_estudia_trabaja', + 'organizacion_personal', + 'claridad_expresarse', + 'manejo_objeciones', + 'experiencia_otros_trabajos', + 'experiencia_relevante_ventas', + 'empatia_trato', + ], + ]; + + $checkColStmt = $pdo->prepare( + 'SELECT COUNT(*) AS c ' + . 'FROM information_schema.columns ' + . 'WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?' + ); + + $missingColumns = []; + foreach ($requiredColumns as $table => $cols) { + foreach ($cols as $col) { + $checkColStmt->execute([$table, $col]); + $row = $checkColStmt->fetch(PDO::FETCH_ASSOC); + $count = (int)($row['c'] ?? 0); + if ($count === 0) { + $missingColumns[] = $table . '.' . $col; + } + } + } + + if (!empty($missingColumns)) { + $file = __DIR__ . '/../db/migrations/084_add_hr_reclutamiento_indicadores_columns.sql'; + if (!is_file($file)) { + throw new RuntimeException('No se encontró la migración: ' . basename($file)); + } + $sql = file_get_contents($file); + if ($sql === false) { + throw new RuntimeException('No se pudo leer la migración: ' . basename($file)); + } + $pdo->exec($sql); + } + + // 3) Asegurar tipos correctos para columnas de indicadores (deben aceptar LETRAS) + // Si alguna columna quedó como INT en una BD antigua, evitará errores al guardar valores como "ESTUDIANTE". + $indicatorColumns = [ + 'organizacion_personal', + 'claridad_expresarse', + 'manejo_objeciones', + 'experiencia_otros_trabajos', + 'experiencia_relevante_ventas', + 'empatia_trato', + ]; + + $checkTypeStmt = $pdo->prepare( + 'SELECT DATA_TYPE FROM information_schema.columns ' . + 'WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?' + ); + + $needsIndicatorConversion = false; + foreach ($indicatorColumns as $col) { + $checkTypeStmt->execute(['hr_reclutamiento', $col]); + $row = $checkTypeStmt->fetch(PDO::FETCH_ASSOC); + $dataType = strtolower((string)($row['DATA_TYPE'] ?? '')); + + // Si no es VARCHAR, la convertimos (cubre casos INT/NUMERIC en BD viejas). + if ($dataType !== 'varchar') { + // Si la columna no existe, lo manejamos con el bloque anterior de "missingColumns". + if ($dataType !== '') { + $needsIndicatorConversion = true; + break; + } + } + } + + if ($needsIndicatorConversion) { + $file = __DIR__ . '/../db/migrations/085_convert_hr_reclutamiento_indicadores_to_varchar.sql'; + if (!is_file($file)) { + throw new RuntimeException('No se encontró la migración: ' . basename($file)); + } + $sql = file_get_contents($file); + if ($sql === false) { + throw new RuntimeException('No se pudo leer la migración: ' . basename($file)); + } + $pdo->exec($sql); + } +} diff --git a/layout_header.php b/layout_header.php index f0b6890d..6ef4b3cd 100644 --- a/layout_header.php +++ b/layout_header.php @@ -283,6 +283,38 @@ $navItems = [ 'text' => 'Gestionar Usuarios', 'roles' => ['Administrador', 'admin'] ], + + 'hr_group' => [ + 'icon' => 'fa-user-tie', + 'text' => 'Recursos Humanos', + 'roles' => ['Administrador', 'admin'], + 'submenu' => [ + 'hr_reclutamiento' => [ + 'url' => 'hr_reclutamiento.php', + 'icon' => 'fa-user-plus', + 'text' => 'Reclutamiento', + 'roles' => ['Administrador', 'admin'] + ], + 'hr_colaboradores' => [ + 'url' => 'hr_colaboradores.php', + 'icon' => 'fa-user-check', + 'text' => 'Colaboradores', + 'roles' => ['Administrador', 'admin'] + ], + 'hr_manuales' => [ + 'url' => 'hr_manuales.php', + 'icon' => 'fa-book-open', + 'text' => 'Manuales y Capacitación', + 'roles' => ['Administrador', 'admin'] + ], + 'hr_evaluaciones' => [ + 'url' => 'hr_evaluaciones.php', + 'icon' => 'fa-star', + 'text' => 'Evaluaciones', + 'roles' => ['Administrador', 'admin'] + ] + ] + ], 'marketing_group' => [ 'icon' => 'fa-bullhorn', 'text' => 'Marketing',