- 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
225 lines
9.0 KiB
PHP
225 lines
9.0 KiB
PHP
<?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);
|
||
}
|