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é usuario filtrar: // - Socio/admin: por user seleccionado (id+nombre), o todos si user=0 // - Resto: forzado a su propio user_id (con fallback a su name para legacy) $filterUserId = null; $filterName = null; if (Auth::isSocio()) { if ($userParam > 0) { $sel = $pdo->prepare('SELECT id, name FROM users WHERE id = :id'); $sel->execute([':id' => $userParam]); $u = $sel->fetch(); if ($u) { $filterUserId = (int)$u['id']; $filterName = (string)$u['name']; } } } else { $filterUserId = Auth::userId(); $filterName = Auth::userName(); if ($filterUserId <= 0 && $filterName === '') { ok([ 'entries' => [], 'by_user' => [], 'by_user_tramite' => [], '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.', ]); } } // Filtro por usuario: prefiere user_id (canónico); cae al name solo // para filas legacy donde user_id quedó NULL. $where = ['1=1']; $params = []; if ($filterUserId !== null && $filterUserId > 0) { $where[] = '(h.user_id = :uid OR (h.user_id IS NULL AND h.abogado = :name))'; $params[':uid'] = $filterUserId; $params[':name'] = (string)$filterName; } elseif ($filterName !== null && $filterName !== '') { // Fallback: solo nombre disponible (no debería ocurrir tras 014) $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 COALESCE(u.name, h.abogado) AS name, SUM(h.hrs) AS total, COUNT(*) AS entries FROM tramites_hours h LEFT JOIN users u ON u.id = h.user_id WHERE $whereByUser GROUP BY COALESCE(u.name, 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 COALESCE(u.name, 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 LEFT JOIN users u ON u.id = h.user_id WHERE $whereByUser GROUP BY COALESCE(u.name, h.abogado), t.id ORDER BY name 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 ($filterUserId !== null && $filterUserId > 0) { // Abogado/usuario regular: su propio desglose por trámite (vía user_id + fallback name) $butParams = [':uid' => $filterUserId, ':name' => (string)$filterName]; $butWhere = ['(h.user_id = :uid OR (h.user_id IS NULL AND 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 COALESCE(u.name, 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 LEFT JOIN users u ON u.id = h.user_id WHERE " . implode(' AND ', $butWhere) . " GROUP BY COALESCE(u.name, 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); }