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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Entradas
—
registros de horas
+
Personas con horas
—
en el período
+
+
+
+
+
Detalle de entradas
+
+
+
+
+ | Fecha |
+ Trámite |
+ Cliente |
+ Abogado |
+ Horas |
+ Descripción |
+
+
+
+
+
Sin entradas de horas en el período seleccionado.
+
+
+
+
+
+
+
+
+
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…
+
+
+