370 lines
18 KiB
PHP
370 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Oficio.php — Modelo de oficios
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/../config/config.php';
|
|
|
|
class OficioModel {
|
|
|
|
private PDO $db;
|
|
|
|
public function __construct() {
|
|
$this->db = getDB();
|
|
}
|
|
|
|
// ── Generar número de oficio ──────────────────────────────────────────────
|
|
public function generarNumero(string $tipo = 'REC'): string {
|
|
$anio = date('Y');
|
|
$prefijo = strtoupper($tipo === 'enviado' ? 'ENV' : 'REC');
|
|
$stmt = $this->db->prepare(
|
|
"SELECT numero_oficio FROM oficios WHERE numero_oficio LIKE ? ORDER BY id DESC LIMIT 1"
|
|
);
|
|
$stmt->execute(["$prefijo-$anio-%"]);
|
|
$ultimo = $stmt->fetchColumn();
|
|
$secuencia = 1;
|
|
if ($ultimo) {
|
|
$partes = explode('-', $ultimo);
|
|
$secuencia = (int)end($partes) + 1;
|
|
}
|
|
return sprintf('%s-%s-%04d', $prefijo, $anio, $secuencia);
|
|
}
|
|
|
|
// ── Listar oficios con filtros ────────────────────────────────────────────
|
|
public function listar(array $filtros = [], bool $soloPropio = false, int $usuarioId = 0): array {
|
|
$sql = "SELECT * FROM v_oficios_completo WHERE 1=1";
|
|
$params = [];
|
|
|
|
if ($soloPropio && $usuarioId) {
|
|
$sql .= " AND responsable_id = ?"; $params[] = $usuarioId;
|
|
}
|
|
if (!empty($filtros['tipo'])) {
|
|
$sql .= " AND tipo = ?"; $params[] = $filtros['tipo'];
|
|
}
|
|
if (!empty($filtros['estado'])) {
|
|
$sql .= " AND estado = ?"; $params[] = $filtros['estado'];
|
|
}
|
|
if (!empty($filtros['prioridad'])) {
|
|
$sql .= " AND prioridad = ?"; $params[] = $filtros['prioridad'];
|
|
}
|
|
if (!empty($filtros['responsable_id'])) {
|
|
$sql .= " AND responsable_id = ?"; $params[] = $filtros['responsable_id'];
|
|
}
|
|
if (!empty($filtros['fecha_desde'])) {
|
|
$sql .= " AND fecha_recepcion >= ?"; $params[] = $filtros['fecha_desde'];
|
|
}
|
|
if (!empty($filtros['fecha_hasta'])) {
|
|
$sql .= " AND fecha_recepcion <= ?"; $params[] = $filtros['fecha_hasta'];
|
|
}
|
|
if (!empty($filtros['semaforo'])) {
|
|
$sql .= " AND semaforo = ?"; $params[] = $filtros['semaforo'];
|
|
}
|
|
if (!empty($filtros['etiqueta_id'])) {
|
|
$sql .= " AND id IN (SELECT oficio_id FROM oficio_etiquetas WHERE etiqueta_id = ?)";
|
|
$params[] = $filtros['etiqueta_id'];
|
|
}
|
|
if (!empty($filtros['busqueda'])) {
|
|
$sql .= " AND MATCH(numero_oficio, remitente, destinatario, asunto) AGAINST(? IN BOOLEAN MODE)";
|
|
$params[] = $filtros['busqueda'] . '*';
|
|
}
|
|
|
|
$sql .= " ORDER BY fecha_vencimiento ASC, prioridad DESC";
|
|
$stmt = $this->db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
// ── Papelera de reciclaje ─────────────────────────────────────────────────
|
|
public function papelera(): array {
|
|
$stmt = $this->db->query(
|
|
"SELECT o.*, CONCAT(u.nombre,' ',u.apellido) AS responsable_nombre
|
|
FROM oficios o LEFT JOIN usuarios u ON u.id = o.responsable_id
|
|
WHERE o.deleted_at IS NOT NULL ORDER BY o.deleted_at DESC"
|
|
);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
// ── Buscar por ID ─────────────────────────────────────────────────────────
|
|
public function buscarPorId(int $id): ?array {
|
|
$stmt = $this->db->prepare("SELECT * FROM v_oficios_completo WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
return $stmt->fetch() ?: null;
|
|
}
|
|
|
|
// ── Verificar duplicado ───────────────────────────────────────────────────
|
|
public function existeNumero(string $numero, ?int $excepto = null): bool {
|
|
$sql = "SELECT id FROM oficios WHERE numero_oficio = ? AND deleted_at IS NULL";
|
|
$params = [$numero];
|
|
if ($excepto) { $sql .= " AND id != ?"; $params[] = $excepto; }
|
|
$stmt = $this->db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return (bool)$stmt->fetchColumn();
|
|
}
|
|
|
|
// ── Crear ─────────────────────────────────────────────────────────────────
|
|
public function crear(array $datos, int $creadoPor): int {
|
|
$stmt = $this->db->prepare(
|
|
"INSERT INTO oficios
|
|
(numero_oficio, tipo, remitente, destinatario, asunto, descripcion,
|
|
fecha_recepcion, fecha_vencimiento, prioridad, estado,
|
|
responsable_id, creado_por, es_confidencial)
|
|
VALUES
|
|
(:numero_oficio, :tipo, :remitente, :destinatario, :asunto, :descripcion,
|
|
:fecha_recepcion, :fecha_vencimiento, :prioridad, :estado,
|
|
:responsable_id, :creado_por, :es_confidencial)"
|
|
);
|
|
$stmt->execute([
|
|
':numero_oficio' => $datos['numero_oficio'],
|
|
':tipo' => $datos['tipo'],
|
|
':remitente' => $datos['remitente'],
|
|
':destinatario' => $datos['destinatario'],
|
|
':asunto' => $datos['asunto'],
|
|
':descripcion' => $datos['descripcion'] ?? null,
|
|
':fecha_recepcion' => $datos['fecha_recepcion'],
|
|
':fecha_vencimiento'=> $datos['fecha_vencimiento'] ?: null,
|
|
':prioridad' => $datos['prioridad'],
|
|
':estado' => $datos['estado'] ?? 'recibido',
|
|
':responsable_id' => $datos['responsable_id'] ?: null,
|
|
':creado_por' => $creadoPor,
|
|
':es_confidencial' => $datos['es_confidencial'] ?? 0,
|
|
]);
|
|
$id = (int)$this->db->lastInsertId();
|
|
|
|
// Guardar etiquetas
|
|
if (!empty($datos['etiquetas'])) {
|
|
$this->sincronizarEtiquetas($id, $datos['etiquetas']);
|
|
}
|
|
|
|
$this->registrarHistorial($id, 'crear', $creadoPor, 'Oficio creado.');
|
|
return $id;
|
|
}
|
|
|
|
// ── Actualizar ────────────────────────────────────────────────────────────
|
|
public function actualizar(int $id, array $datos, int $usuarioId): bool {
|
|
// Capturar valor anterior para auditoría
|
|
$anterior = $this->buscarPorId($id);
|
|
|
|
$stmt = $this->db->prepare(
|
|
"UPDATE oficios SET
|
|
numero_oficio=:numero_oficio, tipo=:tipo, remitente=:remitente,
|
|
destinatario=:destinatario, asunto=:asunto, descripcion=:descripcion,
|
|
fecha_recepcion=:fecha_recepcion, fecha_vencimiento=:fecha_vencimiento,
|
|
prioridad=:prioridad, estado=:estado, responsable_id=:responsable_id,
|
|
es_confidencial=:es_confidencial
|
|
WHERE id=:id AND deleted_at IS NULL"
|
|
);
|
|
$ok = $stmt->execute([
|
|
':numero_oficio' => $datos['numero_oficio'],
|
|
':tipo' => $datos['tipo'],
|
|
':remitente' => $datos['remitente'],
|
|
':destinatario' => $datos['destinatario'],
|
|
':asunto' => $datos['asunto'],
|
|
':descripcion' => $datos['descripcion'] ?? null,
|
|
':fecha_recepcion' => $datos['fecha_recepcion'],
|
|
':fecha_vencimiento'=> $datos['fecha_vencimiento'] ?: null,
|
|
':prioridad' => $datos['prioridad'],
|
|
':estado' => $datos['estado'],
|
|
':responsable_id' => $datos['responsable_id'] ?: null,
|
|
':es_confidencial' => $datos['es_confidencial'] ?? 0,
|
|
':id' => $id,
|
|
]);
|
|
|
|
if (!empty($datos['etiquetas'])) {
|
|
$this->sincronizarEtiquetas($id, $datos['etiquetas']);
|
|
}
|
|
|
|
if ($ok && $anterior) {
|
|
$cambios = [];
|
|
$campos = ['estado', 'prioridad', 'responsable_id', 'fecha_vencimiento', 'asunto'];
|
|
foreach ($campos as $campo) {
|
|
if (($anterior[$campo] ?? '') != ($datos[$campo] ?? '')) {
|
|
$cambios[] = "$campo: '{$anterior[$campo]}' → '{$datos[$campo]}'";
|
|
}
|
|
}
|
|
$detalle = implode(' | ', $cambios) ?: 'Sin cambios relevantes';
|
|
$this->registrarHistorial($id, 'editar', $usuarioId, $detalle);
|
|
}
|
|
return $ok;
|
|
}
|
|
|
|
// ── Eliminación lógica ────────────────────────────────────────────────────
|
|
public function eliminarLogico(int $id, int $usuarioId): bool {
|
|
$stmt = $this->db->prepare("UPDATE oficios SET deleted_at=NOW() WHERE id=?");
|
|
$ok = $stmt->execute([$id]);
|
|
if ($ok) $this->registrarHistorial($id, 'eliminar', $usuarioId, 'Movido a papelera.');
|
|
return $ok;
|
|
}
|
|
|
|
// ── Eliminación física (solo admin) ───────────────────────────────────────
|
|
public function eliminarFisico(int $id): bool {
|
|
$stmt = $this->db->prepare("DELETE FROM oficios WHERE id=?");
|
|
return $stmt->execute([$id]);
|
|
}
|
|
|
|
// ── Restaurar desde papelera ──────────────────────────────────────────────
|
|
public function restaurar(int $id, int $usuarioId): bool {
|
|
$stmt = $this->db->prepare("UPDATE oficios SET deleted_at=NULL WHERE id=?");
|
|
$ok = $stmt->execute([$id]);
|
|
if ($ok) $this->registrarHistorial($id, 'restaurar', $usuarioId, 'Restaurado desde papelera.');
|
|
return $ok;
|
|
}
|
|
|
|
// ── Derivar oficio ────────────────────────────────────────────────────────
|
|
public function derivar(int $id, int $nuevoResponsable, int $usuarioId, ?string $comentario = null): bool {
|
|
$stmt = $this->db->prepare(
|
|
"UPDATE oficios SET responsable_id=?, derivado_a_id=?, comentario_derivacion=?, estado='en_proceso'
|
|
WHERE id=? AND deleted_at IS NULL"
|
|
);
|
|
$ok = $stmt->execute([$nuevoResponsable, $nuevoResponsable, $comentario, $id]);
|
|
if ($ok) {
|
|
$this->registrarHistorial($id, 'derivar', $usuarioId, "Derivado a usuario #$nuevoResponsable. Comentario: $comentario");
|
|
}
|
|
return $ok;
|
|
}
|
|
|
|
// ── Historial de cambios ──────────────────────────────────────────────────
|
|
public function historial(int $id): array {
|
|
$stmt = $this->db->prepare(
|
|
"SELECT h.*, CONCAT(u.nombre,' ',u.apellido) AS usuario_nombre
|
|
FROM historial_cambios h
|
|
LEFT JOIN usuarios u ON u.id = h.usuario_id
|
|
WHERE h.tabla='oficios' AND h.registro_id=?
|
|
ORDER BY h.created_at DESC"
|
|
);
|
|
$stmt->execute([$id]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
// ── KPIs para dashboard ───────────────────────────────────────────────────
|
|
public function kpis(?int $usuarioId = null): array {
|
|
if ($usuarioId) {
|
|
$where = "AND o.responsable_id = $usuarioId";
|
|
} else {
|
|
$where = '';
|
|
}
|
|
$row = $this->db->query("SELECT * FROM v_dashboard_kpis")->fetch();
|
|
|
|
$stmt = $this->db->prepare(
|
|
"SELECT estado, COUNT(*) as total FROM oficios WHERE deleted_at IS NULL $where GROUP BY estado"
|
|
);
|
|
$stmt->execute();
|
|
$estados = [];
|
|
foreach ($stmt->fetchAll() as $r) {
|
|
$estados[$r['estado']] = (int)$r['total'];
|
|
}
|
|
|
|
$stmt2 = $this->db->prepare(
|
|
"SELECT prioridad, COUNT(*) as total FROM oficios WHERE deleted_at IS NULL $where GROUP BY prioridad"
|
|
);
|
|
$stmt2->execute();
|
|
$prioridades = [];
|
|
foreach ($stmt2->fetchAll() as $r) {
|
|
$prioridades[$r['prioridad']] = (int)$r['total'];
|
|
}
|
|
|
|
return ['global' => $row, 'estados' => $estados, 'prioridades' => $prioridades];
|
|
}
|
|
|
|
// ── Próximos a vencer (para cron y dashboard) ─────────────────────────────
|
|
public function proximosAVencer(int $dias = 3): array {
|
|
$stmt = $this->db->prepare(
|
|
"SELECT o.*, CONCAT(u.nombre,' ',u.apellido) AS responsable_nombre, u.email AS responsable_email
|
|
FROM oficios o
|
|
LEFT JOIN usuarios u ON u.id = o.responsable_id
|
|
WHERE o.deleted_at IS NULL
|
|
AND o.estado NOT IN ('respondido','archivado')
|
|
AND o.fecha_vencimiento BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
|
ORDER BY o.fecha_vencimiento ASC"
|
|
);
|
|
$stmt->execute([$dias]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
// ── Vencidos sin respuesta (para escalación) ──────────────────────────────
|
|
public function vencidosSinRespuesta(): array {
|
|
$stmt = $this->db->query(
|
|
"SELECT o.*, CONCAT(u.nombre,' ',u.apellido) AS responsable_nombre,
|
|
u.email AS responsable_email, u.supervisor_id,
|
|
CONCAT(s.nombre,' ',s.apellido) AS supervisor_nombre, s.email AS supervisor_email
|
|
FROM oficios o
|
|
LEFT JOIN usuarios u ON u.id = o.responsable_id
|
|
LEFT JOIN usuarios s ON s.id = u.supervisor_id
|
|
WHERE o.deleted_at IS NULL
|
|
AND o.estado NOT IN ('respondido','archivado')
|
|
AND o.fecha_vencimiento < CURDATE()
|
|
ORDER BY o.fecha_vencimiento ASC"
|
|
);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
// ── Sincronizar etiquetas ─────────────────────────────────────────────────
|
|
private function sincronizarEtiquetas(int $oficioId, array $etiquetaIds): void {
|
|
$this->db->prepare("DELETE FROM oficio_etiquetas WHERE oficio_id = ?")->execute([$oficioId]);
|
|
$stmt = $this->db->prepare("INSERT INTO oficio_etiquetas (oficio_id, etiqueta_id) VALUES (?, ?)");
|
|
foreach ($etiquetaIds as $eId) {
|
|
if ((int)$eId > 0) $stmt->execute([$oficioId, (int)$eId]);
|
|
}
|
|
}
|
|
|
|
// ── Registrar historial ───────────────────────────────────────────────────
|
|
private function registrarHistorial(int $registroId, string $accion, int $usuarioId, string $detalle = ''): void {
|
|
$stmt = $this->db->prepare(
|
|
"INSERT INTO historial_cambios (tabla, registro_id, accion, usuario_id, ip_address, detalle)
|
|
VALUES ('oficios', ?, ?, ?, ?, ?)"
|
|
);
|
|
$stmt->execute([$registroId, $accion, $usuarioId, $_SERVER['REMOTE_ADDR'] ?? null, $detalle]);
|
|
}
|
|
|
|
// ── Etiquetas disponibles ─────────────────────────────────────────────────
|
|
public function etiquetas(): array {
|
|
return $this->db->query("SELECT * FROM etiquetas ORDER BY nombre")->fetchAll();
|
|
}
|
|
|
|
// ── Etiquetas de un oficio ────────────────────────────────────────────────
|
|
public function etiquetasDeOficio(int $id): array {
|
|
$stmt = $this->db->prepare(
|
|
"SELECT e.* FROM etiquetas e
|
|
JOIN oficio_etiquetas oe ON oe.etiqueta_id = e.id
|
|
WHERE oe.oficio_id = ?"
|
|
);
|
|
$stmt->execute([$id]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
// ── Comentarios ───────────────────────────────────────────────────────────
|
|
public function comentarios(int $id, bool $soloPublicos = false): array {
|
|
$sql = "SELECT c.*, CONCAT(u.nombre,' ',u.apellido) AS autor_nombre
|
|
FROM comentarios c JOIN usuarios u ON u.id = c.usuario_id
|
|
WHERE c.oficio_id = ?";
|
|
if ($soloPublicos) $sql .= " AND c.es_privado = 0";
|
|
$sql .= " ORDER BY c.created_at DESC";
|
|
$stmt = $this->db->prepare($sql);
|
|
$stmt->execute([$id]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function agregarComentario(int $oficioId, int $usuarioId, string $texto, bool $privado = false): int {
|
|
$stmt = $this->db->prepare(
|
|
"INSERT INTO comentarios (oficio_id, usuario_id, comentario, es_privado) VALUES (?,?,?,?)"
|
|
);
|
|
$stmt->execute([$oficioId, $usuarioId, $texto, (int)$privado]);
|
|
return (int)$this->db->lastInsertId();
|
|
}
|
|
|
|
// ── Estadísticas mensuales (para gráfico de línea) ───────────────────────
|
|
public function estadisticasMensuales(int $meses = 6): array {
|
|
$stmt = $this->db->prepare(
|
|
"SELECT DATE_FORMAT(created_at,'%Y-%m') AS mes,
|
|
estado, COUNT(*) AS total
|
|
FROM oficios
|
|
WHERE deleted_at IS NULL
|
|
AND created_at >= DATE_SUB(NOW(), INTERVAL ? MONTH)
|
|
GROUP BY mes, estado
|
|
ORDER BY mes"
|
|
);
|
|
$stmt->execute([$meses]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
}
|