diff --git a/public/horas/api.php b/public/horas/api.php new file mode 100644 index 0000000..c26dab9 --- /dev/null +++ b/public/horas/api.php @@ -0,0 +1,144 @@ +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é 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 = []; + 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'; } + $bySql = "SELECT h.abogado AS name, SUM(h.hrs) AS total, COUNT(*) AS entries + FROM tramites_hours h + WHERE " . implode(' AND ', $byWhere) . " + 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); + } + + // 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, + '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); +} diff --git a/public/horas/index.php b/public/horas/index.php new file mode 100644 index 0000000..c801c2f --- /dev/null +++ b/public/horas/index.php @@ -0,0 +1,8 @@ +
+ + + diff --git a/views/admin.html b/views/admin.html index 559d7ba..775c83f 100644 --- a/views/admin.html +++ b/views/admin.html @@ -49,6 +49,11 @@ header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0 .btn-logout { font-size:11px; padding:4px 10px; border:1px solid rgba(255,255,255,.2); border-radius:6px; background:transparent; color:rgba(255,255,255,.7); transition:all .15s; } .btn-logout:hover { background:rgba(255,255,255,.1); color:#fff; } +.btn-clock { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; + border:1px solid rgba(255,255,255,0.18); border-radius:8px; color:rgba(255,255,255,0.85); + transition:all .15s; text-decoration:none; } +.btn-clock:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); } +.btn-clock svg { width:16px; height:16px; } .intro { max-width:1100px; margin:24px auto 0; padding:0 24px; } .intro h1 { font-family:'DM Serif Display',serif; font-size:30px; color:var(--navy); font-weight:400; margin-bottom:6px; } @@ -155,6 +160,9 @@ tr.is-me td { background:#FFFBEB; font-weight:500; }
?
…
+ + + Cerrar sesión diff --git a/views/calendario.html b/views/calendario.html index 3d182ae..4478947 100644 --- a/views/calendario.html +++ b/views/calendario.html @@ -54,11 +54,11 @@ header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0 .btn-logout { font-size:11px; padding:4px 10px; border:1px solid rgba(255,255,255,.2); border-radius:6px; background:transparent; color:rgba(255,255,255,.7); transition:all .15s; } .btn-logout:hover { background:rgba(255,255,255,.1); color:#fff; } -.btn-gear { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; +.btn-gear, .btn-clock { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; border:1px solid rgba(255,255,255,0.18); border-radius:8px; color:rgba(255,255,255,0.85); transition:all .15s; text-decoration:none; cursor:pointer; font-family:'DM Sans',sans-serif; } -.btn-gear:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); } -.btn-gear svg { width:16px; height:16px; } +.btn-gear:hover, .btn-clock:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); } +.btn-gear svg, .btn-clock svg { width:16px; height:16px; } .cal-toolbar { background:#fff; border-bottom:1px solid var(--gray-100); padding:12px 24px; display:flex; align-items:center; gap:14px; flex-wrap:wrap; box-shadow:var(--shadow); } @@ -278,6 +278,9 @@ footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px;
?
… + + + diff --git a/views/escrituras.html b/views/escrituras.html index 55dc35b..70e80cc 100644 --- a/views/escrituras.html +++ b/views/escrituras.html @@ -77,15 +77,15 @@ header { transition:all .15s; } .btn-logout:hover { background:rgba(255,255,255,0.1); color:#fff; } -.btn-gear { +.btn-gear, .btn-clock { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; border:1px solid rgba(255,255,255,0.18); border-radius:8px; color:rgba(255,255,255,0.85); transition:all .15s; text-decoration:none; cursor:pointer; font-family:'DM Sans',sans-serif; } -.btn-gear:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); } -.btn-gear svg { width:16px; height:16px; } +.btn-gear:hover, .btn-clock:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); } +.btn-gear svg, .btn-clock svg { width:16px; height:16px; } /* BACK TO PORTAL */ .header-left { display:flex; align-items:center; gap:14px; } @@ -469,6 +469,9 @@ tr.editing-row td textarea.editable {
?
Cargando… + + + diff --git a/views/horas.html b/views/horas.html new file mode 100644 index 0000000..fabe32d --- /dev/null +++ b/views/horas.html @@ -0,0 +1,418 @@ + + + + + +Reporte de Horas — AALN Donna + + + + + + + +
+
+ + + Portal + + +
+
+
+
?
+ … +
+ + Cerrar sesión +
+
+ +
+ +
+

Reporte de Horas

+

…

+
+ +
+
+ + +
+
+ + +
+ +
+ +
+ + + + + +
+
+
+ +
+ +
+
Total horas
—
—
+
Entradas
—
registros de horas
+ +
+ + + +
Detalle de entradas
+
+ + + + + + + + + + + + +
FechaTrámiteClienteAbogadoHorasDescripción
+ +
+ +
+ +
+ + + + diff --git a/views/tramites.html b/views/tramites.html index 6b6dd17..9aa002e 100644 --- a/views/tramites.html +++ b/views/tramites.html @@ -33,9 +33,9 @@ header{background:var(--navy);border-bottom:3px solid var(--gold);padding:0 24px .user-name{font-size:12px;color:rgba(255,255,255,0.85);max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .btn-logout{font-size:11px;padding:4px 10px;border:1px solid rgba(255,255,255,0.20);border-radius:6px;background:transparent;color:rgba(255,255,255,0.70);cursor:pointer;font-family:'DM Sans',sans-serif;transition:all .15s;text-decoration:none;display:inline-block;line-height:1.6;} .btn-logout:hover{background:rgba(255,255,255,0.10);color:#fff;border-color:rgba(255,255,255,0.30);} -.btn-gear{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:1px solid rgba(255,255,255,0.18);border-radius:8px;color:rgba(255,255,255,0.85);transition:all .15s;text-decoration:none;cursor:pointer;font-family:'DM Sans',sans-serif;} -.btn-gear:hover{background:var(--gold);border-color:var(--gold);color:var(--navy);} -.btn-gear svg{width:16px;height:16px;} +.btn-gear,.btn-clock{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:1px solid rgba(255,255,255,0.18);border-radius:8px;color:rgba(255,255,255,0.85);transition:all .15s;text-decoration:none;cursor:pointer;font-family:'DM Sans',sans-serif;} +.btn-gear:hover,.btn-clock:hover{background:var(--gold);border-color:var(--gold);color:var(--navy);} +.btn-gear svg,.btn-clock svg{width:16px;height:16px;} .inv-stack{display:inline-flex;align-items:center;gap:3px;} .inv-avatar{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--navy);color:#fff;font-size:9.5px;font-weight:700;border:1.5px solid #fff;cursor:default;font-family:'DM Sans',sans-serif;} .inv-more{font-size:10px;color:var(--gray);margin-left:3px;} @@ -161,6 +161,9 @@ tr.expanded>td{background:#fffdf5;}
?
Cargando… + + +