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); }