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';
+?>
+
+
+
+
+ = $notice; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nombre Completo |
+ DNI |
+ Celular |
+ Correo |
+ Área |
+ Cargo |
+ Fecha Ingreso |
+ Estado |
+ Observaciones |
+ Acciones |
+
+
+
+
+
+ | No hay colaboradores para los filtros seleccionados. |
+
+
+
+
+ | = (int)$c['id']; ?> |
+ = htmlspecialchars($c['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['dni'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['celular'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['correo'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['cargo'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['fecha_ingreso'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['estado'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+
+ = htmlspecialchars($c['observaciones'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
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';
+?>
+
+
+
+
+ = $notice; ?>
+
+
+
+
+
+
+
+
+
+
+ Primero agrega colaboradores en Colaboradores.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Colaborador |
+ Área |
+ Fecha |
+ Tipo |
+ Puntaje |
+ Resultado |
+ Observaciones |
+
+
+
+
+
+ | No hay evaluaciones para los filtros seleccionados. |
+
+
+
+
+ | = (int)$e['id']; ?> |
+
+ = htmlspecialchars($e['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+
+ DNI: = htmlspecialchars($e['dni'], ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+ = htmlspecialchars($e['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($e['fecha'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($e['tipo_evaluacion'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = (int)($e['puntaje'] ?? 0); ?> |
+
+
+ = htmlspecialchars($e['resultado'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+ = htmlspecialchars($e['observaciones'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+
+
+
+
+
+
+
+
+
+
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';
+?>
+
+
+
+
+ = $notice; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Título |
+ Categoría |
+ Área |
+ Fecha |
+ Estado |
+ Archivo |
+ Descripción |
+ Acciones |
+
+
+
+
+
+ | No hay manuales con los filtros seleccionados. |
+
+
+
+
+ | = (int)$m['id']; ?> |
+ = htmlspecialchars($m['titulo'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+
+ = htmlspecialchars($m['categoria'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+ = htmlspecialchars($m['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+ |
+ = htmlspecialchars($m['fecha_creacion'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+
+
+ = htmlspecialchars($m['estado'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+ |
+
+
+ = htmlspecialchars($descripcion, ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
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';
+?>
+
+
+
+
+ = $notice; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Fecha Registro |
+ Edad, Hijos/Estudia Trabaja |
+ OCUPACION |
+ Claridad en Expresarse |
+ Manejo de Objeciones |
+ EXPERIENCIA LABORAL |
+ RESPONSABILIDAD/COMPROMISO |
+ DISPONIBILIDAD INMEDIATA |
+ Nombre Completo |
+ DNI |
+ Celular |
+ Correo |
+ Ciudad |
+ Puesto |
+ Estado |
+ CV |
+ Portafolio |
+ Observaciones |
+ Acciones |
+
+
+
+
+
+ | No hay postulantes para los filtros seleccionados. |
+
+
+
+
+ | = (int)$c['id']; ?> |
+ = htmlspecialchars($c['fecha_registro'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+
+
+
+
+ —
+
+ = htmlspecialchars($edad, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+
+
+ —
+
+ = htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+
+
+ —
+
+ = htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+
+
+ —
+
+ = htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+
+
+ —
+
+ = htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+
+
+ —
+
+ = htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+
+
+
+ —
+
+ = htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?>
+
+ |
+
+ = htmlspecialchars($c['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['dni'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['celular'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['correo'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['ciudad'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+ = htmlspecialchars($c['puesto_postulado'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
+
+
+ = htmlspecialchars($c['estado'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+
+
+
+
+
+
+ —
+
+ |
+
+
+
+
+
+
+
+ —
+
+ |
+
+
+ = htmlspecialchars($c['observaciones'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
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',