462 lines
21 KiB
PHP
462 lines
21 KiB
PHP
<?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'; ?>
|