Files
sam-portal/src/app/page.tsx
T

258 lines
10 KiB
TypeScript

"use client";
import Link from "next/link";
import { useModules, refreshModules } from "@/lib/store";
import {
displayName,
envOf,
getModuleMeta,
getStatusInfo,
moduleUrl,
sectionOf,
SECTIONS,
type ModuleState,
} from "@/lib/sam";
export default function Home() {
const { data, error } = useModules();
const loading = data === null && error === null;
const fetchModules = refreshModules;
const modules = data?.modules ?? [];
const backend = data?.backend;
const bySection: Record<string, ModuleState[]> = {};
modules.forEach((m) => {
const s = sectionOf(m.name, m.category);
if (!bySection[s]) bySection[s] = [];
bySection[s].push(m);
});
const renderModuleCard = (m: ModuleState) => {
const meta = getModuleMeta(m.name);
const status = getStatusInfo(m);
const env = envOf(m.branch);
const url = moduleUrl(m);
const name = displayName(m.name);
return (
<div className="col-md-6" key={m.name}>
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i
className={`bx ${meta.icon} bx-sm bx-border text-light ${meta.bg}`}
/>
</td>
<td>
<Link
href={`/m/${m.name}`}
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
{name}
</Link>
<div className="d-flex align-items-center gap-2">
<span className={`status-dot ${status.cls}`} />
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
{status.label} · :{m.port} · {m.branch}
</small>
<span className={`badge ${env.cls} badge-respaldo`}>
{env.label}
</span>
</div>
</td>
<td className="text-end pe-3" style={{ width: 40 }}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
title={`Abrir ${url}`}
className="position-relative"
style={{ zIndex: 2 }}
>
<i className="bx bx-window-open text-muted" />
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
};
return (
<>
<div className="sidenav">
<div className="row text-light login-main-text">
<div className="col-12 text-center">
<div
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3 overflow-hidden"
style={{ width: 80, height: 80 }}
>
<img src="/logo-sam.png" alt="SAM" style={{ width: 64, height: 64, objectFit: "contain" }} />
</div>
</div>
<div className="col-12 text-center mt-3">
<h1 className="fw-bold fs-2">Sistema SAM</h1>
</div>
<div className="col-12 text-center">
<h4 className="fw-semibold opacity-75">Aplicaciones</h4>
</div>
<div className="col-12 text-center mt-4 d-flex flex-column gap-2 align-items-center">
<Link href="/tablero" className="btn btn-light btn-sm px-4">
<i className="bx bx-pie-chart-alt-2 me-1" /> Tablero IT
</Link>
<Link href="/upload" className="btn btn-outline-light btn-sm px-4">
<i className="bx bx-plus me-1" /> Nuevo Módulo
</Link>
</div>
<div className="col-12 text-center mt-3">
<small className="opacity-50">Portal de integración</small>
</div>
</div>
</div>
<div className="main">
<div className="row" id="modulos">
<div className="col-lg-1" />
<div className="col-lg-10 mt-4">
{loading && (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status" />
<p className="mt-2 text-muted">Cargando módulos...</p>
</div>
)}
{error && (
<div className="alert alert-danger d-flex align-items-center">
<i className="bx bx-error-circle me-2 fs-5" />
<div>
<strong>Error:</strong> {error}
<button
onClick={fetchModules}
className="btn btn-sm btn-outline-danger ms-3"
>
Reintentar
</button>
</div>
</div>
)}
{!loading && !error && (
<div className="row row-cards">
{modules.length === 0 && (
<div className="col-12">
<div className="app-card p-4 text-center">
<i className="bx bx-inbox fs-1 text-muted" />
<p className="text-muted mt-2 mb-3">
No hay módulos integrados. ¡Sube tu primer proyecto!
</p>
<Link href="/upload" className="btn btn-primary btn-sm">
<i className="bx bx-upload me-1" /> Subir Proyecto
</Link>
</div>
</div>
)}
{SECTIONS.map((section) => {
const list = bySection[section.id] ?? [];
const isReview = section.id === "otras-apps";
if (!isReview && list.length === 0) return null;
return (
<div className="col-12" key={section.id} id={section.id}>
<div className="row row-cards">
<div className="col-12">
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
<span>
<i className={`bx ${section.icon} me-1`} />{" "}
{section.title}
</span>
{list.length > 0 && (
<span className="badge bg-primary rounded-pill">
{list.length} módulo
{list.length !== 1 ? "s" : ""}
</span>
)}
</div>
</div>
{list.map(renderModuleCard)}
{isReview && backend && (
<>
<div className="col-md-6">
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i className="bx bx-code-alt bx-sm bx-border text-light bg-dark" />
</td>
<td>
<a
href="https://api.devops.norteamericano.cl/health"
target="_blank"
rel="noopener noreferrer"
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
API Backend
</a>
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
{backend.container ? "Activo" : "Detenido"} · {backend.branch}
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div className="col-md-6">
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i className="bx bx-cog bx-sm bx-border text-light bg-secondary" />
</td>
<td>
<Link
href="/upload"
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
Gestionar Módulos
</Link>
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
Subir / actualizar proyectos
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
)}
</div>
</div>
);
})}
<div className="col-12 mt-4 mb-4 text-center">
<small className="text-muted">
Auto-actualizado cada 15s ·{" "}
<a href="#" onClick={(e) => { e.preventDefault(); fetchModules(); }}>
actualizar ahora
</a>
</small>
</div>
</div>
)}
</div>
</div>
</div>
</>
);
}