Autosave: 20260624-214502

This commit is contained in:
Flatlogic Bot 2026-06-24 21:44:23 +00:00
parent 7da0c2905a
commit e282778646
13 changed files with 2078 additions and 3 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
];
}

356
hr_colaboradores.php Normal file
View File

@ -0,0 +1,356 @@
<?php
session_start();
$userRole = $_SESSION['user_role'] ?? '';
if (!isset($_SESSION['user_id']) || ($userRole !== 'Administrador' && $userRole !== 'admin')) {
header('Location: dashboard.php');
exit;
}
require_once 'db/config.php';
$pdo = db();
$schemaInitError = null;
require_once 'includes/hr_bootstrap.php';
try {
hr_ensure_schema($pdo);
} catch (Throwable $e) {
$schemaInitError = $e->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';
?>
<div class="container-fluid mt-4">
<?php if ($notice): ?>
<div class="alert alert-<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8'); ?> mb-4" role="alert">
<?= $notice; ?>
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h2 class="h5 mb-0">Nuevo Colaborador</h2>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="action" value="add_colaborador">
<div class="mb-3">
<label class="form-label">Nombre Completo</label>
<input type="text" name="nombre_completo" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">DNI</label>
<input type="text" name="dni" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Celular</label>
<input type="text" name="celular" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Correo</label>
<input type="email" name="correo" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Área</label>
<select name="area" class="form-select" required>
<?php foreach ($areasValidas as $a): ?>
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?>"><?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Cargo</label>
<input type="text" name="cargo" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Fecha de Ingreso</label>
<input type="date" name="fecha_ingreso" class="form-control" value="<?= htmlspecialchars($fechaHoy, ENT_QUOTES, 'UTF-8'); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select" required>
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>"><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Observaciones</label>
<textarea name="observaciones" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-save me-1"></i> Guardar
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">Colaboradores</h2>
<span class="badge bg-secondary">Total: <?= count($colaboradores); ?></span>
</div>
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-sm-4">
<label class="form-label">Buscar</label>
<input type="text" name="q" class="form-control" placeholder="Nombre, DNI o Cargo" value="<?= htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-sm-3">
<label class="form-label">Área</label>
<select name="area" class="form-select">
<option value="Todas">Todas</option>
<?php foreach ($areasValidas as $a): ?>
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?>" <?= $filter_area === $a ? 'selected' : ''; ?>><?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select">
<option value="Todos">Todos</option>
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= $filter_estado === $e ? 'selected' : ''; ?>><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 d-flex justify-content-end gap-2">
<a class="btn btn-outline-secondary" href="hr_colaboradores.php">Limpiar</a>
<button class="btn btn-primary" type="submit">
<i class="fas fa-filter me-1"></i> Filtrar
</button>
</div>
</form>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Nombre Completo</th>
<th>DNI</th>
<th>Celular</th>
<th>Correo</th>
<th>Área</th>
<th>Cargo</th>
<th>Fecha Ingreso</th>
<th>Estado</th>
<th>Observaciones</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($colaboradores)): ?>
<tr>
<td colspan="11" class="text-center py-4 text-muted">No hay colaboradores para los filtros seleccionados.</td>
</tr>
<?php else: ?>
<?php foreach ($colaboradores as $c): ?>
<tr>
<td><?= (int)$c['id']; ?></td>
<td><?= htmlspecialchars($c['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['dni'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['celular'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['correo'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><span class="badge bg-primary"><?= htmlspecialchars($c['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?= htmlspecialchars($c['cargo'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['fecha_ingreso'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><span class="badge bg-info text-dark"><?= htmlspecialchars($c['estado'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span></td>
<td style="max-width: 220px;">
<div style="white-space: pre-wrap;"><?= htmlspecialchars($c['observaciones'] ?? '', ENT_QUOTES, 'UTF-8'); ?></div>
</td>
<td>
<form method="POST" class="d-flex flex-column gap-1">
<input type="hidden" name="action" value="update_colaborador_estado">
<input type="hidden" name="id" value="<?= (int)$c['id']; ?>">
<select name="estado" class="form-select form-select-sm">
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= ($c['estado'] ?? '') === $e ? 'selected' : ''; ?>>
<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-success">
<i class="fas fa-save me-1"></i> Guardar
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include 'layout_footer.php'; ?>

331
hr_evaluaciones.php Normal file
View File

@ -0,0 +1,331 @@
<?php
session_start();
$userRole = $_SESSION['user_role'] ?? '';
if (!isset($_SESSION['user_id']) || ($userRole !== 'Administrador' && $userRole !== 'admin')) {
header('Location: dashboard.php');
exit;
}
require_once 'db/config.php';
$pdo = db();
$schemaInitError = null;
require_once 'includes/hr_bootstrap.php';
try {
hr_ensure_schema($pdo);
} catch (Throwable $e) {
$schemaInitError = $e->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';
?>
<div class="container-fluid mt-4">
<?php if ($notice): ?>
<div class="alert alert-<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8'); ?> mb-4" role="alert">
<?= $notice; ?>
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h2 class="h5 mb-0">Nueva Evaluación</h2>
</div>
<div class="card-body">
<?php if (empty($colaboradores)): ?>
<div class="alert alert-warning">
Primero agrega colaboradores en <b>Colaboradores</b>.
</div>
<?php else: ?>
<form method="POST">
<input type="hidden" name="action" value="add_evaluacion">
<div class="mb-3">
<label class="form-label">Colaborador</label>
<select name="colaborador_id" class="form-select" required>
<?php foreach ($colaboradores as $c): ?>
<option value="<?= (int)$c['id']; ?>">
<?= htmlspecialchars($c['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
(<?= htmlspecialchars($c['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Fecha</label>
<input type="date" name="fecha" class="form-control" value="<?= htmlspecialchars($fechaHoy, ENT_QUOTES, 'UTF-8'); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Tipo de Evaluación</label>
<input type="text" name="tipo_evaluacion" class="form-control" placeholder="Ej: Evaluación Mensual" required>
</div>
<div class="mb-3">
<label class="form-label">Puntaje</label>
<input type="number" name="puntaje" class="form-control" value="0" min="0" required>
</div>
<div class="mb-3">
<label class="form-label">Resultado</label>
<select name="resultado" class="form-select" required>
<?php foreach ($resultadosValidos as $r): ?>
<option value="<?= htmlspecialchars($r, ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($r, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Observaciones</label>
<textarea name="observaciones" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-save me-1"></i> Guardar
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">Evaluaciones</h2>
<span class="badge bg-secondary">Total: <?= count($evaluaciones); ?></span>
</div>
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-sm-5">
<label class="form-label">Colaborador</label>
<select name="colaborador_id" class="form-select">
<option value="0">Todos</option>
<?php foreach ($colaboradores as $c): ?>
<option value="<?= (int)$c['id']; ?>" <?= $f_colaborador === (int)$c['id'] ? 'selected' : ''; ?>>
<?= htmlspecialchars($c['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-3">
<label class="form-label">Resultado</label>
<select name="resultado" class="form-select">
<option value="Todos" <?= $f_resultado === 'Todos' ? 'selected' : ''; ?>>Todos</option>
<?php foreach ($resultadosValidos as $r): ?>
<option value="<?= htmlspecialchars($r, ENT_QUOTES, 'UTF-8'); ?>" <?= $f_resultado === $r ? 'selected' : ''; ?>>
<?= htmlspecialchars($r, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-2">
<label class="form-label">Desde</label>
<input type="date" name="fecha_desde" class="form-control" value="<?= htmlspecialchars($f_fecha_desde, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-sm-2">
<label class="form-label">Hasta</label>
<input type="date" name="fecha_hasta" class="form-control" value="<?= htmlspecialchars($f_fecha_hasta, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-12 d-flex justify-content-end gap-2 mt-2">
<a class="btn btn-outline-secondary" href="hr_evaluaciones.php">Limpiar</a>
<button class="btn btn-primary" type="submit">
<i class="fas fa-filter me-1"></i> Filtrar
</button>
</div>
</form>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Colaborador</th>
<th>Área</th>
<th>Fecha</th>
<th>Tipo</th>
<th>Puntaje</th>
<th>Resultado</th>
<th>Observaciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($evaluaciones)): ?>
<tr>
<td colspan="8" class="text-center py-4 text-muted">No hay evaluaciones para los filtros seleccionados.</td>
</tr>
<?php else: ?>
<?php foreach ($evaluaciones as $e): ?>
<tr>
<td><?= (int)$e['id']; ?></td>
<td>
<?= htmlspecialchars($e['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
<?php if (!empty($e['dni'])): ?>
<div class="text-muted small">DNI: <?= htmlspecialchars($e['dni'], ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
</td>
<td><span class="badge bg-primary"><?= htmlspecialchars($e['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?= htmlspecialchars($e['fecha'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($e['tipo_evaluacion'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><b><?= (int)($e['puntaje'] ?? 0); ?></b></td>
<td>
<span class="badge bg-info text-dark">
<?= htmlspecialchars($e['resultado'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</span>
</td>
<td style="max-width: 260px;">
<div style="white-space: pre-wrap;"><?= htmlspecialchars($e['observaciones'] ?? '', ENT_QUOTES, 'UTF-8'); ?></div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include 'layout_footer.php'; ?>

461
hr_manuales.php Normal file
View File

@ -0,0 +1,461 @@
<?php
session_start();
$userRole = $_SESSION['user_role'] ?? '';
if (!isset($_SESSION['user_id']) || ($userRole !== 'Administrador' && $userRole !== 'admin')) {
header('Location: dashboard.php');
exit;
}
require_once 'db/config.php';
$pdo = db();
$schemaInitError = null;
require_once 'includes/hr_bootstrap.php';
try {
hr_ensure_schema($pdo);
} catch (Throwable $e) {
$schemaInitError = $e->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';
?>
<div class="container-fluid mt-4">
<?php if ($notice): ?>
<div class="alert alert-<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8'); ?> mb-4" role="alert">
<?= $notice; ?>
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h2 class="h5 mb-0">Subir Manual</h2>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="add_manual">
<div class="mb-3">
<label class="form-label">Título</label>
<input type="text" name="titulo" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Categoría</label>
<select name="categoria" class="form-select" required>
<?php foreach ($categoriasValidas as $c): ?>
<option value="<?= htmlspecialchars($c, ENT_QUOTES, 'UTF-8'); ?>"><?= htmlspecialchars($c, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Área</label>
<select name="area" class="form-select" required>
<?php foreach ($areasValidas as $a): ?>
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?>" <?= $a === 'General' ? 'selected' : ''; ?>><?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Descripción</label>
<textarea name="descripcion" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Fecha de Creación</label>
<input type="date" name="fecha_creacion" class="form-control" value="<?= htmlspecialchars($fechaHoy, ENT_QUOTES, 'UTF-8'); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select" required>
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= $e === 'Activo' ? 'selected' : ''; ?>><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Subir PDF (Opcional)</label>
<input type="file" name="archivo_pdf" class="form-control" accept="application/pdf">
</div>
<div class="mb-3">
<label class="form-label">Subir Word (Opcional)</label>
<input type="file" name="archivo_word" class="form-control" accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-upload me-1"></i> Guardar
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">Manuales y Capacitación</h2>
<span class="badge bg-secondary">Total: <?= count($manuales); ?></span>
</div>
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-sm-5">
<label class="form-label">Buscar por nombre</label>
<input type="text" name="q" class="form-control" placeholder="Ej: Ventas" value="<?= htmlspecialchars($q, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-sm-3">
<label class="form-label">Categoría</label>
<select name="categoria" class="form-select">
<option value="Todas">Todas</option>
<?php foreach ($categoriasValidas as $c): ?>
<option value="<?= htmlspecialchars($c, ENT_QUOTES, 'UTF-8'); ?>" <?= $fCategoria === $c ? 'selected' : ''; ?>><?= htmlspecialchars($c, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-2">
<label class="form-label">Área</label>
<select name="area" class="form-select">
<option value="Todas">Todas</option>
<?php foreach ($areasValidas as $a): ?>
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?>" <?= $fArea === $a ? 'selected' : ''; ?>><?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-2">
<label class="form-label">Estado</label>
<select name="estado" class="form-select">
<option value="Activo" <?= $fEstado === 'Activo' ? 'selected' : ''; ?>>Activo</option>
<option value="Inactivo" <?= $fEstado === 'Inactivo' ? 'selected' : ''; ?>>Inactivo</option>
<option value="Todos" <?= $fEstado === 'Todos' ? 'selected' : ''; ?>>Todos</option>
</select>
</div>
<div class="col-12 d-flex justify-content-end gap-2 mt-2">
<a class="btn btn-outline-secondary" href="hr_manuales.php">Limpiar</a>
<button class="btn btn-primary" type="submit">
<i class="fas fa-filter me-1"></i> Filtrar
</button>
</div>
</form>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Título</th>
<th>Categoría</th>
<th>Área</th>
<th>Fecha</th>
<th>Estado</th>
<th>Archivo</th>
<th>Descripción</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($manuales)): ?>
<tr>
<td colspan="9" class="text-center py-4 text-muted">No hay manuales con los filtros seleccionados.</td>
</tr>
<?php else: ?>
<?php foreach ($manuales as $m): ?>
<tr>
<td><?= (int)$m['id']; ?></td>
<td><?= htmlspecialchars($m['titulo'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<span class="badge bg-primary"><?= htmlspecialchars($m['categoria'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td>
<span class="badge bg-info text-dark"><?= htmlspecialchars($m['area'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td><?= htmlspecialchars($m['fecha_creacion'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<span class="badge bg-<?= ($m['estado'] ?? '') === 'Activo' ? 'success' : 'secondary'; ?>">
<?= htmlspecialchars($m['estado'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
</span>
</td>
<td>
<div class="d-flex flex-column gap-1">
<?php if (!empty($m['archivo_pdf_path'])): ?>
<a href="<?= htmlspecialchars($m['archivo_pdf_path'], ENT_QUOTES, 'UTF-8'); ?>" download class="btn btn-sm btn-outline-primary">
<i class="fas fa-file-pdf me-1"></i> PDF
</a>
<?php endif; ?>
<?php if (!empty($m['archivo_word_path'])): ?>
<a href="<?= htmlspecialchars($m['archivo_word_path'], ENT_QUOTES, 'UTF-8'); ?>" download class="btn btn-sm btn-outline-success">
<i class="fas fa-file-word me-1"></i> Word
</a>
<?php endif; ?>
<?php if (empty($m['archivo_pdf_path']) && empty($m['archivo_word_path'])): ?>
<span class="text-muted"></span>
<?php endif; ?>
</div>
</td>
<td style="max-width: 220px;">
<?php
$descripcion = (string)($m['descripcion'] ?? '');
?>
<div style="white-space: pre-wrap;"><?= htmlspecialchars($descripcion, ENT_QUOTES, 'UTF-8'); ?></div>
</td>
<td>
<form method="POST" class="d-flex flex-column gap-1">
<input type="hidden" name="action" value="update_manual_estado">
<input type="hidden" name="id" value="<?= (int)$m['id']; ?>">
<select name="estado" class="form-select form-select-sm">
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= ($m['estado'] ?? '') === $e ? 'selected' : ''; ?>>
<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-success">
<i class="fas fa-save me-1"></i> Guardar
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include 'layout_footer.php'; ?>

604
hr_reclutamiento.php Normal file
View File

@ -0,0 +1,604 @@
<?php
session_start();
$userRole = $_SESSION['user_role'] ?? '';
if (!isset($_SESSION['user_id']) || ($userRole !== 'Administrador' && $userRole !== 'admin')) {
header('Location: dashboard.php');
exit;
}
require_once 'db/config.php';
$pdo = db();
$schemaInitError = null;
require_once 'includes/hr_bootstrap.php';
try {
hr_ensure_schema($pdo);
} catch (Throwable $e) {
$schemaInitError = $e->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';
?>
<div class="container-fluid mt-4">
<?php if ($notice): ?>
<div class="alert alert-<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8'); ?> mb-4" role="alert">
<?= $notice; ?>
</div>
<?php endif; ?>
<div class="row g-4">
<!-- NUEVO POSTULANTE (arriba, ancho completo) -->
<div class="col-12">
<div class="card shadow-sm mb-2">
<div class="card-header bg-light">
<h2 class="h5 mb-0">Nuevo Postulante</h2>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="add_reclutamiento">
<div class="row g-3">
<div class="col-lg-2 col-md-3">
<label class="form-label">Fecha de Registro</label>
<input type="date" name="fecha_registro" class="form-control" value="<?= htmlspecialchars($fechaHoy, ENT_QUOTES, 'UTF-8'); ?>" required>
</div>
<div class="col-lg-4 col-md-6">
<label class="form-label">Edad, Hijos/Estudia Trabaja</label>
<input type="text" name="edad_hijos_estudia_trabaja" class="form-control" placeholder="Ej: 28 años, 1 hijo, trabaja" >
</div>
<div class="col-lg-2 col-md-3">
<label class="form-label">OCUPACION</label>
<input type="text" name="organizacion_personal" class="form-control" maxlength="50" placeholder="Ej: CARRERA/ESTUDIANTE" >
</div>
<div class="col-lg-2 col-md-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select" required>
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= $e === 'Postulación Recibida' ? 'selected' : ''; ?>>
<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-lg-2 col-md-4">
<label class="form-label">Claridad en Expresarse</label>
<input type="text" name="claridad_expresarse" class="form-control" maxlength="50" placeholder="Ej: A/B/C" >
</div>
<div class="col-lg-2 col-md-4">
<label class="form-label">Manejo de Objeciones</label>
<input type="text" name="manejo_objeciones" class="form-control" maxlength="50" placeholder="Ej: A/B/C" >
</div>
<div class="col-lg-2 col-md-4">
<label class="form-label">EXPERIENCIA LABORAL</label>
<input type="text" name="experiencia_otros_trabajos" class="form-control" maxlength="50" placeholder="Ej: VENTAS" >
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label">RESPONSABILIDAD/COMPROMISO</label>
<input type="text" name="experiencia_relevante_ventas" class="form-control" maxlength="50" placeholder="Ej: ORGANIZACION" >
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label">DISPONIBILIDAD INMEDIATA</label>
<input type="text" name="empatia_trato" class="form-control" maxlength="50" placeholder="Ej: EQUIPOS/INMEDIATES" >
</div>
</div>
<hr class="my-4">
<div class="row g-3">
<div class="col-lg-4 col-md-6">
<label class="form-label">Nombre Completo</label>
<input type="text" name="nombre_completo" class="form-control" required>
</div>
<div class="col-lg-2 col-md-3">
<label class="form-label">DNI (Opcional)</label>
<input type="text" name="dni" class="form-control" maxlength="20" placeholder="Opcional">
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label">Celular</label>
<input type="text" name="celular" class="form-control" required>
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label">Correo (Opcional)</label>
<input type="email" name="correo" class="form-control" maxlength="255" placeholder="tu@email.com">
</div>
</div>
<div class="row g-3">
<div class="col-lg-4 col-md-6">
<label class="form-label">Ciudad</label>
<input type="text" name="ciudad" class="form-control" required>
</div>
<div class="col-lg-4 col-md-6">
<label class="form-label">Puesto Postulado</label>
<input type="text" name="puesto_postulado" class="form-control" required>
</div>
<div class="col-lg-4 col-md-12">
<label class="form-label">CV (PDF)</label>
<input type="file" name="cv_pdf" class="form-control" accept="application/pdf" required>
<div class="form-text">Sube el PDF del CV.</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6 col-md-12">
<label class="form-label">Portafolio (Opcional)</label>
<input type="file" name="portafolio" class="form-control" accept="application/pdf,.doc,.docx">
<div class="form-text">PDF o documento Word (si aplica).</div>
</div>
<div class="col-lg-6 col-md-12">
<label class="form-label">Observaciones</label>
<textarea name="observaciones" class="form-control" rows="3"></textarea>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 mt-3">
<i class="fas fa-save me-1"></i> Guardar
</button>
</form>
</div>
</div>
</div>
<!-- FILTROS + TABLA (ancho completo) -->
<div class="col-12">
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">Postulantes</h2>
<span class="badge bg-secondary">Total: <?= count($candidatos); ?></span>
</div>
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-sm-5">
<label class="form-label">Puesto</label>
<input type="text" name="puesto" class="form-control" placeholder="Ej: Ventas" value="<?= htmlspecialchars($filter_puesto, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-sm-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select">
<option value="Todos">Todos</option>
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= $filter_estado === $e ? 'selected' : ''; ?>>
<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-2">
<label class="form-label">Desde</label>
<input type="date" name="fecha_desde" class="form-control" value="<?= htmlspecialchars($fecha_desde, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-sm-2">
<label class="form-label">Hasta</label>
<input type="date" name="fecha_hasta" class="form-control" value="<?= htmlspecialchars($fecha_hasta, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-12 d-flex justify-content-end gap-2 mt-2">
<a class="btn btn-outline-secondary" href="hr_reclutamiento.php">Limpiar</a>
<button class="btn btn-primary" type="submit">
<i class="fas fa-filter me-1"></i> Filtrar
</button>
</div>
</form>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Fecha Registro</th>
<th>Edad, Hijos/Estudia Trabaja</th>
<th>OCUPACION</th>
<th>Claridad en Expresarse</th>
<th>Manejo de Objeciones</th>
<th>EXPERIENCIA LABORAL</th>
<th>RESPONSABILIDAD/COMPROMISO</th>
<th>DISPONIBILIDAD INMEDIATA</th>
<th>Nombre Completo</th>
<th>DNI</th>
<th>Celular</th>
<th>Correo</th>
<th>Ciudad</th>
<th>Puesto</th>
<th>Estado</th>
<th>CV</th>
<th>Portafolio</th>
<th>Observaciones</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($candidatos)): ?>
<tr>
<td colspan="20" class="text-center py-4 text-muted">No hay postulantes para los filtros seleccionados.</td>
</tr>
<?php else: ?>
<?php foreach ($candidatos as $c): ?>
<tr>
<td><?= (int)$c['id']; ?></td>
<td><?= htmlspecialchars($c['fecha_registro'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php $edad = $c['edad_hijos_estudia_trabaja'] ?? null; ?>
<?php if ($edad === null || $edad === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<div style="white-space: pre-wrap; word-break: break-word; max-width: 240px;"><?= htmlspecialchars($edad, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
</td>
<td>
<?php $v = $c['organizacion_personal'] ?? null; ?>
<?php if ($v === null || $v === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<span class="badge bg-primary text-white"><?= htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<?php $v = $c['claridad_expresarse'] ?? null; ?>
<?php if ($v === null || $v === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<span class="badge bg-primary text-white"><?= htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<?php $v = $c['manejo_objeciones'] ?? null; ?>
<?php if ($v === null || $v === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<span class="badge bg-primary text-white"><?= htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<?php $v = $c['experiencia_otros_trabajos'] ?? null; ?>
<?php if ($v === null || $v === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<span class="badge bg-primary text-white"><?= htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<?php $v = $c['experiencia_relevante_ventas'] ?? null; ?>
<?php if ($v === null || $v === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<span class="badge bg-primary text-white"><?= htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<?php $v = $c['empatia_trato'] ?? null; ?>
<?php if ($v === null || $v === ''): ?>
<span class="text-muted"></span>
<?php else: ?>
<span class="badge bg-primary text-white"><?= htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($c['nombre_completo'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['dni'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['celular'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['correo'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['ciudad'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($c['puesto_postulado'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<span class="badge bg-info text-dark"><?= htmlspecialchars($c['estado'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td>
<?php if (!empty($c['cv_pdf_path'])): ?>
<a href="<?= htmlspecialchars($c['cv_pdf_path'], ENT_QUOTES, 'UTF-8'); ?>" download class="btn btn-sm btn-outline-primary">
<i class="fas fa-file-pdf"></i>
</a>
<?php else: ?>
<span class="text-muted"></span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($c['portafolio_path'])): ?>
<a href="<?= htmlspecialchars($c['portafolio_path'], ENT_QUOTES, 'UTF-8'); ?>" download class="btn btn-sm btn-outline-secondary">
<i class="fas fa-file"></i>
</a>
<?php else: ?>
<span class="text-muted"></span>
<?php endif; ?>
</td>
<td style="max-width: 220px;">
<div style="white-space: pre-wrap;"><?= htmlspecialchars($c['observaciones'] ?? '', ENT_QUOTES, 'UTF-8'); ?></div>
</td>
<td>
<form method="POST" class="d-flex flex-column gap-1">
<input type="hidden" name="action" value="update_reclutamiento_estado">
<input type="hidden" name="id" value="<?= (int)$c['id']; ?>">
<select name="estado" class="form-select form-select-sm">
<?php foreach ($estadosValidos as $e): ?>
<option value="<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>" <?= ($c['estado'] ?? '') === $e ? 'selected' : ''; ?>>
<?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-success">
<i class="fas fa-save me-1"></i> Guardar
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include 'layout_footer.php'; ?>

145
includes/hr_bootstrap.php Normal file
View File

@ -0,0 +1,145 @@
<?php
// Auto-inicializa el esquema de "Recursos Humanos" si faltan tablas o columnas.
// Esto evita que el usuario vea errores 500 cuando la base de datos del entorno
// no tiene ejecutadas las migraciones HR.
function hr_ensure_schema(PDO $pdo): void
{
static $attempted = false;
if ($attempted) {
return;
}
$attempted = true;
$requiredTables = [
'hr_reclutamiento',
'hr_colaboradores',
'hr_manuales',
'hr_evaluaciones',
];
// 1) Asegurar tablas requeridas
$missingTables = [];
$checkTableStmt = $pdo->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);
}
}

View File

@ -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',