Files
donna/public/horas/api.php
T
Soporte ZH Consultores eff590af47 feat(export): UI unificada de Excel + PDF en los 4 módulos
- Nuevos assets compartidos /assets/donna-export.{css,js}: botón
  "Exportar ▾" + menú dropdown Excel/PDF y helpers DonnaExport.toExcel
  / .toPDF / .attach / .buttonHtml
- escrituras: refactor de exportExcel/exportPDF para usar el módulo
  compartido (sin cambio de comportamiento)
- tramites: nuevo botón en la toolbar de cada tab (adm/corp/mig/lit),
  exporta los rows filtrados de ese tab; PDF excluye Involucrados
- calendario: nuevo botón al final de la toolbar, exporta visibleEvents()
  (respeta chips de origen y búsqueda)
- horas: reemplaza CSV server-side por XLSX client-side + agrega PDF;
  removida la acción ?action=export del api.php
2026-06-04 22:30:37 -05:00

195 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/Auth.php';
require_once __DIR__ . '/../../src/Db.php';
Auth::requireAuth();
header('Content-Type: application/json; charset=utf-8');
function ok($d = null): void { echo json_encode(['ok'=>true, 'data'=>$d, 'user'=>Auth::user()], JSON_UNESCAPED_UNICODE); exit; }
function err(string $m, int $c = 400): void { http_response_code($c); echo json_encode(['ok'=>false, 'error'=>$m], JSON_UNESCAPED_UNICODE); exit; }
function clean_date(?string $v): ?string {
if (!$v) return null;
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : null;
}
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$pdo = Db::pdo();
if ($method !== 'GET' || $action !== 'list') err('Acción no reconocida', 404);
try {
$from = clean_date($_GET['from'] ?? null);
$to = clean_date($_GET['to'] ?? null);
$userParam = isset($_GET['user']) ? (int)$_GET['user'] : 0;
// Determinar por qué nombre filtrar:
// - Socio/admin: por user seleccionado, o todos si user=0
// - Resto: forzado a sus propias horas (matched por su name)
$filterName = null;
if (Auth::isSocio()) {
if ($userParam > 0) {
$sel = $pdo->prepare('SELECT name FROM users WHERE id = :id');
$sel->execute([':id' => $userParam]);
$name = $sel->fetchColumn();
if ($name !== false && $name !== '') $filterName = (string)$name;
}
} else {
$filterName = Auth::userName();
if ($filterName === '') {
ok([
'entries' => [],
'by_user' => [],
'avail_users' => [],
'total_hrs' => 0,
'is_socio' => false,
'my_name' => '',
'note' => 'Tu usuario no tiene nombre asignado. Pídele al admin que lo complete en el Panel de Control.',
]);
}
}
$where = ['1=1'];
$params = [];
if ($filterName !== null) {
$where[] = 'h.abogado = :name';
$params[':name'] = $filterName;
}
if ($from) {
$where[] = 'h.fecha >= :from';
$params[':from'] = $from . ' 00:00:00';
}
if ($to) {
$where[] = 'h.fecha <= :to';
$params[':to'] = $to . ' 23:59:59';
}
$whereSql = implode(' AND ', $where);
$sql = "SELECT h.id, h.fecha, h.abogado, h.hrs, h.descripcion, h.autor_email,
h.tramite_id,
t.codigo AS tramite_codigo,
t.departamento AS tramite_departamento,
COALESCE(NULLIF(t.nombre,''), NULLIF(t.tipo_tramite,''), NULLIF(t.tipo_proceso,''), t.codigo) AS tramite_title,
t.cliente AS tramite_cliente,
t.estatus AS tramite_estatus
FROM tramites_hours h
JOIN tramites_legales t ON t.id = h.tramite_id
WHERE $whereSql
ORDER BY h.fecha DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$entries = $stmt->fetchAll();
foreach ($entries as &$e) {
$e['id'] = (int)$e['id'];
$e['tramite_id'] = (int)$e['tramite_id'];
$e['hrs'] = (float)$e['hrs'];
}
unset($e);
$totalHrs = array_sum(array_column($entries, 'hrs'));
// Breakdown por usuario (solo socio/admin): ignora filtro de abogado pero
// respeta rango de fechas → permite ver "todos los usuarios" del período
$byUser = [];
$byUserTramite = [];
if (Auth::isSocio()) {
$byWhere = ['1=1'];
$byParams = [];
if ($from) { $byWhere[] = 'h.fecha >= :from'; $byParams[':from'] = $from . ' 00:00:00'; }
if ($to) { $byWhere[] = 'h.fecha <= :to'; $byParams[':to'] = $to . ' 23:59:59'; }
$whereByUser = implode(' AND ', $byWhere);
$bySql = "SELECT h.abogado AS name, SUM(h.hrs) AS total, COUNT(*) AS entries
FROM tramites_hours h
WHERE $whereByUser
GROUP BY h.abogado
ORDER BY total DESC";
$byStmt = $pdo->prepare($bySql);
$byStmt->execute($byParams);
$byUser = $byStmt->fetchAll();
foreach ($byUser as &$bu) {
$bu['total'] = (float)$bu['total'];
$bu['entries'] = (int)$bu['entries'];
}
unset($bu);
// Breakdown por usuario × trámite
$butSql = "SELECT h.abogado AS name, t.id AS tramite_id, t.codigo AS tramite_codigo,
t.departamento AS departamento,
COALESCE(NULLIF(t.nombre,''), NULLIF(t.tipo_tramite,''), NULLIF(t.tipo_proceso,''), t.codigo) AS tramite_title,
t.cliente AS tramite_cliente,
SUM(h.hrs) AS total, COUNT(*) AS entries
FROM tramites_hours h
JOIN tramites_legales t ON t.id = h.tramite_id
WHERE $whereByUser
GROUP BY h.abogado, t.id
ORDER BY h.abogado ASC, total DESC";
$butStmt = $pdo->prepare($butSql);
$butStmt->execute($byParams);
$byUserTramite = $butStmt->fetchAll();
foreach ($byUserTramite as &$r) {
$r['tramite_id'] = (int)$r['tramite_id'];
$r['total'] = (float)$r['total'];
$r['entries'] = (int)$r['entries'];
}
unset($r);
} elseif ($filterName !== null) {
// Abogado/usuario regular: su propio desglose por trámite
$butParams = [':name' => $filterName];
$butWhere = ['h.abogado = :name'];
if ($from) { $butWhere[] = 'h.fecha >= :from'; $butParams[':from'] = $from . ' 00:00:00'; }
if ($to) { $butWhere[] = 'h.fecha <= :to'; $butParams[':to'] = $to . ' 23:59:59'; }
$butSql = "SELECT h.abogado AS name, t.id AS tramite_id, t.codigo AS tramite_codigo,
t.departamento AS departamento,
COALESCE(NULLIF(t.nombre,''), NULLIF(t.tipo_tramite,''), NULLIF(t.tipo_proceso,''), t.codigo) AS tramite_title,
t.cliente AS tramite_cliente,
SUM(h.hrs) AS total, COUNT(*) AS entries
FROM tramites_hours h
JOIN tramites_legales t ON t.id = h.tramite_id
WHERE " . implode(' AND ', $butWhere) . "
GROUP BY h.abogado, t.id
ORDER BY total DESC";
$butStmt = $pdo->prepare($butSql);
$butStmt->execute($butParams);
$byUserTramite = $butStmt->fetchAll();
foreach ($byUserTramite as &$r) {
$r['tramite_id'] = (int)$r['tramite_id'];
$r['total'] = (float)$r['total'];
$r['entries'] = (int)$r['entries'];
}
unset($r);
}
// Lista de usuarios para el dropdown de filtro (solo socios/admins)
$availUsers = [];
if (Auth::isSocio()) {
$availUsers = $pdo->query(
"SELECT id, name, email FROM users
WHERE activo = 1 AND name != '' AND role != 'admin'
ORDER BY name"
)->fetchAll();
foreach ($availUsers as &$u) $u['id'] = (int)$u['id'];
unset($u);
}
ok([
'entries' => $entries,
'by_user' => $byUser,
'by_user_tramite' => $byUserTramite,
'avail_users' => $availUsers,
'total_hrs' => $totalHrs,
'is_socio' => Auth::isSocio(),
'my_name' => Auth::userName(),
]);
} catch (PDOException $e) {
error_log('horas db error: ' . $e->getMessage());
err('Error de base de datos', 500);
} catch (Throwable $e) {
error_log('horas error: ' . $e->getMessage());
err('Error interno', 500);
}