383 lines
17 KiB
PHP
383 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* detalle.php — Vista detalle de un oficio con historial, adjuntos y comentarios
|
|
*/
|
|
require_once __DIR__ . '/../../config/config.php';
|
|
require_once __DIR__ . '/../../controllers/AuthController.php';
|
|
require_once __DIR__ . '/../../models/Oficio.php';
|
|
require_once __DIR__ . '/../../models/Usuario.php';
|
|
|
|
AuthController::requerirAuth();
|
|
|
|
$id = (int)($_GET['id'] ?? 0);
|
|
if (!$id) redirect(APP_URL . '/views/oficios/lista.php');
|
|
|
|
$model = new OficioModel();
|
|
$oficio = $model->buscarPorId($id);
|
|
|
|
if (!$oficio) {
|
|
http_response_code(404);
|
|
redirect(APP_URL . '/views/oficios/lista.php?error='.urlencode('Oficio no encontrado.'));
|
|
}
|
|
|
|
// Marcar notificación si viene de ella
|
|
if (isset($_GET['notif'])) {
|
|
$db = getDB();
|
|
$db->prepare("UPDATE notificaciones SET leida=1,leida_at=NOW() WHERE id=? AND usuario_id=?")
|
|
->execute([(int)$_GET['notif'], $_SESSION['usuario_id']]);
|
|
}
|
|
|
|
$historial = $model->historial($id);
|
|
$comentarios = $model->comentarios($id, !AuthController::esAdmin());
|
|
$etiquetas = $model->etiquetasDeOficio($id);
|
|
$adjuntos = getDB()->prepare("SELECT * FROM documentos_adjuntos WHERE oficio_id=? ORDER BY created_at DESC");
|
|
$adjuntos->execute([$id]);
|
|
$adjuntos = $adjuntos->fetchAll();
|
|
|
|
$usuarios = (new UsuarioModel())->usuariosParaSelector();
|
|
$esAdmin = AuthController::esAdmin();
|
|
$userId = $_SESSION['usuario_id'];
|
|
|
|
// Calcular semáforo
|
|
$semaforo = $oficio['semaforo'] ?? 'vigente';
|
|
$semaforoColors = ['vigente'=>'success','proximo'=>'warning','vencido'=>'danger','completado'=>'info'];
|
|
$semaColor = $semaforoColors[$semaforo] ?? 'secondary';
|
|
|
|
$pageTitle = 'Oficio: '.$oficio['numero_oficio'];
|
|
$activeNav = $oficio['tipo'] === 'recibido' ? 'entrada' : 'salida';
|
|
|
|
include __DIR__ . '/../../views/layout/header.php';
|
|
include __DIR__ . '/../../views/layout/sidebar.php';
|
|
include __DIR__ . '/../../views/layout/topbar.php';
|
|
?>
|
|
<div class="page-content">
|
|
|
|
<div class="breadcrumb">
|
|
<a href="<?= APP_URL ?>/dashboard.php"><i class="fa-solid fa-house"></i></a>
|
|
<i class="fa-solid fa-chevron-right sep"></i>
|
|
<a href="<?= APP_URL ?>/views/oficios/lista.php?tipo=<?= $oficio['tipo'] ?>">
|
|
<?= $oficio['tipo']==='recibido' ? 'Entrada' : 'Salida' ?>
|
|
</a>
|
|
<i class="fa-solid fa-chevron-right sep"></i>
|
|
<span><?= htmlspecialchars($oficio['numero_oficio']) ?></span>
|
|
</div>
|
|
|
|
<!-- Flash messages -->
|
|
<?php
|
|
$success = $_GET['success'] ?? null;
|
|
$error = $_GET['error'] ?? null;
|
|
?>
|
|
<?php if ($success): ?>
|
|
<div class="alert alert-success"><i class="fa-solid fa-circle-check"></i> <?= htmlspecialchars($success) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger"><i class="fa-solid fa-circle-exclamation"></i> <?= htmlspecialchars($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Encabezado del oficio -->
|
|
<div class="page-header">
|
|
<div class="page-header-content">
|
|
<div class="d-flex align-items-center gap-3 flex-wrap">
|
|
<h1><?= htmlspecialchars($oficio['numero_oficio']) ?></h1>
|
|
<span class="badge badge-<?= $semaColor ?> semaforo-<?= $semaforo ?>" style="font-size:.85rem;padding:.3rem .8rem">
|
|
<?= ucfirst($semaforo) ?>
|
|
</span>
|
|
<?php if ($oficio['es_confidencial']): ?>
|
|
<span class="badge badge-warning"><i class="fa-solid fa-lock"></i> Confidencial</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<p style="margin-top:.4rem;font-size:.9rem;color:var(--text-muted)">
|
|
<?= htmlspecialchars($oficio['asunto']) ?>
|
|
</p>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<a href="<?= APP_URL ?>/views/oficios/editar.php?id=<?= $id ?>" class="btn btn-warning">
|
|
<i class="fa-solid fa-pen"></i> Editar
|
|
</a>
|
|
<a href="<?= APP_URL ?>/controllers/OficioController.php?action=pdf&id=<?= $id ?>" class="btn btn-primary" target="_blank">
|
|
<i class="fa-solid fa-file-pdf"></i> PDF
|
|
</a>
|
|
<button class="btn btn-secondary" data-modal="modalDerivar">
|
|
<i class="fa-solid fa-reply-all"></i> Derivar
|
|
</button>
|
|
<a href="<?= APP_URL ?>/controllers/OficioController.php?action=eliminar&id=<?= $id ?>"
|
|
class="btn btn-danger"
|
|
data-confirm="¿Mover este oficio a la papelera?">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2" style="grid-template-columns:1.4fr 1fr;gap:1.5rem;align-items:start">
|
|
|
|
<!-- Columna izquierda: Datos + Adjuntos -->
|
|
<div>
|
|
|
|
<!-- Datos principales -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<i class="fa-solid fa-file-lines text-primary"></i>
|
|
<span class="card-title">Datos del Oficio</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="grid-2" style="gap:1rem;margin-bottom:1rem">
|
|
<div>
|
|
<div class="fs-sm text-muted fw-600">TIPO</div>
|
|
<div style="margin-top:.2rem">
|
|
<?= $oficio['tipo']==='recibido'
|
|
? '<span class="badge badge-info"><i class="fa-solid fa-inbox"></i> Recibido</span>'
|
|
: '<span class="badge badge-secondary"><i class="fa-solid fa-paper-plane"></i> Enviado</span>' ?>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="fs-sm text-muted fw-600">PRIORIDAD</div>
|
|
<div style="margin-top:.2rem">
|
|
<span class="badge badge-<?= ['alta'=>'danger','media'=>'warning','baja'=>'success'][$oficio['prioridad']]??'secondary' ?>">
|
|
<?= ucfirst($oficio['prioridad']) ?>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="fs-sm text-muted fw-600">ESTADO</div>
|
|
<div style="margin-top:.2rem">
|
|
<span class="badge badge-<?= ['recibido'=>'primary','en_proceso'=>'warning','respondido'=>'success','vencido'=>'danger','archivado'=>'secondary'][$oficio['estado']]??'secondary' ?>">
|
|
<?= ucfirst(str_replace('_',' ',$oficio['estado'])) ?>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="fs-sm text-muted fw-600">RESPONSABLE</div>
|
|
<div style="margin-top:.2rem;font-weight:600"><?= htmlspecialchars($oficio['responsable_nombre'] ?? '—') ?></div>
|
|
</div>
|
|
<div>
|
|
<div class="fs-sm text-muted fw-600">FECHA RECEPCIÓN</div>
|
|
<div style="margin-top:.2rem"><?= date('d/m/Y', strtotime($oficio['fecha_recepcion'])) ?></div>
|
|
</div>
|
|
<div>
|
|
<div class="fs-sm text-muted fw-600">FECHA VENCIMIENTO</div>
|
|
<div style="margin-top:.2rem">
|
|
<?php if ($oficio['fecha_vencimiento']): ?>
|
|
<strong class="<?= $semaforo==='vencido'?'text-danger':($semaforo==='proximo'?'text-warning':'') ?>">
|
|
<?= date('d/m/Y', strtotime($oficio['fecha_vencimiento'])) ?>
|
|
</strong>
|
|
<?php if ($oficio['dias_para_vencer'] !== null): ?>
|
|
<div class="fs-sm text-muted">
|
|
<?= $oficio['dias_para_vencer'] < 0
|
|
? abs($oficio['dias_para_vencer']).' día(s) vencido'
|
|
: ($oficio['dias_para_vencer'] === '0' ? 'Vence HOY' : $oficio['dias_para_vencer'].' día(s) restante(s)') ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php else: ?>—<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:1rem">
|
|
<div class="fs-sm text-muted fw-600">REMITENTE</div>
|
|
<div style="margin-top:.2rem"><?= htmlspecialchars($oficio['remitente']) ?></div>
|
|
</div>
|
|
<div style="margin-bottom:1rem">
|
|
<div class="fs-sm text-muted fw-600">DESTINATARIO</div>
|
|
<div style="margin-top:.2rem"><?= htmlspecialchars($oficio['destinatario']) ?></div>
|
|
</div>
|
|
<div style="margin-bottom:1rem">
|
|
<div class="fs-sm text-muted fw-600">ASUNTO</div>
|
|
<div style="margin-top:.2rem"><?= htmlspecialchars($oficio['asunto']) ?></div>
|
|
</div>
|
|
<?php if ($oficio['descripcion']): ?>
|
|
<div style="margin-bottom:.5rem">
|
|
<div class="fs-sm text-muted fw-600">DESCRIPCIÓN</div>
|
|
<div style="margin-top:.2rem;font-size:.85rem;line-height:1.7"><?= nl2br(htmlspecialchars($oficio['descripcion'])) ?></div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($etiquetas): ?>
|
|
<div style="margin-top:1rem">
|
|
<div class="fs-sm text-muted fw-600 mb-2">ETIQUETAS</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<?php foreach ($etiquetas as $et): ?>
|
|
<span class="badge" style="background:<?= $et['color'] ?>25;color:<?= $et['color'] ?>">
|
|
<i class="fa-solid <?= $et['icono'] ?>"></i>
|
|
<?= htmlspecialchars($et['nombre']) ?>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documentos adjuntos -->
|
|
<?php if (!empty($adjuntos)): ?>
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<i class="fa-solid fa-paperclip text-primary"></i>
|
|
<span class="card-title">Documentos Adjuntos (<?= count($adjuntos) ?>)</span>
|
|
</div>
|
|
<div class="card-body" style="display:flex;flex-wrap:wrap;gap:.75rem">
|
|
<?php foreach ($adjuntos as $adj):
|
|
$ext = strtolower(pathinfo($adj['nombre_original'], PATHINFO_EXTENSION));
|
|
$iconos = ['pdf'=>'fa-file-pdf','doc'=>'fa-file-word','docx'=>'fa-file-word','xls'=>'fa-file-excel','xlsx'=>'fa-file-excel','jpg'=>'fa-file-image','jpeg'=>'fa-file-image','png'=>'fa-file-image'];
|
|
$iconColor = ['pdf'=>'#ef4444','doc'=>'#3b82f6','docx'=>'#3b82f6','xls'=>'#10b981','xlsx'=>'#10b981','jpg'=>'#f59e0b','jpeg'=>'#f59e0b','png'=>'#f59e0b'];
|
|
?>
|
|
<a href="<?= APP_URL ?>/uploads/<?= $adj['ruta'] ?>"
|
|
target="_blank"
|
|
style="display:flex;align-items:center;gap:.5rem;padding:.5rem .875rem;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);text-decoration:none;color:var(--text);font-size:.8rem;transition:all .2s"
|
|
onmouseover="this.style.borderColor='var(--primary)'"
|
|
onmouseout="this.style.borderColor='var(--border)'">
|
|
<i class="fa-solid <?= $iconos[$ext]??'fa-file' ?>" style="color:<?= $iconColor[$ext]??'var(--primary)' ?>;font-size:1.1rem"></i>
|
|
<div>
|
|
<div style="font-weight:600;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
|
<?= htmlspecialchars($adj['nombre_original']) ?>
|
|
</div>
|
|
<div style="color:var(--text-muted);font-size:.7rem">
|
|
<?= number_format($adj['tamanio']/1024, 1) ?> KB
|
|
</div>
|
|
</div>
|
|
<i class="fa-solid fa-download" style="color:var(--text-muted);margin-left:.25rem"></i>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
|
|
<!-- Columna derecha: Comentarios + Historial -->
|
|
<div>
|
|
|
|
<!-- Tabs -->
|
|
<div data-tabs>
|
|
<div class="tabs">
|
|
<button class="tab-btn active" data-tab="tabComentarios">
|
|
<i class="fa-solid fa-comments"></i> Comentarios (<?= count($comentarios) ?>)
|
|
</button>
|
|
<button class="tab-btn" data-tab="tabHistorial">
|
|
<i class="fa-solid fa-clock-rotate-left"></i> Historial (<?= count($historial) ?>)
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab comentarios -->
|
|
<div id="tabComentarios" class="tab-content active">
|
|
<!-- Nuevo comentario -->
|
|
<form method="POST" action="<?= APP_URL ?>/controllers/OficioController.php?action=comentar" class="card mb-3">
|
|
<?= csrfField() ?>
|
|
<input type="hidden" name="oficio_id" value="<?= $id ?>">
|
|
<div class="card-body">
|
|
<textarea name="comentario" class="form-control" rows="3" placeholder="Escribe un comentario…" required></textarea>
|
|
<?php if ($esAdmin): ?>
|
|
<label style="display:flex;align-items:center;gap:.4rem;margin-top:.5rem;font-size:.8rem;cursor:pointer">
|
|
<input type="checkbox" name="es_privado" value="1">
|
|
<i class="fa-solid fa-eye-slash text-warning"></i> Comentario privado (solo admins)
|
|
</label>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="card-footer">
|
|
<button type="submit" class="btn btn-primary btn-sm">
|
|
<i class="fa-solid fa-paper-plane"></i> Comentar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Lista de comentarios -->
|
|
<div class="comment-thread">
|
|
<?php if (empty($comentarios)): ?>
|
|
<div style="text-align:center;padding:1.5rem;color:var(--text-muted);font-size:.83rem">
|
|
<i class="fa-solid fa-comment-slash" style="font-size:1.5rem;display:block;margin-bottom:.5rem;opacity:.3"></i>
|
|
Sin comentarios aún
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($comentarios as $c):
|
|
$iniciales = implode('', array_map(fn($p)=>strtoupper($p[0]), array_slice(explode(' ', $c['autor_nombre']), 0, 2)));
|
|
?>
|
|
<div class="comment-item">
|
|
<div class="comment-avatar"><?= $iniciales ?></div>
|
|
<div class="comment-bubble" style="<?= $c['es_privado'] ? 'border-color:var(--warning);background:rgba(245,158,11,.05)' : '' ?>">
|
|
<div class="comment-meta">
|
|
<strong><?= htmlspecialchars($c['autor_nombre']) ?></strong>
|
|
· <?= date('d/m/Y H:i', strtotime($c['created_at'])) ?>
|
|
<?php if ($c['es_privado']): ?>
|
|
<span class="badge badge-warning" style="font-size:.65rem;margin-left:.3rem"><i class="fa-solid fa-eye-slash"></i> Privado</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="comment-text"><?= nl2br(htmlspecialchars($c['comentario'])) ?></div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab historial -->
|
|
<div id="tabHistorial" class="tab-content">
|
|
<div class="timeline">
|
|
<?php if (empty($historial)): ?>
|
|
<p class="text-muted fs-sm">Sin historial registrado.</p>
|
|
<?php else: ?>
|
|
<?php foreach ($historial as $h): ?>
|
|
<div class="timeline-item">
|
|
<div class="timeline-date">
|
|
<?= date('d/m/Y H:i', strtotime($h['created_at'])) ?>
|
|
</div>
|
|
<div class="timeline-body">
|
|
<span class="timeline-author"><?= htmlspecialchars($h['usuario_nombre'] ?? 'Sistema') ?></span>
|
|
· <span class="badge badge-<?= ['crear'=>'success','editar'=>'info','eliminar'=>'danger','restaurar'=>'warning','derivar'=>'primary','escalacion'=>'secondary'][$h['accion']]??'secondary' ?>">
|
|
<?= ucfirst($h['accion']) ?>
|
|
</span>
|
|
<?php if ($h['detalle']): ?>
|
|
<div style="margin-top:.2rem;font-size:.78rem;color:var(--text-muted)">
|
|
<?= htmlspecialchars($h['detalle']) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div><!-- /.page-content -->
|
|
|
|
<!-- Modal: Derivar oficio -->
|
|
<div class="modal-overlay" id="modalDerivar">
|
|
<div class="modal-box">
|
|
<div class="modal-header">
|
|
<i class="fa-solid fa-reply-all text-primary"></i>
|
|
<span class="modal-title">Derivar Oficio</span>
|
|
<button class="btn-icon" data-modal-close><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
<form method="POST" action="<?= APP_URL ?>/controllers/OficioController.php?action=derivar">
|
|
<?= csrfField() ?>
|
|
<input type="hidden" name="id" value="<?= $id ?>">
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Nuevo Responsable <span class="required">*</span></label>
|
|
<select class="form-control" name="nuevo_responsable" required>
|
|
<option value="">— Seleccionar usuario —</option>
|
|
<?php foreach ($usuarios as $u): ?>
|
|
<option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['nombre_completo']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Comentario de Derivación <span class="required">*</span></label>
|
|
<textarea class="form-control" name="comentario_derivacion" rows="3"
|
|
placeholder="Explica el motivo de la derivación…" required></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-modal-close>Cancelar</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fa-solid fa-reply-all"></i> Derivar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<?php include __DIR__ . '/../../views/layout/footer.php'; ?>
|