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
This commit is contained in:
Soporte ZH Consultores
2026-06-04 22:10:05 -05:00
parent 8ff56d3d78
commit 3f922bc877
2 changed files with 171 additions and 27 deletions
+88 -8
View File
@@ -19,7 +19,7 @@ $method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$pdo = Db::pdo();
if ($method !== 'GET' || $action !== 'list') err('Acción no reconocida', 404);
if ($method !== 'GET' || !in_array($action, ['list','export'], true)) err('Acción no reconocida', 404);
try {
$from = clean_date($_GET['from'] ?? null);
@@ -94,14 +94,17 @@ try {
// 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 " . implode(' AND ', $byWhere) . "
WHERE $whereByUser
GROUP BY h.abogado
ORDER BY total DESC";
$byStmt = $pdo->prepare($bySql);
@@ -112,6 +115,52 @@ try {
$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)
@@ -126,13 +175,44 @@ try {
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,
'avail_users' => $availUsers,
'total_hrs' => $totalHrs,
'is_socio' => Auth::isSocio(),
'my_name' => Auth::userName(),
'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) {