34849-vm/hr_manuales.php
2026-06-24 21:44:23 +00:00

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'; ?>