Autosave: 20260624-214502
This commit is contained in:
parent
7da0c2905a
commit
e282778646
26
db/migrations/080_create_hr_reclutamiento_table.sql
Normal file
26
db/migrations/080_create_hr_reclutamiento_table.sql
Normal 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;
|
||||
24
db/migrations/081_create_hr_colaboradores_table.sql
Normal file
24
db/migrations/081_create_hr_colaboradores_table.sql
Normal 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;
|
||||
29
db/migrations/082_create_hr_manuales_table.sql
Normal file
29
db/migrations/082_create_hr_manuales_table.sql
Normal 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;
|
||||
18
db/migrations/083_create_hr_evaluaciones_table.sql
Normal file
18
db/migrations/083_create_hr_evaluaciones_table.sql
Normal 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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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
356
hr_colaboradores.php
Normal 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
331
hr_evaluaciones.php
Normal 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
461
hr_manuales.php
Normal 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
604
hr_reclutamiento.php
Normal 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
145
includes/hr_bootstrap.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user