Files
front-Censo/src/components/Equipo_Computo/Pregunta3.tsx
T

447 lines
15 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import styles from "./pregunta3.module.scss";
import ToggleButton from "../Toggle/ToggleButton";
import { PROCESADORES_POR_EQUIPO } from "@/data/procesadores";
/* ---------- types ---------- */
type ProcessorEntry = {
tipo: string;
alumnos: string;
profesores: string;
tecnicos: string;
investigadores: string;
administrativos: string;
total: string;
isTotal?: boolean;
};
type PlatformKey =
| "pc-desktop"
| "apple-desktop"
| "pc-laptop"
| "apple-laptop"
| "servers";
type PlatformData = Record<PlatformKey, ProcessorEntry[]>;
/* ---------- labels ---------- */
const PLATFORM_LABELS: Record<PlatformKey, string> = {
"pc-desktop": "Computadoras de escritorio Plataforma PC",
"apple-desktop": "Computadoras de escritorio Plataforma Apple",
"pc-laptop": "Computadoras portátiles Plataforma PC",
"apple-laptop": "Computadoras portátiles Plataforma Apple",
servers: "Alto rendimiento Servidores",
};
/* ---------- mapeo uso -> campo ---------- */
const USO_TO_FIELD: Record<
string,
keyof Omit<ProcessorEntry, "tipo" | "total" | "isTotal">
> = {
ALUMNO: "alumnos",
PROFESOR: "profesores",
"TÉCNICO ACADEMICO": "tecnicos",
INVESTIGADOR: "investigadores",
ADMINISTRATIVO: "administrativos",
};
/* ---------- mapa platformKey -> PROCESADORES_POR_EQUIPO keys ---------- */
/* Este mapa indica qué conjuntos de procesadores corresponden a cada PlatformKey.
Ajusta si quieres incluir/excluir grupos. */
const PLATFORM_PROCESSOR_GROUPS: Record<PlatformKey, number[]> = {
"pc-desktop": [1, 3], // escritorio PC + escritorio Linux (ambos comparten procesadores)
"apple-desktop": [2],
"pc-laptop": [4, 5],
"apple-laptop": [6],
servers: [10],
};
/* ---------- helpers ---------- */
function cleanName(str: string) {
return (
String(str)
.normalize?.("NFKD")
.replace?.(/\u00A0/g, " ")
.replace?.(/\s+/g, " ")
.trim?.() ?? String(str)
);
}
/* Devuelve la lista "oficial" de procesadores (strings) para la plataforma */
function getOfficialProcessors(platform: PlatformKey): string[] {
const groups = PLATFORM_PROCESSOR_GROUPS[platform] || [];
const names: string[] = [];
for (const g of groups) {
const arr = PROCESADORES_POR_EQUIPO[g] || [];
for (const p of arr) {
names.push(p.procesador);
}
}
// dedupe preserving order
const seen = new Set<string>();
return names.filter((n) => {
const c = cleanName(n).toLowerCase();
if (seen.has(c)) return false;
seen.add(c);
return true;
});
}
/* ---------- transformación de datos ---------- */
function transformProcessorDataWithOfficial(
rawArray: { procesador: string; uso: string; total: string }[],
platform: PlatformKey
): ProcessorEntry[] {
// 1) crear mapa cleanedName -> aggregated numbers por uso
const mapRaw: Record<string, { [k in keyof typeof USO_TO_FIELD]?: number }> =
{};
for (const r of rawArray) {
const name = cleanName(r.procesador);
const usoKey = String(r.uso).toUpperCase();
const field = USO_TO_FIELD[usoKey];
if (!mapRaw[name]) {
mapRaw[name] = {};
}
const prev = parseInt(r.total || "0") || 0;
if (field) {
mapRaw[name][field] = (mapRaw[name][field] || 0) + prev;
}
}
// 2) obtener lista oficial y construir filas fijas primero
const official = getOfficialProcessors(platform);
const fixedRows: ProcessorEntry[] = official.map((proc) => {
const c = cleanName(proc);
const sums = mapRaw[c] || {};
const alumnos = (sums["alumnos"] || 0).toString();
const profesores = (sums["profesores"] || 0).toString();
const tecnicos = (sums["tecnicos"] || 0).toString();
const investigadores = (sums["investigadores"] || 0).toString();
const administrativos = (sums["administrativos"] || 0).toString();
const total =
(parseInt(alumnos) || 0) +
(parseInt(profesores) || 0) +
(parseInt(tecnicos) || 0) +
(parseInt(investigadores) || 0) +
(parseInt(administrativos) || 0);
return {
tipo: proc,
alumnos,
profesores,
tecnicos,
investigadores,
administrativos,
total: total.toString(),
};
});
// 3) extras: procesadores del backend que no están en official
const officialSet = new Set(official.map((p) => cleanName(p).toLowerCase()));
const extras = Object.keys(mapRaw)
.filter((k) => !officialSet.has(k.toLowerCase()))
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
.map((cleaned) => {
const sums = mapRaw[cleaned] || {};
const alumnos = (sums["alumnos"] || 0).toString();
const profesores = (sums["profesores"] || 0).toString();
const tecnicos = (sums["tecnicos"] || 0).toString();
const investigadores = (sums["investigadores"] || 0).toString();
const administrativos = (sums["administrativos"] || 0).toString();
const total =
(parseInt(alumnos) || 0) +
(parseInt(profesores) || 0) +
(parseInt(tecnicos) || 0) +
(parseInt(investigadores) || 0) +
(parseInt(administrativos) || 0);
return {
tipo: cleaned, // mostrar cleaned name (luego puedes mostrar raw si tienes map)
alumnos,
profesores,
tecnicos,
investigadores,
administrativos,
total: total.toString(),
} as ProcessorEntry;
});
// 4) concatenar fixedRows + extras, luego grand total
const allRows = [...fixedRows, ...extras];
const grandTotal: ProcessorEntry = {
tipo: "Total",
alumnos: allRows
.reduce((sum, r) => sum + (parseInt(r.alumnos || "0") || 0), 0)
.toString(),
profesores: allRows
.reduce((sum, r) => sum + (parseInt(r.profesores || "0") || 0), 0)
.toString(),
tecnicos: allRows
.reduce((sum, r) => sum + (parseInt(r.tecnicos || "0") || 0), 0)
.toString(),
investigadores: allRows
.reduce((sum, r) => sum + (parseInt(r.investigadores || "0") || 0), 0)
.toString(),
administrativos: allRows
.reduce((sum, r) => sum + (parseInt(r.administrativos || "0") || 0), 0)
.toString(),
total: allRows
.reduce((sum, r) => sum + (parseInt(r.total || "0") || 0), 0)
.toString(),
isTotal: true,
};
return [...allRows, grandTotal];
}
/* ---------- componente ---------- */
export default function Pregunta3_2() {
const [activeTab, setActiveTab] = useState<PlatformKey>("pc-desktop");
const [data, setData] = useState<PlatformData | null>(null);
const [loading, setLoading] = useState(true);
const cargarDatos = async (baja: boolean) => {
setLoading(true);
const token = Cookies.get("token");
if (!token) {
console.error("Token no encontrado");
setLoading(false);
return;
}
const body = baja ? ["BAJA"] : ["EN DESUSO", "EN USO"];
const headers = { Authorization: `Bearer ${token}` };
axios
.post(
`${process.env.NEXT_PUBLIC_API_URL}/equipos/reporte/tipoEquipos_procesador`,
body,
{ headers }
)
.then((res) => {
const json = res.data; // array con 5 arrays
const formatted: PlatformData = {
"pc-desktop": transformProcessorDataWithOfficial(
json[0] || [],
"pc-desktop"
),
"apple-desktop": transformProcessorDataWithOfficial(
json[1] || [],
"apple-desktop"
),
"pc-laptop": transformProcessorDataWithOfficial(
json[2] || [],
"pc-laptop"
),
"apple-laptop": transformProcessorDataWithOfficial(
json[3] || [],
"apple-laptop"
),
servers: transformProcessorDataWithOfficial(json[4] || [], "servers"),
};
setData(formatted);
})
.catch((err) => {
console.error("Error cargando datos de procesadores", err);
// en error, dejamos listas vacías pero con rows oficiales (0)
const empty: PlatformData = {
"pc-desktop": transformProcessorDataWithOfficial([], "pc-desktop"),
"apple-desktop": transformProcessorDataWithOfficial(
[],
"apple-desktop"
),
"pc-laptop": transformProcessorDataWithOfficial([], "pc-laptop"),
"apple-laptop": transformProcessorDataWithOfficial(
[],
"apple-laptop"
),
servers: transformProcessorDataWithOfficial([], "servers"),
};
setData(empty);
})
.finally(() => setLoading(false));
};
useEffect(() => {
cargarDatos(false);
}, []);
const currentData = data?.[activeTab] ?? [];
const isInvalidProcessor = (tipo: string, platform: PlatformKey) => {
if (tipo === "Total") return false;
const official = getOfficialProcessors(platform).map((p) =>
cleanName(p).toLowerCase()
);
return !official.includes(cleanName(tipo).toLowerCase());
};
return (
<div className={styles.container_P3}>
<div className={styles["contenedor-censo_P3"]}>
Censo de equipos de cómputo - Plataforma y tipo procesador
</div>
<div className={styles["pregunta-cuadro_P3"]}>
Cantidad de población beneficiada por plataforma y tipo de procesador.
<ToggleButton
onChange={(estado) => {
cargarDatos(estado);
}}
/>
</div>
<div className={styles.scanView_P3}>
<div className={styles.mainContent_P3}>
<div className={styles.tabs_P3}>
{Object.entries(PLATFORM_LABELS).map(([key, label]) => (
<button
key={key}
className={`${styles.tab_P3} ${
activeTab === key ? styles.active_P3 : ""
}`}
onClick={() => setActiveTab(key as PlatformKey)}
aria-selected={activeTab === key}
>
{label}
</button>
))}
</div>
<div className={styles.content_P3}>
<div className={styles.tableWrapper_P3}>
<table className={styles.table_P3}>
<thead>
<tr>
<th rowSpan={2} className={styles.headerProcesador}>
{activeTab.includes("apple")
? "Plataforma Apple"
: "Plataforma PC"}{" "}
<br />
Tipo de procesador
</th>
<th colSpan={5} className={styles.headerPoblacion}>
Población Beneficiada
</th>
<th rowSpan={2} className={styles.headerTotal}>
Total
</th>
</tr>
<tr>
<th className={styles.subHeader}>Alumnos</th>
<th className={styles.subHeader}>Profesores</th>
<th className={styles.subHeader}>Técnicos Académicos</th>
<th className={styles.subHeader}>Investigadores</th>
<th className={styles.subHeader}>Administrativos</th>
</tr>
</thead>
<tbody>
{currentData.map((item, idx) => {
const invalid = isInvalidProcessor(item.tipo, activeTab);
const rowClass = item.isTotal
? styles.totalRow_P3
: invalid
? styles.invalidRow_P3
: styles.dataRow_P3;
return (
<tr key={idx} className={rowClass}>
<td className={styles.processor_P3}>{item.tipo}</td>
<td>
{loading ? (
<div className="skeleton" />
) : (
<input
type="text"
value={item.alumnos}
readOnly
className={styles.inputBox_P3}
/>
)}
</td>
<td>
{loading ? (
<div className="skeleton" />
) : (
<input
type="text"
value={item.profesores}
readOnly
className={styles.inputBox_P3}
/>
)}
</td>
<td>
{loading ? (
<div className="skeleton" />
) : (
<input
type="text"
value={item.tecnicos}
readOnly
className={styles.inputBox_P3}
/>
)}
</td>
<td>
{loading ? (
<div className="skeleton" />
) : (
<input
type="text"
value={item.investigadores}
readOnly
className={styles.inputBox_P3}
/>
)}
</td>
<td>
{loading ? (
<div className="skeleton" />
) : (
<input
type="text"
value={item.administrativos}
readOnly
className={styles.inputBox_P3}
/>
)}
</td>
<td className={styles.totalCell_P3}>
{loading ? (
<div className="skeleton" />
) : (
item.total
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{currentData.length === 0 && (
<div className={styles.emptyState_P3}>
No hay datos disponibles para esta plataforma.
</div>
)}
</div>
</div>
</div>
</div>
);
}