feat: tablero de modulo con uso real (equipo helpdesk, pagos validados, inventario API en dashboard)

This commit is contained in:
Nurfog committed 2026-09-22 13:54:54 -03:00
1 parent b6ca254a82
commit c64e990dfa
7 files changed
+2969 -116

No files matched your search

@@ -0,0 +1,13 @@
// Estadísticas reales de uso de Helpdesk: proxy server-side a
// GET /api/helpdesk/stats con la cuenta de servicio.
// Requiere sesión IT.
import { assertIT, helpdeskFetch, asResponse, errResponse } from "@/lib/correo-server";
export async function GET(req: Request): Promise<Response> {
try {
await assertIT(req);
return await asResponse(await helpdeskFetch("/api/helpdesk/stats"));
} catch (e) {
return errResponse(e);
}
}
+13
View File
@@ -0,0 +1,13 @@
// Equipo real que usa Helpdesk: proxy server-side a
// GET /api/helpdesk/users con la cuenta de servicio
// (HELPDESK_SERVICE_USER/PASS). Requiere sesión IT.
import { assertIT, helpdeskFetch, asResponse, errResponse } from "@/lib/correo-server";
export async function GET(req: Request): Promise<Response> {
try {
await assertIT(req);
return await asResponse(await helpdeskFetch("/api/helpdesk/users"));
} catch (e) {
return errResponse(e);
}
}
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { assertIT } from "@/lib/correo-server";
// Pagos validados reales: proxy server-side a
// GET /api/Boletas/payments?mes=&year= (endpoint público de SAM.API,
// no requiere Bearer). Requiere sesión IT para verlo en el portal.
const SAM_API_URL =
process.env.SAM_API_URL ??
process.env.NEXT_PUBLIC_API_URL ??
"https://api.devops.norteamericano.cl";
export async function GET(req: Request): Promise<Response> {
try {
await assertIT(req);
const url = new URL(req.url);
const now = new Date();
const mes = url.searchParams.get("mes") ?? String(now.getMonth() + 1);
const year = url.searchParams.get("year") ?? String(now.getFullYear());
const m = Number(mes);
const y = Number(year);
if (!Number.isInteger(m) || m < 1 || m > 12 || !Number.isInteger(y) || y < 2000 || y > 2100) {
return NextResponse.json({ error: "mes (1-12) y year válidos requeridos" }, { status: 400 });
}
const res = await fetch(
`${SAM_API_URL}/api/Boletas/payments?mes=${m}&year=${y}`,
{ cache: "no-store" }
);
const text = await res.text();
try {
return NextResponse.json(JSON.parse(text), { status: res.status });
} catch {
return new NextResponse(text, { status: res.status });
}
} catch (e) {
const status =
typeof e === "object" && e !== null && "status" in e
? Number((e as { status: unknown }).status) || 500
: 500;
return NextResponse.json(
{ error: e instanceof Error ? e.message : "Error" },
{ status }
);
}
}
+345 -28
View File
@@ -1,14 +1,16 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react";
import SamShell from "@/components/SamShell"; import SamShell from "@/components/SamShell";
import { useModules, useStats, useMe } from "@/lib/store" import { useModules, useStats, useMe } from "@/lib/store"
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { import {
displayName, displayName,
envOf, envOf,
getModuleFunctions,
getStatusInfo, getStatusInfo,
isPagosModule,
isHelpdeskModule,
moduleUrl, moduleUrl,
} from "@/lib/sam"; } from "@/lib/sam";
import { import {
@@ -36,8 +38,340 @@ function Bar({ value, color }: { value: number; color: string }) {
); );
} }
interface HelpdeskMember {
idUsuario: string;
nombres?: string;
apPaterno?: string;
email?: string;
role?: string;
departmentName?: string;
isActive?: boolean;
}
interface HelpdeskStats {
totalTickets: number;
openTickets: number;
inProgressTickets: number;
resolvedTickets: number;
closedTickets: number;
unassignedTickets: number;
criticalTickets: number;
dailyCounts: { date: string; count: number }[];
}
interface Boleta {
folio: number;
alumnoRut?: string | null;
alumnoNombre?: string | null;
fecha: string;
monto: number;
tipo?: string;
estado?: string;
}
function HelpdeskUsage() {
const [team, setTeam] = useState<HelpdeskMember[] | null>(null);
const [stats, setStats] = useState<HelpdeskStats | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
try {
const [tRes, sRes] = await Promise.all([
fetch("/api/portal/helpdesk-team", { cache: "no-store" }),
fetch("/api/portal/helpdesk-stats", { cache: "no-store" }),
]);
if (!tRes.ok || !sRes.ok) {
const t = await tRes.json().catch(() => ({}));
const s = await sRes.json().catch(() => ({}));
throw new Error(
(t as { error?: string }).error ||
(s as { error?: string }).error ||
`Helpdesk respondió ${tRes.status}/${sRes.status}`
);
}
const t = (await tRes.json()) as HelpdeskMember[];
const s = (await sRes.json()) as HelpdeskStats;
if (!cancelled) {
setTeam(Array.isArray(t) ? t : []);
setStats(s);
}
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : "Error al cargar uso");
}
}
load();
return () => {
cancelled = true;
};
}, []);
if (error) {
return (
<div className="alert alert-warning">
<i className="bx bx-error me-2" />
No se pudo cargar el uso real de Helpdesk: {error}
</div>
);
}
if (!team || !stats) {
return (
<div className="text-center py-4">
<div className="spinner-border text-primary" role="status" />
<p className="mt-2 text-muted small">Cargando equipo y estadísticas reales…</p>
</div>
);
}
const active = team.filter((m) => m.isActive !== false);
const maxDaily = Math.max(...stats.dailyCounts.map((d) => d.count), 1);
return (
<>
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
<i className="bx bx-group me-1" /> Equipo que lo usa ({active.length})
</div>
<div className="row row-cards">
<div className="col-12">
<div className="app-card p-3">
{active.length === 0 && (
<p className="text-muted mb-0">Sin integrantes registrados en el equipo.</p>
)}
<div className="list-group">
{active.map((m) => (
<div
key={m.idUsuario}
className="list-group-item d-flex justify-content-between align-items-center flex-wrap gap-2"
>
<span>
<i className="bx bx-user me-2" />
{[m.nombres, m.apPaterno].filter(Boolean).join(" ") || m.idUsuario}
<small className="text-muted d-block">
{m.idUsuario}
{m.email ? ` · ${m.email}` : ""}
{m.departmentName ? ` · ${m.departmentName}` : ""}
</small>
</span>
<span className="badge bg-primary rounded-pill">{m.role ?? "—"}</span>
</div>
))}
</div>
<small className="text-muted d-block mt-2">
Fuente: <code>GET /api/helpdesk/users</code> (SAM.API, cuenta de servicio).
</small>
</div>
</div>
</div>
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
<i className="bx bx-bar-chart-alt-2 me-1" /> Estadísticas de uso
</div>
<div className="row g-3 mb-3">
{[
{ label: "Total tickets", value: stats.totalTickets },
{ label: "Abiertos", value: stats.openTickets },
{ label: "En progreso", value: stats.inProgressTickets },
{ label: "Resueltos", value: stats.resolvedTickets },
{ label: "Críticos", value: stats.criticalTickets },
{ label: "Sin asignar", value: stats.unassignedTickets },
].map((c) => (
<div className="col-6 col-lg-4 col-xl-2" key={c.label}>
<div className="app-card p-3 text-center h-100">
<div className="fw-bold fs-4">{c.value}</div>
<small className="text-muted">{c.label}</small>
</div>
</div>
))}
</div>
{stats.dailyCounts.length > 0 && (
<div className="card mb-4">
<div className="card-header d-flex align-items-center gap-2">
<i className="bx bx-graph-up" style={{ color: "var(--primary)" }}></i>
Tickets por día (últimos 14 días)
</div>
<div className="card-body">
<div className="d-flex align-items-end gap-1" style={{ height: "120px" }}>
{stats.dailyCounts.map((d) => (
<div
key={d.date}
className="flex-fill rounded"
style={{
height: `${(d.count / maxDaily) * 100}%`,
backgroundColor: "var(--primary)",
opacity: 0.7,
minHeight: "4px",
}}
title={`${d.date}: ${d.count} tickets`}
></div>
))}
</div>
<small className="text-muted">
Fuente: <code>GET /api/helpdesk/stats</code> (SAM.API).
</small>
</div>
</div>
)}
</>
);
}
function PagosUsage() {
const now = new Date();
const [mes, setMes] = useState(String(now.getMonth() + 1));
const [year, setYear] = useState(String(now.getFullYear()));
const [rows, setRows] = useState<Boleta[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function load(m: string, y: string) {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/portal/pagos-validados?mes=${m}&year=${y}`, {
cache: "no-store",
});
const data = await res.json().catch(() => null);
if (!res.ok) {
throw new Error(
(data as { error?: string } | null)?.error || `Pagos respondió ${res.status}`
);
}
setRows(Array.isArray(data) ? data : []);
} catch (e) {
setError(e instanceof Error ? e.message : "Error al cargar pagos");
setRows(null);
} finally {
setLoading(false);
}
}
useEffect(() => {
// Carga inicial: fetch async (setState ocurre en callbacks, no sincronico).
// eslint-disable-next-line react-hooks/set-state-in-effect
load(mes, year);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const total = (rows ?? []).reduce((acc, r) => acc + (Number(r.monto) || 0), 0);
return (
<>
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
<i className="bx bx-receipt me-1" /> Pagos validados
</div>
<div className="row row-cards">
<div className="col-12">
<div className="app-card p-3">
<form
className="d-flex gap-2 flex-wrap align-items-end mb-3"
onSubmit={(e) => {
e.preventDefault();
load(mes, year);
}}
>
<div>
<label className="form-label small text-muted mb-1">Mes</label>
<select
className="form-select form-select-sm"
value={mes}
onChange={(e) => setMes(e.target.value)}
>
{Array.from({ length: 12 }, (_, i) => (
<option key={i + 1} value={String(i + 1)}>
{i + 1}
</option>
))}
</select>
</div>
<div>
<label className="form-label small text-muted mb-1">Año</label>
<input
className="form-control form-control-sm"
value={year}
onChange={(e) => setYear(e.target.value)}
inputMode="numeric"
style={{ width: 100 }}
/>
</div>
<button type="submit" className="btn btn-sm btn-primary" disabled={loading}>
{loading ? "Cargando…" : "Consultar"}
</button>
{rows && (
<span className="ms-auto small text-muted align-self-center">
{rows.length} pagos · Total ${total.toLocaleString("es-CL")}
</span>
)}
</form>
{error && (
<div className="alert alert-warning mb-0">
<i className="bx bx-error me-2" />
No se pudieron cargar los pagos validados: {error}
</div>
)}
{!error && loading && (
<div className="text-center py-4">
<div className="spinner-border text-primary" role="status" />
</div>
)}
{!error && !loading && rows && rows.length === 0 && (
<p className="text-muted mb-0">Sin pagos validados para {mes}/{year}.</p>
)}
{!error && !loading && rows && rows.length > 0 && (
<div className="table-responsive">
<table className="table table-sm table-hover mb-0">
<thead>
<tr>
<th>Folio</th>
<th>Alumno</th>
<th>Fecha</th>
<th className="text-end">Monto</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
{rows.slice(0, 100).map((r, i) => (
<tr key={`${r.folio}-${i}`}>
<td>{r.folio}</td>
<td className="small">
{r.alumnoNombre ?? "—"}
{r.alumnoRut ? (
<span className="text-muted d-block">{r.alumnoRut}</span>
) : null}
</td>
<td className="small">
{r.fecha ? String(r.fecha).slice(0, 10) : "—"}
</td>
<td className="text-end">${Number(r.monto || 0).toLocaleString("es-CL")}</td>
<td className="small">{r.estado || r.tipo || "—"}</td>
</tr>
))}
</tbody>
</table>
{rows.length > 100 && (
<small className="text-muted">Mostrando 100 de {rows.length}.</small>
)}
</div>
)}
<small className="text-muted d-block mt-2">
Fuente: <code>GET /api/Boletas/payments?mes=&amp;year=</code> (SAM.API).
</small>
</div>
</div>
</div>
</>
);
}
// Tablero funcional del módulo: estado + carga de trabajo + // Tablero funcional del módulo: estado + carga de trabajo +
// funciones API en el sidebar (plantilla SamShell). // uso real (equipo + estadísticas). Ya no muestra la lista
// inventada "Funciones disponibles de la API": el uso de la API
// vive solo en el dashboard principal (/tablero).
export default function ModuleBoard() { export default function ModuleBoard() {
const params = useParams(); const params = useParams();
const name = Array.isArray(params.name) ? params.name[0] : params.name ?? ""; const name = Array.isArray(params.name) ? params.name[0] : params.name ?? "";
@@ -49,7 +383,6 @@ export default function ModuleBoard() {
const mod = data?.modules.find( const mod = data?.modules.find(
(m) => m.name.toLowerCase() === name.toLowerCase() (m) => m.name.toLowerCase() === name.toLowerCase()
); );
const functions = getModuleFunctions(name);
const stat = mod ? statForModule(stats, mod.name) : undefined; const stat = mod ? statForModule(stats, mod.name) : undefined;
const status = mod ? getStatusInfo(mod) : null; const status = mod ? getStatusInfo(mod) : null;
const env = mod ? envOf(mod.branch) : null; const env = mod ? envOf(mod.branch) : null;
@@ -58,6 +391,8 @@ export default function ModuleBoard() {
const title = mod ? displayName(mod.name) : displayName(name || "Módulo"); const title = mod ? displayName(mod.name) : displayName(name || "Módulo");
const menu = [{ label: "Aplicaciones", icon: "bx-layer", href: "/" }]; const menu = [{ label: "Aplicaciones", icon: "bx-layer", href: "/" }];
const showHelpdesk = isHelpdeskModule(name);
const showPagos = !showHelpdesk && isPagosModule(name);
return ( return (
<SamShell <SamShell
@@ -160,33 +495,15 @@ export default function ModuleBoard() {
</div> </div>
</div> </div>
{showHelpdesk && <HelpdeskUsage />}
{showPagos && <PagosUsage />}
{!showHelpdesk && !showPagos && (
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3"> <div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
<i className="bx bx-list-ul me-1" /> Funciones disponibles de la API <i className="bx bx-info-circle me-1" /> Este módulo aún no expone
</div> estadísticas de uso en el portal. El uso de la API se muestra solo
<div className="row row-cards"> en el <Link href="/tablero">dashboard principal</Link>.
<div className="col-12">
<div className="app-card p-3">
<div className="list-group">
{functions.map((f) => (
<a
key={f.path}
href={`${API_URL}/${f.path}`}
target="_blank"
rel="noopener noreferrer"
className="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
>
<span>
<i className={`bx ${f.icon} me-2`} />
{f.label}
<small className="text-muted d-block">{f.desc}</small>
</span>
<code className="small">GET {f.path}</code>
</a>
))}
</div>
</div>
</div>
</div> </div>
)}
</> </>
)} )}
</SamShell> </SamShell>
+121 -3
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useMemo, type CSSProperties } from "react"; import { useMemo, useState, type CSSProperties } from "react";
import { useModules, useStats } from "@/lib/store"; import { useModules, useStats } from "@/lib/store";
import { import {
assignColorsForModules, assignColorsForModules,
@@ -24,6 +24,11 @@ import {
type ContainerStat, type ContainerStat,
} from "@/lib/stats"; } from "@/lib/stats";
import type { ModuleState } from "@/lib/sam"; import type { ModuleState } from "@/lib/sam";
import {
API_INVENTORY,
API_TOTAL_CONTROLLERS,
API_TOTAL_ENDPOINTS,
} from "@/lib/api-inventory";
function Donut({ ok, starting, down }: { ok: number; starting: number; down: number }) { function Donut({ ok, starting, down }: { ok: number; starting: number; down: number }) {
const total = ok + starting + down; const total = ok + starting + down;
@@ -180,9 +185,120 @@ function AppCard({
); );
} }
const SAM_API_URL =
process.env.NEXT_PUBLIC_API_URL || "https://api.devops.norteamericano.cl";
function ApiUsage() {
const [q, setQ] = useState("");
const [expanded, setExpanded] = useState(false);
const filtered = useMemo(() => {
const needle = q.trim().toLowerCase();
if (!needle) return API_INVENTORY;
return API_INVENTORY.filter(
(c) =>
c.controller.toLowerCase().includes(needle) ||
c.base.toLowerCase().includes(needle) ||
c.endpoints.some((e) => e.path.toLowerCase().includes(needle))
);
}, [q]);
const shown = expanded ? filtered : filtered.slice(0, 12);
const getCount = API_INVENTORY.reduce(
(acc, c) => acc + c.endpoints.filter((e) => e.method === "GET").length,
0
);
return (
<>
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
<i className="bx bx-link-external me-1" /> Uso de la API (SAM.API real)
</div>
<div className="row row-cards">
<div className="col-12">
<div className="app-card p-4">
<div className="d-flex gap-4 flex-wrap align-items-center mb-3">
<div>
<small className="text-muted d-block">Endpoints reales</small>
<span className="fw-bold fs-4">{API_TOTAL_ENDPOINTS}</span>
</div>
<div>
<small className="text-muted d-block">Controladores</small>
<span className="fw-bold fs-4">{API_TOTAL_CONTROLLERS}</span>
</div>
<div>
<small className="text-muted d-block">Lecturas (GET)</small>
<span className="fw-bold fs-4">{getCount}</span>
</div>
<a
href={`${SAM_API_URL}/swagger`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm btn-outline-primary ms-auto"
>
<i className="bx bx-link-external me-1" />
Abrir Swagger
</a>
</div>
<div className="mb-3" style={{ maxWidth: 420 }}>
<input
className="form-control form-control-sm"
placeholder="Filtrar por controlador o ruta (ej: webpay, boletas, helpdesk)…"
value={q}
onChange={(e) => setQ(e.target.value)}
/>
</div>
<div className="table-responsive" style={{ maxHeight: 420, overflowY: "auto" }}>
<table className="table table-sm table-hover mb-0">
<thead className="table-light" style={{ position: "sticky", top: 0 }}>
<tr>
<th>Controlador</th>
<th>Ruta base</th>
<th className="text-center">Endpoints</th>
<th>Métodos</th>
</tr>
</thead>
<tbody>
{shown.map((c) => {
const methods = Array.from(new Set(c.endpoints.map((e) => e.method))).join(" · ");
return (
<tr key={c.controller}>
<td className="fw-semibold">{c.controller}</td>
<td>
<code className="small">{c.base}</code>
</td>
<td className="text-center">{c.count}</td>
<td className="small text-muted">{methods}</td>
</tr>
);
})}
</tbody>
</table>
{filtered.length === 0 && (
<p className="text-muted small mb-0 mt-2">Sin coincidencias para “{q}”.</p>
)}
</div>
{filtered.length > 12 && (
<button
className="btn btn-sm btn-outline-secondary mt-3"
onClick={() => setExpanded((v) => !v)}
>
{expanded ? "Ver menos" : `Ver todos (${filtered.length})`}
</button>
)}
<small className="text-muted d-block mt-3">
Inventario generado desde <code>backend/SAM.API/Controllers/*.cs</code> — no
son funciones inventadas por el portal. El detalle por aplicación vive en su
tablero: Helpdesk muestra equipo + tickets, WebPay/Pagos muestra pagos
validados.
</small>
</div>
</div>
</div>
</>
);
}
// Vista del tablero (contenido): tarjeta grande de estado global + // Vista del tablero (contenido): tarjeta grande de estado global +
// estado y carga por aplicación. Se usa en /tablero y como // estado y carga por aplicación + uso real de la API.
// landing de it.sam.devops.norteamericano.cl // Se usa en /tablero y como landing de it.sam.devops.norteamericano.cl
export default function TableroView() { export default function TableroView() {
const { data } = useModules(); const { data } = useModules();
const stats = useStats(); const stats = useStats();
@@ -292,6 +408,8 @@ export default function TableroView() {
</> </>
)} )}
<ApiUsage />
<p className="text-muted small mt-4 mb-4 text-center"> <p className="text-muted small mt-4 mb-4 text-center">
Auto-actualizado cada 30s · Carga de trabajo en tiempo real (docker stats) Auto-actualizado cada 30s · Carga de trabajo en tiempo real (docker stats)
</p> </p>
File diff suppressed because it is too large. Load diff
+25 -85
View File
@@ -50,13 +50,6 @@ export interface ApiResponse {
modules: ModuleState[]; modules: ModuleState[];
} }
export interface ApiFunction {
label: string;
icon: string;
path: string;
desc: string;
}
export interface MenuItem { export interface MenuItem {
label: string; label: string;
icon: string; icon: string;
@@ -204,88 +197,35 @@ export function onModuleColor(hex: string): string {
return lum > 0.62 ? "#212529" : "#ffffff"; return lum > 0.62 ? "#212529" : "#ffffff";
} }
const A = ( // ============================================================
label: string, // Uso real por módulo (sin funciones inventadas).
icon: string, // El bloque anterior "Funciones disponibles de la API" (CORE /
path: string, // FUNCTION_SETS) se eliminó: listaba rutas que no corresponden
desc: string // a la API real. Ahora:
): ApiFunction => ({ label, icon, path, desc }); // - El dashboard principal muestra el uso de la API con el
// inventario real (src/lib/api-inventory.ts, generado desde
// backend/SAM.API/Controllers).
// - /m/helpdesk muestra equipo (GET /api/helpdesk/users) +
// estadísticas de uso (GET /api/helpdesk/stats).
// - /m/webpay y /m/pagos muestran pagos validados
// (GET /api/Boletas/payments?mes&year).
// ============================================================
// --- Conjuntos de funciones por familia de módulo --- export type ModuleUsageKind = "helpdesk" | "pagos" | "generic";
const CORE: ApiFunction[] = [
A("Alumnos", "bx-group", "api/alumnos", "Ficha, matrícula y sesiones de alumnos"),
A("Cursos", "bx-book", "api/cursos", "Catálogo, apertura y horarios de cursos"),
A("Profesores", "bx-pencil", "api/profesores", "Ficha y periodos de profesores"),
A("Empresas", "bx-buildings", "api/empresas/extra", "Empresas en convenio y propuestas"),
A("Pagos", "bx-money", "api/pagos", "Caja, cotizaciones y vouchers"),
A("Contratos", "bx-file", "api/contratos", "Contratos firmados y por firmar"),
A("Informes", "bx-bar-chart-alt-2", "api/informes", "Reportes académicos y de gestión"),
];
const FUNCTION_SETS: Record<string, ApiFunction[]> = { export function moduleUsageKind(name: string): ModuleUsageKind {
academico: [ const key = (name || "").toLowerCase();
A("Cursos", "bx-book", "api/cursos", "Catálogo, apertura y horarios"), if (key === "helpdesk" || key.includes("helpdesk")) return "helpdesk";
A("Alumnos", "bx-group", "api/alumnos/extra", "Ficha, matrícula y sesiones"), if (key === "webpay" || key === "pagos" || key.includes("pago")) return "pagos";
A("Apoderados", "bx-user", "api/apoderados", "Apoderados y contactos"), return "generic";
A("Horarios", "bx-time", "api/horarios", "Bloques horarios y salas"), }
A("Jornadas", "bx-sun", "api/jornadas", "Jornadas diurno / vespertino"),
A("Feriados", "bx-calendar", "api/feriados", "Calendario de feriados"),
A("Placement Test", "bx-edit", "api/placement-test", "Pruebas de nivel"),
A("Plan Anual", "bx-calendar-alt", "api/plan-anual", "Planificación anual"),
],
profesores: [
A("Profesores", "bx-pencil", "api/profesores", "Ficha y periodos"),
A("Funciones Extra", "bx-plus-circle", "api/profesores/extra", "Operaciones extendidas"),
A("Horarios", "bx-time", "api/horarios", "Bloques horarios y salas"),
A("Jornadas", "bx-sun", "api/jornadas", "Jornadas diurno / vespertino"),
],
empresas: [
A("Propuestas", "bx-briefcase", "api/propuestas-empresa", "Propuestas a empresas"),
A("Empresas", "bx-buildings", "api/empresas/extra", "Empresas en convenio"),
A("Comercial", "bx-trending-up", "api/comercial/extra", "Gestión comercial"),
A("Contratos", "bx-file", "api/contratos/extra", "Contratos con empresas"),
],
ventas: [
A("Comercial", "bx-trending-up", "api/comercial/extra", "Gestión comercial"),
A("Leads", "bx-target-lock", "api/leads/extra", "Prospectos y seguimiento"),
A("Ventas", "bx-cart", "api/ventas", "Ventas y cotizaciones"),
A("Cotizaciones", "bx-receipt", "api/cotizaciones", "Cotizaciones emitidas"),
A("Ejecutivos", "bx-user-voice", "api/ejecutivos", "Ejecutivos de venta"),
],
pagos: [
A("Portal Pagos", "bx-credit-card", "api/portal-pagos", "Pagos en línea"),
A("Pagos", "bx-money", "api/pagos", "Caja y vouchers"),
A("WebPay", "bx-card", "api/webpay", "Transacciones Transbank"),
A("Boletas", "bx-receipt", "api/boletas", "Boletas y facturación"),
A("Arqueo", "bx-calculator", "api/arqueocaja", "Arqueo de caja"),
],
contabilidad: [
A("Contabilidad", "bx-detail", "api/contabilidad", "Movimientos contables"),
A("Libros", "bx-book-open", "api/libros", "Libros de compra / venta"),
A("DTE", "bx-file-blank", "api/dte", "Documentos tributarios"),
A("Documentos", "bx-folder", "api/documentos", "Repositorio documental"),
],
};
const MODULE_FAMILY: Record<string, string> = { export function isHelpdeskModule(name: string): boolean {
academico: "academico", return moduleUsageKind(name) === "helpdesk";
"academico-empresas": "empresas", }
portal: "empresas",
empresas: "empresas",
ventas: "ventas",
comercial: "ventas",
pagos: "pagos",
contabilidad: "contabilidad",
profesores: "profesores",
};
export function getModuleFunctions(name: string): ApiFunction[] { export function isPagosModule(name: string): boolean {
const key = name.toLowerCase(); return moduleUsageKind(name) === "pagos";
const family =
MODULE_FAMILY[key] ??
Object.keys(FUNCTION_SETS).find((f) => key.includes(f));
if (family && FUNCTION_SETS[family]) return FUNCTION_SETS[family];
return CORE;
} }
// ============================================================ // ============================================================