Files
donna/public/horas/api.php
T
Soporte ZH Consultores 3f922bc877 feat(horas): desglose por trámite en cada abogado + exportar a Excel
- API: by_user_tramite agrupa horas por (abogado, trámite) para
  socios y para el propio abogado (no-socio)
- API: nueva acción ?action=export que devuelve CSV con BOM UTF-8
  (Fecha · Abogado · Departamento · Código · Trámite · Cliente ·
  Estatus · Horas · Descripción), respetando los mismos filtros
- UI: tarjetas de "Por persona" ahora son expandibles (clic en
  la tarjeta) y muestran un mini-listado de trámites con total
  de horas y cantidad de entradas por trámite
- UI: cada trámite del desglose es clickable y abre la ficha
- UI: botón verde "Exportar a Excel" en la barra de herramientas
2026-06-04 22:10:05 -05:00

225 lines
9.0 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' || !in_array($action, ['list','export'], true)) 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);
}
if ($action === 'export') {
// CSV con BOM UTF-8 para que Excel lea acentos correctamente
$filename = 'horas_' . date('Y-m-d_His') . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
$deptLabels = ['adm'=>'Administrativo', 'corp'=>'Corp · Com · Civil', 'mig'=>'Migración', 'lit'=>'Litigios'];
$out = fopen('php://output', 'w');
fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, ['Fecha', 'Abogado', 'Departamento', 'Código', 'Trámite', 'Cliente', 'Estatus', 'Horas', 'Descripción']);
foreach ($entries as $e) {
$hrs = (float)$e['hrs'];
$full = floor($hrs);
$mins = (int)round(($hrs - $full) * 60);
$hrsFmt = $mins === 0 ? (string)$full : sprintf('%d:%02d', $full, $mins);
fputcsv($out, [
date('Y-m-d H:i', strtotime($e['fecha'])),
$e['abogado'],
$deptLabels[$e['tramite_departamento']] ?? $e['tramite_departamento'],
$e['tramite_codigo'],
$e['tramite_title'],
$e['tramite_cliente'],
$e['tramite_estatus'],
$hrsFmt,
$e['descripcion'],
]);
}
fclose($out);
exit;
}
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);
}