added new restriction and fixed button dowload
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#169154" d="M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z"/><path fill="#18482a" d="M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z"/><path fill="#0c8045" d="M14 15.003H29V24.005000000000003H14z"/><path fill="#17472a" d="M14 24.005H29V33.055H14z"/><g><path fill="#29c27f" d="M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z"/><path fill="#27663f" d="M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z"/><path fill="#19ac65" d="M29 15.003H44V24.005000000000003H29z"/><path fill="#129652" d="M29 24.005H44V33.055H29z"/></g><path fill="#0c7238" d="M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z"/><path fill="#fff" d="M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z"/></svg>
|
||||
|
After Width: | Height: | Size: 998 B |
@@ -125,6 +125,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.barNavigation li{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.barNavigation ul {
|
||||
position: absolute;
|
||||
background-color: #f4f5f7;
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-height: 243px;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -71,7 +72,7 @@ thead th {
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
padding: 14px;
|
||||
font-size: 15px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@@ -85,7 +86,7 @@ thead th {
|
||||
tbody td {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
|
||||
@@ -373,6 +373,18 @@ export default function Page() {
|
||||
return;
|
||||
}
|
||||
|
||||
const inventario = formData.inventario;
|
||||
|
||||
if (/^0/.test(inventario)) {
|
||||
toast.error("El inventario no puede iniciar con 0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[0-9E]+$/.test(inventario)) {
|
||||
toast.error("El inventario solo puede contener números y la letra E");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = Cookies.get("token");
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
@@ -538,33 +550,30 @@ export default function Page() {
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Sistema operativo</label>
|
||||
<select
|
||||
disabled={!formData.id_tipo_equipo}
|
||||
value={formData.id_sistema_operativo}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"id_sistema_operativo",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
{formData.id_tipo_equipo
|
||||
? "Selecciona sistema operativo"
|
||||
: "Selecciona primero el tipo de equipo"}
|
||||
<div className="formGroup">
|
||||
<label>Sistema operativo</label>
|
||||
<select
|
||||
disabled={!formData.id_tipo_equipo}
|
||||
value={formData.id_sistema_operativo}
|
||||
onChange={(e) =>
|
||||
handleInputChange("id_sistema_operativo", e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
{formData.id_tipo_equipo
|
||||
? "Selecciona sistema operativo"
|
||||
: "Selecciona primero el tipo de equipo"}
|
||||
</option>
|
||||
{SO_POR_EQUIPO[formData.id_tipo_equipo]?.map((so) => (
|
||||
<option
|
||||
key={so.id_sistema_operativo}
|
||||
value={so.id_sistema_operativo}
|
||||
>
|
||||
{so.sistema_operativo}
|
||||
</option>
|
||||
{SO_POR_EQUIPO[formData.id_tipo_equipo]?.map((so) => (
|
||||
<option
|
||||
key={so.id_sistema_operativo}
|
||||
value={so.id_sistema_operativo}
|
||||
>
|
||||
{so.sistema_operativo}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
|
||||
let zxingReader: BrowserMultiFormatReader | null = null;
|
||||
let detector: any = null;
|
||||
|
||||
/** 🚀 Intentar usar BarcodeDetector API (moderno) */
|
||||
const startBarcodeDetector = async () => {
|
||||
const supported = "BarcodeDetector" in window;
|
||||
if (!supported) {
|
||||
@@ -100,13 +99,10 @@ export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔍 Intentar encontrar la cámara trasera
|
||||
const backCamera =
|
||||
devices.find((d) =>
|
||||
d.label.toLowerCase().includes("back")
|
||||
) || devices.find((d) =>
|
||||
d.label.toLowerCase().includes("rear")
|
||||
) || devices[devices.length - 1]; // fallback
|
||||
devices.find((d) => d.label.toLowerCase().includes("back")) ||
|
||||
devices.find((d) => d.label.toLowerCase().includes("rear")) ||
|
||||
devices[devices.length - 1];
|
||||
|
||||
const selectedDeviceId = backCamera.deviceId;
|
||||
|
||||
@@ -136,7 +132,6 @@ export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
|
||||
|
||||
startBarcodeDetector();
|
||||
|
||||
|
||||
return () => {
|
||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||
if (stream) stream.getTracks().forEach((t) => t.stop());
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
|
||||
import styles from "./style.module.css";
|
||||
import Cookies from "js-cookie";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function DownloadReporteXLSX() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDownload = async () => {
|
||||
const token = Cookies.get("token");
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
setLoading(true); // 🔹 Activa loader
|
||||
const token = Cookies.get("token");
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/equipos/reporteXLSX`,{headers}
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/equipos/reporteXLSX`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -31,12 +36,22 @@ export default function DownloadReporteXLSX() {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Hubo un problema al descargar el archivo.");
|
||||
} finally {
|
||||
setLoading(false); // 🔹 Reactiva botón y oculta loader
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={styles.downloadBtn} onClick={handleDownload}>
|
||||
Descargar XLSX
|
||||
<button
|
||||
className={styles.downloadBtn}
|
||||
onClick={handleDownload}
|
||||
disabled={loading} // 🔹 Botón deshabilitado
|
||||
>
|
||||
{loading ? (
|
||||
<div className={styles.loader}></div> // 🔹 Icono de cargando
|
||||
) : (
|
||||
<img src="/excel.svg" alt="Excel" className={styles.iconExcel} width={30} height={30}/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
.downloadBtn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #217346;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
gap: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom: none;
|
||||
padding: 5px 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px 10px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.downloadBtn:hover {
|
||||
background: #1e5838;
|
||||
background: #95d2b0;
|
||||
}
|
||||
|
||||
/* Icono con css puro */
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
}
|
||||
.downloadBtn:disabled {
|
||||
background: #a5a5a5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 3px solid #ffffff55;
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ const PLATFORM_OS_LIST: Record<PlatformKey, string[]> = {
|
||||
],
|
||||
|
||||
"apple-desktop": [
|
||||
"MAC OS(15 - Sequoia)",
|
||||
"MAC OS (SEQUOIA)",
|
||||
"Mac OS (13 - Ventura, 14 - Sonoma)",
|
||||
"Mac OS X (Mojave, Catalina, 11 - Big Sur, 12 - Monterrey)",
|
||||
"Mac OS X (Snow Leopard, Lion, Mountain Lion, Mavericks)",
|
||||
@@ -75,31 +75,49 @@ const PLATFORM_OS_LIST: Record<PlatformKey, string[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
// reutilizable para normalizar comparaciones
|
||||
function cleanName(str: string) {
|
||||
return (
|
||||
str
|
||||
.normalize?.("NFKD")
|
||||
.replace?.(/\u00A0/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim?.() ?? str
|
||||
);
|
||||
}
|
||||
|
||||
function transformPlatform(
|
||||
raw: RawOsEntry[],
|
||||
platform: PlatformKey
|
||||
): OsEntry[] {
|
||||
const fixedList = PLATFORM_OS_LIST[platform];
|
||||
|
||||
const clean = (str: string) =>
|
||||
str
|
||||
.normalize?.("NFKD")
|
||||
.replace?.(/\u00A0/g, " ")
|
||||
.trim?.() ?? str;
|
||||
|
||||
// mapa limpio de los datos que llegan
|
||||
const mapRaw: Record<string, number> = {};
|
||||
raw.forEach((item) => {
|
||||
const name = clean(item.sistema_operativo);
|
||||
mapRaw[name] = Number(item.total);
|
||||
const name = cleanName(item.sistema_operativo);
|
||||
mapRaw[name] = (mapRaw[name] || 0) + Number(item.total || 0);
|
||||
});
|
||||
|
||||
// 1) primero los fijos, con su conteo (0 si no vienen)
|
||||
const rows: OsEntry[] = fixedList.map((os) => ({
|
||||
os,
|
||||
count: mapRaw[clean(os)] ?? 0,
|
||||
count: mapRaw[cleanName(os)] ?? 0,
|
||||
}));
|
||||
|
||||
const total = rows.reduce((a, r) => a + r.count, 0);
|
||||
// 2) luego agregamos cualquier SO extra que vino en los datos y no esté en la lista fija
|
||||
const fixedCleanSet = new Set(fixedList.map((s) => cleanName(s)));
|
||||
const extras = Object.keys(mapRaw)
|
||||
.filter((rawName) => !fixedCleanSet.has(cleanName(rawName)))
|
||||
// opcional: mantener orden natural o por nombre
|
||||
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
|
||||
.map((name) => ({ os: name, count: mapRaw[name] }));
|
||||
|
||||
// append extras after fixed ones
|
||||
rows.push(...extras);
|
||||
|
||||
// total (incluye extras)
|
||||
const total = rows.reduce((a, r) => a + r.count, 0);
|
||||
rows.push({ os: "Total", count: total, isTotal: true });
|
||||
|
||||
return rows;
|
||||
@@ -159,6 +177,15 @@ export default function Pregunta2() {
|
||||
? data[activeTab]
|
||||
: PLATFORM_OS_LIST[activeTab].map((os) => ({ os, count: 0 }));
|
||||
|
||||
const isOsInvalid = (os: string, tab: PlatformKey) => {
|
||||
if (os === "Total") return false;
|
||||
const fixedList = PLATFORM_OS_LIST[tab];
|
||||
// comparamos con cleaned strings
|
||||
return !fixedList.some(
|
||||
(s) => cleanName(s).toLowerCase() === cleanName(os).toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container_P1}>
|
||||
<div className={styles["contenedor-censo_p1"]}>
|
||||
@@ -175,7 +202,6 @@ export default function Pregunta2() {
|
||||
</div>
|
||||
<div className={styles.scanView_P1}>
|
||||
<div className={styles["main-content_P1"]}>
|
||||
{/* Tabs */}
|
||||
<div className={styles.tabs_P1}>
|
||||
{Object.entries(PLATFORM_LABELS).map(([key, label]) => (
|
||||
<button
|
||||
@@ -198,9 +224,11 @@ export default function Pregunta2() {
|
||||
{rowsToRender.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles["data-row_P1"]} ${
|
||||
item.isTotal ? styles["total-row_P1"] : ""
|
||||
}`}
|
||||
className={`
|
||||
${styles["data-row_P1"]}
|
||||
${item.isTotal ? styles["total-row_P1"] : ""}
|
||||
${isOsInvalid(item.os, activeTab) ? styles.invalidRow : ""}
|
||||
`}
|
||||
>
|
||||
<div className={styles["os-name_P1"]}>{item.os}</div>
|
||||
<div
|
||||
|
||||
@@ -5,7 +5,9 @@ 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;
|
||||
@@ -26,6 +28,7 @@ type PlatformKey =
|
||||
|
||||
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",
|
||||
@@ -34,7 +37,7 @@ const PLATFORM_LABELS: Record<PlatformKey, string> = {
|
||||
servers: "Alto rendimiento Servidores",
|
||||
};
|
||||
|
||||
// Mapeo de "uso" del JSON a campos en ProcessorEntry
|
||||
/* ---------- mapeo uso -> campo ---------- */
|
||||
const USO_TO_FIELD: Record<
|
||||
string,
|
||||
keyof Omit<ProcessorEntry, "tipo" | "total" | "isTotal">
|
||||
@@ -46,80 +49,163 @@ const USO_TO_FIELD: Record<
|
||||
ADMINISTRATIVO: "administrativos",
|
||||
};
|
||||
|
||||
// Transforma un array plano del tipo [{procesador, uso, total}] en ProcessorEntry[]
|
||||
function transformProcessorData(
|
||||
rawArray: { procesador: string; uso: string; total: string }[]
|
||||
): ProcessorEntry[] {
|
||||
const map = new Map<string, ProcessorEntry>();
|
||||
/* ---------- 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],
|
||||
};
|
||||
|
||||
for (const item of rawArray) {
|
||||
const { procesador, uso, total } = item;
|
||||
if (!map.has(procesador)) {
|
||||
map.set(procesador, {
|
||||
tipo: procesador,
|
||||
alumnos: "0",
|
||||
profesores: "0",
|
||||
tecnicos: "0",
|
||||
investigadores: "0",
|
||||
administrativos: "0",
|
||||
total: "0",
|
||||
});
|
||||
/* ---------- 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;
|
||||
});
|
||||
}
|
||||
|
||||
const entry = map.get(procesador)!;
|
||||
const field = USO_TO_FIELD[uso];
|
||||
/* ---------- 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) {
|
||||
entry[field] = total;
|
||||
mapRaw[name][field] = (mapRaw[name][field] || 0) + prev;
|
||||
}
|
||||
}
|
||||
|
||||
// Convertimos a array y calculamos el total por fila
|
||||
const rows = Array.from(map.values()).map((entry) => {
|
||||
const totalNum =
|
||||
parseInt(entry.alumnos || "0") +
|
||||
parseInt(entry.profesores || "0") +
|
||||
parseInt(entry.tecnicos || "0") +
|
||||
parseInt(entry.investigadores || "0") +
|
||||
parseInt(entry.administrativos || "0");
|
||||
// 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 {
|
||||
...entry,
|
||||
total: totalNum.toString(),
|
||||
tipo: proc,
|
||||
alumnos,
|
||||
profesores,
|
||||
tecnicos,
|
||||
investigadores,
|
||||
administrativos,
|
||||
total: total.toString(),
|
||||
};
|
||||
});
|
||||
|
||||
// Calculamos los totales generales
|
||||
// 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: rows
|
||||
.reduce((sum, r) => sum + parseInt(r.alumnos || "0"), 0)
|
||||
alumnos: allRows
|
||||
.reduce((sum, r) => sum + (parseInt(r.alumnos || "0") || 0), 0)
|
||||
.toString(),
|
||||
profesores: rows
|
||||
.reduce((sum, r) => sum + parseInt(r.profesores || "0"), 0)
|
||||
profesores: allRows
|
||||
.reduce((sum, r) => sum + (parseInt(r.profesores || "0") || 0), 0)
|
||||
.toString(),
|
||||
tecnicos: rows
|
||||
.reduce((sum, r) => sum + parseInt(r.tecnicos || "0"), 0)
|
||||
tecnicos: allRows
|
||||
.reduce((sum, r) => sum + (parseInt(r.tecnicos || "0") || 0), 0)
|
||||
.toString(),
|
||||
investigadores: rows
|
||||
.reduce((sum, r) => sum + parseInt(r.investigadores || "0"), 0)
|
||||
investigadores: allRows
|
||||
.reduce((sum, r) => sum + (parseInt(r.investigadores || "0") || 0), 0)
|
||||
.toString(),
|
||||
administrativos: rows
|
||||
.reduce((sum, r) => sum + parseInt(r.administrativos || "0"), 0)
|
||||
administrativos: allRows
|
||||
.reduce((sum, r) => sum + (parseInt(r.administrativos || "0") || 0), 0)
|
||||
.toString(),
|
||||
total: rows
|
||||
.reduce((sum, r) => sum + parseInt(r.total || "0"), 0)
|
||||
total: allRows
|
||||
.reduce((sum, r) => sum + (parseInt(r.total || "0") || 0), 0)
|
||||
.toString(),
|
||||
isTotal: true,
|
||||
};
|
||||
|
||||
return [...rows, grandTotal];
|
||||
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");
|
||||
@@ -128,8 +214,8 @@ export default function Pregunta3_2() {
|
||||
}
|
||||
|
||||
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`,
|
||||
@@ -137,67 +223,64 @@ export default function Pregunta3_2() {
|
||||
{ headers }
|
||||
)
|
||||
.then((res) => {
|
||||
const json = res.data; // array de 5 arreglos
|
||||
const json = res.data; // array con 5 arrays
|
||||
|
||||
const formatted: PlatformData = {
|
||||
"pc-desktop": transformProcessorData(json[0] || []),
|
||||
"apple-desktop": transformProcessorData(json[1] || []),
|
||||
"pc-laptop": transformProcessorData(json[2] || []),
|
||||
"apple-laptop": transformProcessorData(json[3] || []),
|
||||
servers: transformProcessorData(json[4] || []),
|
||||
"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);
|
||||
});
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
cargarDatos(false);
|
||||
}, []);
|
||||
|
||||
if (loading)
|
||||
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}>Cargando datos...</div>
|
||||
</div>
|
||||
);
|
||||
const currentData = data?.[activeTab] ?? [];
|
||||
|
||||
if (!data)
|
||||
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}>Error al cargar los datos.</div>
|
||||
</div>
|
||||
const isInvalidProcessor = (tipo: string, platform: PlatformKey) => {
|
||||
if (tipo === "Total") return false;
|
||||
const official = getOfficialProcessors(platform).map((p) =>
|
||||
cleanName(p).toLowerCase()
|
||||
);
|
||||
|
||||
const currentData = data[activeTab];
|
||||
return !official.includes(cleanName(tipo).toLowerCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container_P3}>
|
||||
@@ -213,9 +296,9 @@ export default function Pregunta3_2() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.scanView_P3}>
|
||||
<div className={styles.mainContent_P3}>
|
||||
{/* Pestañas verticales */}
|
||||
<div className={styles.tabs_P3}>
|
||||
{Object.entries(PLATFORM_LABELS).map(([key, label]) => (
|
||||
<button
|
||||
@@ -231,97 +314,126 @@ export default function Pregunta3_2() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contenido */}
|
||||
<div className={styles.content_P3}>
|
||||
{currentData.length > 0 ? (
|
||||
<>
|
||||
<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, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={
|
||||
item.isTotal
|
||||
? styles.totalRow_P3
|
||||
: styles.dataRow_P3
|
||||
}
|
||||
>
|
||||
<td className={styles.processor_P3}>{item.tipo}</td>
|
||||
<td>
|
||||
<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>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{loading ? (
|
||||
<div className="skeleton" />
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={item.profesores}
|
||||
readOnly
|
||||
className={styles.inputBox_P3}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{loading ? (
|
||||
<div className="skeleton" />
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={item.tecnicos}
|
||||
readOnly
|
||||
className={styles.inputBox_P3}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{loading ? (
|
||||
<div className="skeleton" />
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={item.investigadores}
|
||||
readOnly
|
||||
className={styles.inputBox_P3}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{loading ? (
|
||||
<div className="skeleton" />
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={item.administrativos}
|
||||
readOnly
|
||||
className={styles.inputBox_P3}
|
||||
/>
|
||||
</td>
|
||||
<td className={styles.totalCell_P3}>{item.total}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.scanView_P1 {
|
||||
padding: 20px;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.contenedor-censo_p1 {
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.scanView_P3 {
|
||||
padding: 20px;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
@@ -44,7 +44,6 @@
|
||||
.scanView_P1 {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -150,7 +149,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invalidRow {
|
||||
background-color: rgba(223, 53, 53, 0.08) !important;
|
||||
color: #b30000;
|
||||
font-weight: 600;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.scanView_P1 .tabs_P1 {
|
||||
width: 160px;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
.scanView_P3 {
|
||||
padding: 20px;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
@@ -70,7 +70,6 @@
|
||||
|
||||
.content_P3 {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -94,7 +93,7 @@
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
font-weight: 700;
|
||||
padding: 12px 14px;
|
||||
padding: 10px;
|
||||
color: #003f80;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
text-align: center;
|
||||
@@ -102,7 +101,7 @@
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
@@ -128,7 +127,6 @@
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #f8fafc;
|
||||
font-size: 14px;
|
||||
transition: 0.2s ease;
|
||||
text-align: center;
|
||||
@@ -185,6 +183,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.invalidRow_P3 {
|
||||
background-color: rgba(255, 0, 0, 0.06) !important;
|
||||
color: #8b0000;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.scanView_P3 .mainContent_P3 {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
.tabla-contenedor {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -52,6 +52,8 @@ export const PROCESADORES_POR_EQUIPO: Record<
|
||||
{ id_procesador: 19, procesador: "Celeron Serie J, G" },
|
||||
],
|
||||
/*"ESCRITORIO MAC OS"*/ 2: [
|
||||
{ id_procesador: 32, procesador: "Familia M4" },
|
||||
{ id_procesador: 31, procesador: "Familia M3" },
|
||||
{ id_procesador: 20, procesador: "Familia M2" },
|
||||
{ id_procesador: 21, procesador: "Familia M1" },
|
||||
{ id_procesador: 2, procesador: "i7 o equivalentes (9a - 12a generación)" },
|
||||
@@ -61,7 +63,6 @@ export const PROCESADORES_POR_EQUIPO: Record<
|
||||
{ id_procesador: 8, procesador: "i5 o equivalentes (1a - 4a generación)" },
|
||||
{ id_procesador: 12, procesador: "i3 o equivalentes (1a - 4a generación)" },
|
||||
{ id_procesador: 22, procesador: "Core2 Mac, Quad Core o anteriores" },
|
||||
{ id_procesador: 22, procesador: "MAC OS(15 - Sequoia)" },
|
||||
],
|
||||
/*"ESCRITORIO LINUX"*/ 3: [
|
||||
{
|
||||
@@ -205,6 +206,8 @@ export const PROCESADORES_POR_EQUIPO: Record<
|
||||
{ id_procesador: 19, procesador: "Celeron Serie J, G" },
|
||||
],
|
||||
/*"PORTÁTILES MAC OS"*/ 6: [
|
||||
{ id_procesador: 32, procesador: "Familia M4" },
|
||||
{ id_procesador: 31, procesador: "Familia M3" },
|
||||
{ id_procesador: 20, procesador: "Familia M2" },
|
||||
{ id_procesador: 21, procesador: "Familia M1" },
|
||||
{ id_procesador: 2, procesador: "i7 o equivalentes (9a - 12a generación)" },
|
||||
|
||||
@@ -12,6 +12,10 @@ export const SO_POR_EQUIPO: Record<
|
||||
{ id_sistema_operativo: 5, sistema_operativo: "Linux" },
|
||||
],
|
||||
/*"ESCRITORIO MAC OS"*/ 2: [
|
||||
{
|
||||
id_sistema_operativo: 13,
|
||||
sistema_operativo: "MAC OS (SEQUOIA)",
|
||||
},
|
||||
{
|
||||
id_sistema_operativo: 6,
|
||||
sistema_operativo: "Mac OS (13 - Ventura, 14 - Sonoma)",
|
||||
@@ -91,7 +95,7 @@ export const SO_POR_EQUIPO: Record<
|
||||
"Linux (CentOS, Fedora, Ubuntu, Red Hat Enterprise, entre otros)",
|
||||
},
|
||||
{
|
||||
id_sistema_operativo: 13,
|
||||
id_sistema_operativo: 11,
|
||||
sistema_operativo: "Unix (AIX, MAC OS Server, Solaris, entre otros)",
|
||||
},
|
||||
{ id_sistema_operativo: 14, sistema_operativo: "Windows Server 2022/2023" },
|
||||
|
||||
Reference in New Issue
Block a user