447 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|