31 Commits

Author SHA1 Message Date
IO e271e2414e new fond 2026-04-23 20:11:29 -05:00
IO 84e29d4a20 added new picture 2026-04-23 11:38:33 -05:00
IO 10d92b97cd added new image Carlos 2026-04-22 19:25:32 -05:00
IO 78e62295e1 added new icons 2026-04-21 17:57:40 -05:00
IO f5a4f66715 adding new icons and new style of header 2026-04-21 17:37:22 -05:00
Tyrannuss f7297a2f64 se cambiaron los filtros para reportes por servicio y recibo para recibir solo parametros de año y mees ademas se elimino la parte de filtro por mes en inscritos 2026-04-06 15:20:36 -06:00
Tyrannuss bb90e40906 reporte por mes y año 2026-03-26 18:43:54 -06:00
IO 4ab004c5ea Merge branch 'Axel' of repositorio.acatlan.unam.mx:IO/front-AT 2026-03-26 17:27:51 -05:00
IO 4070d73eae package 2026-03-25 20:46:25 -05:00
IO 6a5f15e247 package 2026-03-25 20:45:40 -05:00
IO 45c3ba3076 Merge branch 'Carlos' of repositorio.acatlan.unam.mx:IO/front-AT into develop 2026-03-20 17:51:33 -05:00
IO c4c06d9ed6 fixed modified machines' programs 2026-03-20 17:50:15 -05:00
frcarlos 22753e9d84 stylebitacoramesas 2026-03-20 18:50:06 -04:00
Tyrannuss 1f29e72ad2 se modifico el reporte de inscritos para poder buscar por rango ademas de agregar la columna del periodo y porder descargar un excel 2026-03-19 18:33:56 -06:00
IO edd4e6f0c0 merge axel 2026-03-18 18:33:11 -05:00
IO a7bcddb759 Merge branch 'Axel' of repositorio.acatlan.unam.mx:IO/front-AT into develop 2026-03-18 18:32:53 -05:00
Tyrannuss 4d727d1766 se agregaron graficas a los periodos ademas de poder descargar un excel del intervalo de periodos 2026-03-18 15:45:51 -06:00
IO 329a1b0faf new format fecha to sanction 2026-03-18 15:30:21 -05:00
IO c60ddf0797 added new boton to refresh monitor and modified search machine and merge Carlos 2026-03-17 09:37:14 -05:00
IO 68d61446c3 Merge branch 'Carlos' of repositorio.acatlan.unam.mx:IO/front-AT 2026-03-17 08:12:43 -05:00
IO 5b65a4795f Merge branch 'Lino' 2026-03-10 17:50:04 -05:00
IO e294d226de Merge branch 'Axel' of repositorio.acatlan.unam.mx:IO/front-AT into Lino 2026-03-10 17:49:24 -05:00
IO 2fc0267360 package 2026-03-10 17:48:26 -05:00
Tyrannuss 05f6ea24a7 Se modifico la tabla eliminando la columna de genero cambiandola por femenino y masculino 2026-03-10 16:37:25 -06:00
frcarlos 9a81c800fd CREAR USUARIO 2026-03-05 14:38:52 -05:00
IO 07f67a6ac5 Merge branch 'Carlos' of https://repositorio.acatlan.unam.mx/IO/front-AT into Carlos 2026-03-04 17:36:16 -06:00
IO 2faf13ee2c Merge branch 'master' of https://repositorio.acatlan.unam.mx/IO/front-AT into Carlos 2026-03-04 17:35:58 -06:00
IO 625084572e merge Carlos 2026-03-04 17:01:41 -06:00
frcarlos f9aae13241 go sweet alert 2026-03-04 17:07:44 -05:00
frcarlos 52423dbac2 swetalert usuarios 2026-03-04 15:53:33 -05:00
frcarlos 4f2ef1dd6e function UserPage 2026-03-04 13:55:55 -05:00
35 changed files with 5908 additions and 199 deletions
+29 -8
View File
@@ -27,6 +27,8 @@ export default function PlaticaGate({ inscripcion, numAcount }: Props) {
const [bitacora, setBitacora] = useState<[] | null>([]); const [bitacora, setBitacora] = useState<[] | null>([]);
const [error, setError] = useState<string>("Equipo"); const [error, setError] = useState<string>("Equipo");
const [search, setSearch] = useState("");
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
@@ -148,6 +150,16 @@ export default function PlaticaGate({ inscripcion, numAcount }: Props) {
} }
}; };
const equiposFiltrados = equipos.filter((eq) => {
const texto = search.toLowerCase();
return (
eq.ubicacion?.toString().toLowerCase().includes(texto) ||
eq.plataforma?.nombre?.toLowerCase().includes(texto) ||
eq.nombre_equipo?.toLowerCase().includes(texto)
);
});
if (bitacora) { if (bitacora) {
return error === "Mesa" return error === "Mesa"
? <p style={{ margin: "1rem", color: "red" }}>Ya cuentas con una mesa asignada</p> ? <p style={{ margin: "1rem", color: "red" }}>Ya cuentas con una mesa asignada</p>
@@ -181,12 +193,20 @@ export default function PlaticaGate({ inscripcion, numAcount }: Props) {
> >
<div <div
className={`selectHeader ${equipoSeleccionado && equipoSeleccionado.plataforma?.nombre?.toUpperCase()}`} className={`selectHeader ${equipoSeleccionado && equipoSeleccionado.plataforma?.nombre?.toUpperCase()}`}
onClick={() => !loadingEquipos && setOpen(!open)} onClick={() => !loadingEquipos && setOpen(!open)} >
> {open ? (
{equipoSeleccionado ? ( <input
autoFocus
type="text"
placeholder="Buscar equipo..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
className="inputSelect"
/>
) : equipoSeleccionado ? (
<span> <span>
{equipoSeleccionado.ubicacion}{" "} {equipoSeleccionado.ubicacion} {equipoSeleccionado.nombre_equipo}
{equipoSeleccionado.nombre_equipo}
</span> </span>
) : ( ) : (
<span className="placeholder"> <span className="placeholder">
@@ -201,19 +221,20 @@ export default function PlaticaGate({ inscripcion, numAcount }: Props) {
{open && ( {open && (
<div className="options"> <div className="options">
{equipos.length === 0 && ( {equiposFiltrados.length === 0 && (
<div className="option disabled"> <div className="option disabled">
No hay equipos disponibles No hay resultados
</div> </div>
)} )}
{equipos.map((eq) => ( {equiposFiltrados.map((eq) => (
<div <div
key={eq.id_equipo} key={eq.id_equipo}
className={`option ${eq.plataforma?.nombre?.toUpperCase()}`} className={`option ${eq.plataforma?.nombre?.toUpperCase()}`}
onClick={() => { onClick={() => {
setEquipoSeleccionado(eq); setEquipoSeleccionado(eq);
setOpen(false); setOpen(false);
setSearch("");
}} }}
> >
{eq.ubicacion} {eq.nombre_equipo} {eq.ubicacion} {eq.nombre_equipo}
+14 -1
View File
@@ -64,7 +64,8 @@
.selectHeader { .selectHeader {
border-radius: 6px; border-radius: 6px;
padding: 8px 12px; padding: 0px 6px;
height: 35px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -92,3 +93,15 @@
max-height: 250px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
} }
.inputSelect{
border: none;
padding: 0 5px;
margin: 0;
}
.inputSelect:focus{
background-color: #fff;
outline: none;
box-shadow:none;
}
+34 -26
View File
@@ -13,11 +13,13 @@ type Mesa = {
export default function TableMesas() { export default function TableMesas() {
const [mesas, setMesas] = useState<Mesa[]>([]); const [mesas, setMesas] = useState<Mesa[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refresh, setRefresh] = useState<boolean>(false)
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
const fetchMesas = async () => { const fetchMesas = async () => {
setLoading(true);
try { try {
const res = await axios.get( const res = await axios.get(
`${process.env.NEXT_PUBLIC_API_URL}/mesa/usoWhitout`, `${process.env.NEXT_PUBLIC_API_URL}/mesa/usoWhitout`,
@@ -34,37 +36,43 @@ export default function TableMesas() {
useEffect(() => { useEffect(() => {
fetchMesas(); fetchMesas();
}, []); setRefresh(false)
}, [refresh]);
if (loading) { if (loading) {
return <div className={styles.tableContainer}> return (
<table className={styles.machineTable}> <div className={styles.tableContainer}>
<thead> <table className={styles.machineTable}>
<tr> <thead>
<th>Cargando mesas...</th> <tr>
</tr> <th>Cargando mesas...</th>
</thead> </tr>
</table> </thead>
</div> </table>
</div>
)
} }
return ( return (
<div className={styles.tableContainer}> <>
<table className={styles.machineTable}> <button className="button buttonCancel" onClick={() => { setRefresh(true) }}>Actualizar informacion</button>
<thead> <div className={styles.tableContainer}>
<tr> <table className={styles.machineTable}>
<th>ID Mesa</th> <thead>
</tr> <tr>
</thead> <th>ID Mesa</th>
<tbody>
{mesas.map((mesa) => (
<tr key={mesa.id_mesa} className={!mesa.ocupado ? '' : styles.ocupado}>
<td>{mesa.id_mesa}</td>
</tr> </tr>
))} </thead>
</tbody>
</table> <tbody>
</div> {mesas.map((mesa) => (
<tr key={mesa.id_mesa} className={!mesa.ocupado ? '' : styles.ocupado}>
<td>{mesa.id_mesa}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
); );
} }
@@ -16,11 +16,11 @@ export default function InformacionEquipo({
defaultKey?: string; defaultKey?: string;
}) { }) {
const [id, setId] = useState<number | null>(null); const [id, setId] = useState<number | null>(null);
const [ubicacion, setUbicacion] = useState<string | null>(null); const [ubicacion, setUbicacion] = useState<number | null>(null);
return ( return (
<section className="containerSection"> <section className="containerSection">
<h2 className="title"> INFORMACION DE EQUIPOS </h2> <h2 className="title"> </h2>
<Toggle <Toggle
defaultView={defaultKey} defaultView={defaultKey}
+209 -62
View File
@@ -3,96 +3,197 @@ import { envConfig } from "@/app/lib/config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import "./inscripciones.css";
import "./inscripciones.css" import * as XLSX from "xlsx";
import { saveAs } from "file-saver";
interface Periodo { interface Periodo {
id_periodo: number; id_periodo: number;
semestre: string; semestre: string;
fecha_inicio_servicio: string;
} }
interface ApiResponse { interface ApiResponse {
semestre: string;
carrera: string; carrera: string;
genero: string | null;
profesor: string; profesor: string;
femenino: string;
masculino: string;
total: string; total: string;
} }
type Tipo = "periodo" | "mes" | "anio";
type TipoConteo = "WINDOWS" | "MACINTOSH" | "LINUX" | "PROFESORES";
interface TablaRow { interface TablaRow {
carrera: string; carrera: string;
genero: string | null; semestre: string;
WINDOWS: number; WINDOWS: number;
MACINTOSH: number; MACINTOSH: number;
LINUX: number; LINUX: number;
PROFESORES: number; PROFESORES: number;
TOTAL: number; TOTAL: number;
femenino: number;
masculino: number;
} }
type TipoConteo = "WINDOWS" | "MACINTOSH" | "LINUX" | "PROFESORES";
export default function Inscripciones() { export default function Inscripciones() {
const [periodo, setPeriodo] = useState<Periodo[]>([]); const [periodo, setPeriodo] = useState<Periodo[]>([]);
const [selectedPeriodo, setSelectedPeriodo] = useState<number | null>(null); const [tipo, setTipo] = useState<Tipo>("periodo");
const [periodoInicio, setPeriodoInicio] = useState<any>(null);
const [periodoFin, setPeriodoFin] = useState<any>(null);
const [anio, setAnio] = useState<number | null>(null);
const [tablaData, setTablaData] = useState<TablaRow[]>([]); const [tablaData, setTablaData] = useState<TablaRow[]>([]);
const [rawData, setRawData] = useState<ApiResponse[]>([]);
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
// Obtener periodos
useEffect(() => { useEffect(() => {
const getPeriodo = async () => { const getPeriodo = async () => {
const response = await axios.get(`${envConfig.apiUrl}/periodo`, { headers } const response = await axios.get(`${envConfig.apiUrl}/periodo`, { headers });
);
setPeriodo(response.data); setPeriodo(response.data);
}; };
getPeriodo(); getPeriodo();
}, []); }, []);
// Obtener años desde fechas
const aniosDisponibles = Array.from(
new Set(
periodo.map((p) =>
new Date(p.fecha_inicio_servicio).getFullYear()
)
)
).sort();
// ============================
// 🔍 BUSCAR
// ============================
const handleBuscar = async (e: React.FormEvent) => { const handleBuscar = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!selectedPeriodo) return; let url = "";
const response = await axios.get( if (tipo === "periodo") {
`${envConfig.apiUrl}/alumno-inscrito/inscritos/${selectedPeriodo}`, if (!periodoInicio || !periodoFin) {
{ headers } alert("Selecciona ambos periodos");
return;
);
const data: ApiResponse[] = response.data;
const agrupado: Record<string, TablaRow> = {};
data.forEach((item) => {
const generoNormalizado =
item.genero === "F" || item.genero === "Femenino"
? "Femenino"
: item.genero === "M" || item.genero === "Masculino"
? "Masculino"
: "-";
const key = `${item.carrera}_${generoNormalizado}`;
if (!agrupado[key]) {
agrupado[key] = {
carrera: item.carrera,
genero: generoNormalizado,
WINDOWS: 0,
MACINTOSH: 0,
LINUX: 0,
PROFESORES: 0,
TOTAL: 0,
};
} }
const tipo = item.profesor as TipoConteo; url = `${envConfig.apiUrl}/alumno-inscrito/inscritos/periodo/${periodoInicio}/${periodoFin}`;
const cantidad = Number(item.total); }
agrupado[key][tipo] += cantidad; if (tipo === "anio") {
agrupado[key].TOTAL += cantidad; if (!periodoInicio || !periodoFin) {
alert("Selecciona ambos años");
return;
}
url = `${envConfig.apiUrl}/alumno-inscrito/inscritos/anio/${periodoInicio}/${periodoFin}`;
}
try {
console.log("URL:", url);
const response = await axios.get(url, { headers });
const data: ApiResponse[] = response.data;
setRawData(data);
const agrupado: Record<string, TablaRow> = {};
data.forEach((item) => {
const key = `${item.carrera}-${item.semestre}`;
if (!agrupado[key]) {
agrupado[key] = {
carrera: item.carrera,
semestre: item.semestre,
WINDOWS: 0,
MACINTOSH: 0,
LINUX: 0,
PROFESORES: 0,
TOTAL: 0,
femenino: 0,
masculino: 0,
};
}
const fila = agrupado[key];
const tipoProfesor = item.profesor as TipoConteo;
const total = Number(item.total);
const fem = Number(item.femenino);
const masc = Number(item.masculino);
fila[tipoProfesor] += total;
fila.TOTAL += total;
fila.femenino += fem;
fila.masculino += masc;
});
setTablaData(Object.values(agrupado));
} catch (error) {
console.error("Error al obtener inscritos", error);
}
};
const exportarExcel = () => {
if (!rawData.length) return;
const periodosUnicos = Array.from(
new Set(rawData.map((t) => t.semestre))
);
const carrerasUnicas = Array.from(
new Set(rawData.map((t) => t.carrera))
);
const dataExcel = carrerasUnicas.map((carrera) => {
const fila: any = { Carrera: carrera };
let totalFila = 0;
periodosUnicos.forEach((periodo) => {
const items = rawData.filter(
(t) => t.carrera === carrera && t.semestre === periodo
);
const totalPeriodo = items.reduce(
(acc, item) => acc + Number(item.total),
0
);
fila[periodo] = totalPeriodo;
totalFila += totalPeriodo;
});
fila["Total"] = totalFila;
return fila;
}); });
setTablaData(Object.values(agrupado)); const worksheet = XLSX.utils.json_to_sheet(dataExcel);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Inscritos");
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, "reporte_inscritos.xlsx");
}; };
return ( return (
@@ -100,35 +201,79 @@ export default function Inscripciones() {
<h2 className="title">INSCRITOS</h2> <h2 className="title">INSCRITOS</h2>
<form className="containerForm" onSubmit={handleBuscar}> <form className="containerForm" onSubmit={handleBuscar}>
<label>Seleccione el periodo</label> <label>Filtro</label>
{/* Selector tipo */}
<select value={tipo} onChange={(e) => setTipo(e.target.value as Tipo)}>
<option value="periodo">Periodo</option>
<option value="anio">Año</option>
</select>
<div className="groupInput"> <div className="groupInput">
<select {/* PERIODO */}
onChange={(e) => setSelectedPeriodo(Number(e.target.value))} {tipo === "periodo" && (
defaultValue="" <>
> <select onChange={(e) => setPeriodoInicio(Number(e.target.value))}>
<option value="" disabled> <option value="">Inicio</option>
Seleccione {periodo.map((p) => (
</option> <option key={p.id_periodo} value={p.id_periodo}>
{p.semestre}
</option>
))}
</select>
{periodo.map((p) => ( <select onChange={(e) => setPeriodoFin(Number(e.target.value))}>
<option key={p.id_periodo} value={p.id_periodo}> <option value="">Fin</option>
{p.semestre} {periodo.map((p) => (
</option> <option key={p.id_periodo} value={p.id_periodo}>
))} {p.semestre}
</select> </option>
))}
</select>
</>
)}
{/* AÑO */}
{tipo === "anio" && (
<>
<select onChange={(e) => setPeriodoInicio(Number(e.target.value))}>
<option value="">Año inicio</option>
{aniosDisponibles.map((a) => (
<option key={a} value={a}>
{a}
</option>
))}
</select>
<select onChange={(e) => setPeriodoFin(Number(e.target.value))}>
<option value="">Año fin</option>
{aniosDisponibles.map((a) => (
<option key={a} value={a}>
{a}
</option>
))}
</select>
</>
)}
<button className="button buttonSearch" type="submit"> <button className="button buttonSearch" type="submit">
Buscar Buscar
</button> </button>
<button type="button" className="button buttonSearch" onClick={exportarExcel}>
Exportar Excel
</button>
</div> </div>
</form> </form>
{/* TABLA */}
<div className="containerTable"> <div className="containerTable">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Carrera</th> <th>Carrera</th>
<th>Genero</th> <th>Periodo</th>
<th>Femenino</th>
<th>Masculino</th>
<th>WINDOWS</th> <th>WINDOWS</th>
<th>MACINTOSH</th> <th>MACINTOSH</th>
<th>LINUX</th> <th>LINUX</th>
@@ -140,8 +285,10 @@ export default function Inscripciones() {
<tbody> <tbody>
{tablaData.map((row, index) => ( {tablaData.map((row, index) => (
<tr key={index}> <tr key={index}>
<td className="textLeft">{row.carrera}</td> <td>{row.carrera}</td>
<td>{row.genero ?? "-"}</td> <td>{row.semestre}</td>
<td>{row.femenino}</td>
<td>{row.masculino}</td>
<td>{row.WINDOWS}</td> <td>{row.WINDOWS}</td>
<td>{row.MACINTOSH}</td> <td>{row.MACINTOSH}</td>
<td>{row.LINUX}</td> <td>{row.LINUX}</td>
+17
View File
@@ -0,0 +1,17 @@
"use client"
import { useRouter } from "next/navigation"
import styles from "./Page.module.css"
export default function RefreshButton() {
const router = useRouter()
return (
<button
className={styles.resetButton}
onClick={() => router.refresh()}
>
Actualizar información
</button>
)
}
+4 -1
View File
@@ -4,6 +4,7 @@ import DSC from "./DSC";
import ToggleTable from "@/app/Components/Global/Toggle/ToggleTable"; import ToggleTable from "@/app/Components/Global/Toggle/ToggleTable";
import TableTable from "./TableTable"; import TableTable from "./TableTable";
import { Metadata } from "next"; import { Metadata } from "next";
import RefreshButton from "./RefreshButton";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Monitor", title: "Monitor",
@@ -21,9 +22,11 @@ export default async function Page(props: {
<h2 className="title">MONITOR</h2> <h2 className="title">MONITOR</h2>
<div className={styles.actions}> <div className={styles.actions}>
<button className={styles.resetButton}>Actualizar información</button> <RefreshButton />
</div> </div>
<ToggleTable <ToggleTable
key={Date.now()}
defaultView={key} defaultView={key}
options={[ options={[
{ {
+162 -37
View File
@@ -4,7 +4,19 @@ import { useState, useEffect } from "react";
import { envConfig } from "@/app/lib/config"; import { envConfig } from "@/app/lib/config";
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { Metadata } from "next"; import * as XLSX from "xlsx";
import { saveAs } from "file-saver";
import {
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
Line,
LineChart,
} from "recharts";
interface TotalServicioPeriodo { interface TotalServicioPeriodo {
nombre_servicio: string; nombre_servicio: string;
@@ -27,7 +39,6 @@ export default function ReporteTotalesPage() {
const [servicios, setServicios] = useState<Servicio[]>([]); const [servicios, setServicios] = useState<Servicio[]>([]);
const [periodos, setPeriodos] = useState<Periodo[]>([]); const [periodos, setPeriodos] = useState<Periodo[]>([]);
const [cargando, setCargando] = useState(true); const [cargando, setCargando] = useState(true);
const [periodoInicio, setPeriodoInicio] = useState<string>(""); const [periodoInicio, setPeriodoInicio] = useState<string>("");
const [periodoFin, setPeriodoFin] = useState<string>(""); const [periodoFin, setPeriodoFin] = useState<string>("");
const [servicioId, setServicioId] = useState<string>(""); const [servicioId, setServicioId] = useState<string>("");
@@ -35,6 +46,77 @@ export default function ReporteTotalesPage() {
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
// TOOLTIP ORDENADO
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const sorted = [...payload]
.filter((item) => item.value > 0)
.sort((a, b) => b.value - a.value);
return (
<div style={{ background: "white", padding: "10px", border: "1px solid #ccc", borderRadius: "8px" }}>
<p style={{ fontWeight: "bold" }}>Periodo: {label}</p>
{sorted.map((entry: any, index: number) => (
<div key={index} style={{ color: entry.color }}>
{entry.name}: ${entry.value.toFixed(2)}
</div>
))}
</div>
);
}
return null;
};
// EXPORTAR EXCEL PIVOT
const exportarExcelPivot = () => {
if (!totales.length) return;
const periodosUnicos = Array.from(new Set(totales.map((t) => t.periodo))).sort();
const serviciosUnicos = Array.from(new Set(totales.map((t) => t.nombre_servicio)));
const dataExcel = serviciosUnicos.map((servicio) => {
const fila: any = { Servicio: servicio };
periodosUnicos.forEach((periodo) => {
const item = totales.find(
(t) => t.nombre_servicio === servicio && t.periodo === periodo
);
fila[periodo] = item ? item.monto_total : 0;
});
// total por fila
fila["Total"] = periodosUnicos.reduce(
(acc, periodo) => acc + (fila[periodo] || 0),
0
);
return fila;
});
const worksheet = XLSX.utils.json_to_sheet(dataExcel);
worksheet["!cols"] = [
{ wch: 30 },
...periodosUnicos.map(() => ({ wch: 15 })),
{ wch: 15 },
];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Pivot");
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const data = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(data, "reporte_periodo.xlsx");
};
// FETCH
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
@@ -45,7 +127,7 @@ export default function ReporteTotalesPage() {
setServicios(serviciosRes.data); setServicios(serviciosRes.data);
setPeriodos(periodosRes.data); setPeriodos(periodosRes.data);
} catch (error) { } catch (error) {
console.error("Error al cargar servicios o periodos", error); console.error("Error al cargar datos", error);
} }
}; };
fetchData(); fetchData();
@@ -54,7 +136,6 @@ export default function ReporteTotalesPage() {
const obtenerTotales = async () => { const obtenerTotales = async () => {
setCargando(true); setCargando(true);
try { try {
// Construir query params
const params = new URLSearchParams(); const params = new URLSearchParams();
if (periodoInicio) params.append("periodoInicio", periodoInicio); if (periodoInicio) params.append("periodoInicio", periodoInicio);
if (periodoFin) params.append("periodoFin", periodoFin); if (periodoFin) params.append("periodoFin", periodoFin);
@@ -74,16 +155,38 @@ export default function ReporteTotalesPage() {
obtenerTotales(); obtenerTotales();
}; };
// DATA PARA GRAFICA
const transformDataForChart = () => {
const periodosUnicos = Array.from(new Set(totales.map((t) => t.periodo))).sort();
const serviciosUnicos = Array.from(new Set(totales.map((t) => t.nombre_servicio)));
const data = periodosUnicos.map((periodo) => {
const row: any = { periodo };
serviciosUnicos.forEach((servicio) => {
const item = totales.find(
(t) => t.periodo === periodo && t.nombre_servicio === servicio
);
row[servicio] = item ? item.monto_total : 0;
});
return row;
});
return { data, serviciosUnicos };
};
const { data: chartData, serviciosUnicos } = transformDataForChart();
const colors = ["black", "red", "blue", "orange", "purple", "green"];
return ( return (
<div> <div>
<h1 style={{ color: "rgb(3, 1, 72)" }}>REPORTE DE TOTALES POR SERVICIO Y PERIODO</h1> <h1 style={{ color: "rgb(3, 1, 72)", marginBottom: "20px" }}>
REPORTE DE TOTALES POR SERVICIO Y PERIODO
</h1>
<div style={{ marginBottom: "20px", display: "flex", gap: "10px", flexWrap: "wrap" }}> <div style={{ marginBottom: "20px", display: "flex", gap: "10px", flexWrap: "wrap" }}>
<select <select value={periodoInicio} onChange={(e) => setPeriodoInicio(e.target.value)}>
value={periodoInicio} <option value="">Periodo inicio</option>
onChange={(e) => setPeriodoInicio(e.target.value)}
>
<option value="">Periodo inicio (todos)</option>
{periodos.map((p) => ( {periodos.map((p) => (
<option key={p.id_periodo} value={p.id_periodo}> <option key={p.id_periodo} value={p.id_periodo}>
{p.semestre} {p.semestre}
@@ -91,11 +194,8 @@ export default function ReporteTotalesPage() {
))} ))}
</select> </select>
<select <select value={periodoFin} onChange={(e) => setPeriodoFin(e.target.value)}>
value={periodoFin} <option value="">Periodo fin</option>
onChange={(e) => setPeriodoFin(e.target.value)}
>
<option value="">Periodo fin (todos)</option>
{periodos.map((p) => ( {periodos.map((p) => (
<option key={p.id_periodo} value={p.id_periodo}> <option key={p.id_periodo} value={p.id_periodo}>
{p.semestre} {p.semestre}
@@ -103,10 +203,7 @@ export default function ReporteTotalesPage() {
))} ))}
</select> </select>
<select <select value={servicioId} onChange={(e) => setServicioId(e.target.value)}>
value={servicioId}
onChange={(e) => setServicioId(e.target.value)}
>
<option value="">Todos los servicios</option> <option value="">Todos los servicios</option>
{servicios.map((s) => ( {servicios.map((s) => (
<option key={s.id_servicio} value={s.id_servicio}> <option key={s.id_servicio} value={s.id_servicio}>
@@ -116,30 +213,58 @@ export default function ReporteTotalesPage() {
</select> </select>
<button onClick={aplicarFiltros}>Filtrar</button> <button onClick={aplicarFiltros}>Filtrar</button>
<button onClick={exportarExcelPivot}>Exportar Excel</button>
</div> </div>
{cargando ? ( {cargando ? (
<p>Cargando...</p> <p>Cargando...</p>
) : ( ) : (
<div style={{ overflow: "auto", maxHeight: "500px" }}> <div style={{ display: "flex", gap: "50px", flexWrap: "wrap" }}>
<table>
<thead> {/* TABLA */}
<tr> <div style={{ flex: 1, minWidth: "500px", maxHeight: "500px", overflow: "auto" }}>
<th>Servicio</th> <table style={{ width: "100%" }}>
<th>Periodo</th> <thead>
<th>Monto Total</th> <tr>
</tr> <th>Servicio</th>
</thead> <th>Periodo</th>
<tbody> <th>Monto</th>
{totales.map((item, index) => (
<tr key={index}>
<td>{item.nombre_servicio}</td>
<td>{item.periodo}</td>
<td>${item.monto_total.toFixed(2)}</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {totales.map((item, i) => (
<tr key={i}>
<td>{item.nombre_servicio}</td>
<td>{item.periodo}</td>
<td>${item.monto_total.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* GRAFICA */}
<div style={{ flex: 1, minWidth: "700px", background: "white" }}>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="periodo" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
{serviciosUnicos.map((servicio, i) => (
<Line
key={servicio}
dataKey={servicio}
stroke={colors[i % colors.length]}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div> </div>
)} )}
</div> </div>
+5 -4
View File
@@ -5,6 +5,7 @@ import Toggle from "@/app/Components/Global/Toggle/Toggle";
import SearchDateBetween from "@/app/Components/SearchDateBetween/SearchDateBetween"; import SearchDateBetween from "@/app/Components/SearchDateBetween/SearchDateBetween";
import PorServicios from "@/app/Components/Reportes/porServicio"; import PorServicios from "@/app/Components/Reportes/porServicio";
import PorRecibos from "@/app/Components/Reportes/porRecibo"; import PorRecibos from "@/app/Components/Reportes/porRecibo";
import MonthYearPicker from "@/app/Components/MonthYearPicker/MonthYearPicker";
export default function ReportesClient({ export default function ReportesClient({
defaultKey, defaultKey,
@@ -26,8 +27,8 @@ export default function ReportesClient({
label: "Por recibo", label: "Por recibo",
content: ( content: (
<> <>
<SearchDateBetween <MonthYearPicker
onSearch={(d, h) => { onChange={(d, h) => {
setDesde(d); setDesde(d);
setHasta(h); setHasta(h);
}} }}
@@ -41,8 +42,8 @@ export default function ReportesClient({
label: "Por Servicio", label: "Por Servicio",
content: ( content: (
<> <>
<SearchDateBetween <MonthYearPicker
onSearch={(d, h) => { onChange={(d, h) => {
setDesde(d); setDesde(d);
setHasta(h); setHasta(h);
}} }}
+234
View File
@@ -0,0 +1,234 @@
"use client";
import { useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import { envConfig } from "@/app/lib/config";
import Swal from "sweetalert2";
import "./user.css"
interface Perfil {
id_perfil: number;
perfil: string;
}
interface Usuario {
id_usuario: number;
nombre: string;
apellido_paterno: string;
apellido_materno: string;
usuario: string;
activo: number;
descripcion: string;
perfil: Perfil;
}
export default function UserPage() {
const [usuarios, setUsuarios] = useState<Usuario[]>([]);
const [perfiles, setPerfiles] = useState<Perfil[]>([]);
const [nombre, setNombre] = useState("");
const [apellidoPaterno, setApellidoPaterno] = useState("");
const [apellidoMaterno, setApellidoMaterno] = useState("");
const [usuario, setUsuario] = useState("");
const [password, setPassword] = useState("");
const [descripcion, setDescripcion] = useState("");
const [idPerfil, setIdPerfil] = useState<number | "">("");
const [activo, setActivo] = useState(true);
const API = `${envConfig.apiUrl}/user`;
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
const obtenerUsuarios = async () => {
try {
const res = await axios.get(API, { headers });
setUsuarios(res.data);
} catch (error) {
console.error("Error al obtener usuarios", error);
}
};
const obtenerPerfiles = async () => {
try {
const res = await axios.get(`${API}/perfil`, { headers });
setPerfiles(res.data);
} catch (error) {
console.error("Error al obtener perfiles", error);
Swal.fire({
title: "Error al obtener perfiles!",
icon: "error",
draggable: true
});
}
};
useEffect(() => {
obtenerUsuarios();
obtenerPerfiles();
}, []);
const crearUsuario = async () => {
if (!nombre || !apellidoPaterno || !usuario || !password || !idPerfil) {
Swal.fire({
title: "Todos los campos obligatorios deben llenarse.!",
icon: "error",
draggable: true
});
return;
}
try {
await axios.post(
`${API}/create`,
{
nombre,
apellido_paterno: apellidoPaterno,
apellido_materno: apellidoMaterno,
usuario,
password,
descripcion,
activo: activo ? 1 : 0,
id_perfil: idPerfil,
},
{ headers }
);
limpiarFormulario();
obtenerUsuarios();
} catch (error) {
console.error("Error al crear usuario", error);
Swal.fire({
title: "Error al crear usuario.!",
icon: "error",
draggable: true
});
}
};
const limpiarFormulario = () => {
setNombre("");
setApellidoPaterno("");
setApellidoMaterno("");
setUsuario("");
setPassword("");
setDescripcion("");
setIdPerfil("");
setActivo(true);
};
const active = async (id_usuario: number) => {
try {
await axios.patch(
`${API}/${id_usuario}/activo`,
{},
{ headers }
);
setUsuarios((prev) =>
prev.map((u) =>
u.id_usuario === id_usuario
? { ...u, activo: u.activo === 1 ? 0 : 1 }
: u
)
);
} catch (error) {
console.error("Error al cambiar estado del usuario", error);
Swal.fire({
title: "Error al cambiar estado del usuario",
icon: "error",
draggable: true
});
}
};
return (
<div>
<h1 style={{ color: "rgb(3, 1, 72)" }}>CREAR USUARIO</h1>
<div style={{ marginBottom: "10px" }}>
<input placeholder="Nombre" value={nombre} onChange={(e) => setNombre(e.target.value)} />
<input placeholder="Apellido Paterno" value={apellidoPaterno} onChange={(e) => setApellidoPaterno(e.target.value)} />
<input placeholder="Apellido Materno" value={apellidoMaterno} onChange={(e) => setApellidoMaterno(e.target.value)} />
<input placeholder="Usuario" value={usuario} onChange={(e) => setUsuario(e.target.value)} />
<input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
<input placeholder="Descripción" value={descripcion} onChange={(e) => setDescripcion(e.target.value)} />
<select value={idPerfil} onChange={(e) => setIdPerfil(Number(e.target.value))}>
<option value="">Seleccione Perfil</option>
{perfiles.map((p) => (
<option key={p.id_perfil} value={p.id_perfil}>
{p.perfil}
</option>
))}
</select>
<label className="user">
Activo
<input
type="checkbox"
checked={activo}
onChange={(e) => setActivo(e.target.checked)}
/>
</label>
<button className="button buttonSearch" onClick={crearUsuario}>
Crear Usuario
</button>
</div>
<div style={{ overflow: "auto", maxHeight: "350px" }}>
<table>
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Usuario</th>
<th>Perfil</th>
<th>Activo</th>
</tr>
</thead>
<tbody>
{usuarios.map((u) => (
<tr key={u.id_usuario}>
<td>{u.id_usuario}</td>
<td>{u.nombre} {u.apellido_paterno} </td>
<td>{u.usuario}</td>
<td>{u.perfil?.perfil}</td>
<td style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span>{u.activo === 1 ? "Sí" : "No"}</span>
<input
type="checkbox"
checked={u.activo === 1}
style={{ height: "2rem" }}
onChange={() => active(u.id_usuario)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
.user{
padding-left: 20px;
margin-left: 180px;
font-size: 25px;;
}
@@ -21,6 +21,8 @@ export default function BitacoraEquipo({ date }: { date: string | null }) {
const [equipoSeleccionado, setEquipoSeleccionado] = useState<any>(null); const [equipoSeleccionado, setEquipoSeleccionado] = useState<any>(null);
const [equipos, setEquipos] = useState<any[]>([]); const [equipos, setEquipos] = useState<any[]>([]);
const [search, setSearch] = useState("");
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
@@ -52,6 +54,15 @@ export default function BitacoraEquipo({ date }: { date: string | null }) {
}); });
}, [equipoSeleccionado, date]); }, [equipoSeleccionado, date]);
const equiposFiltrados = equipos.filter((eq) => {
const texto = search.toLowerCase();
return (
eq.ubicacion?.toString().toLowerCase().includes(texto) ||
eq.plataforma?.nombre?.toLowerCase().includes(texto) ||
eq.nombre_equipo?.toLowerCase().includes(texto)
);
});
return ( return (
<> <>
@@ -64,13 +75,20 @@ export default function BitacoraEquipo({ date }: { date: string | null }) {
> >
<div <div
className={`selectHeader ${equipoSeleccionado && equipoSeleccionado.plataforma?.nombre?.toUpperCase()}`} className={`selectHeader ${equipoSeleccionado && equipoSeleccionado.plataforma?.nombre?.toUpperCase()}`}
onClick={() => !loadingEquipos && setOpen(!open)} onClick={() => !loadingEquipos && setOpen(!open)} >
> {open ? (
{equipoSeleccionado ? ( <input
<span autoFocus
> type="text"
{equipoSeleccionado.ubicacion}{" "} placeholder="Buscar equipo..."
{equipoSeleccionado.nombre_equipo} value={search}
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
className="inputSelect"
/>
) : equipoSeleccionado ? (
<span>
{equipoSeleccionado.ubicacion} {equipoSeleccionado.nombre_equipo}
</span> </span>
) : ( ) : (
<span className="placeholder"> <span className="placeholder">
@@ -85,19 +103,20 @@ export default function BitacoraEquipo({ date }: { date: string | null }) {
{open && ( {open && (
<div className="options" style={{ bottom: "auto" }}> <div className="options" style={{ bottom: "auto" }}>
{equipos.length === 0 && ( {equiposFiltrados.length === 0 && (
<div className="option disabled"> <div className="option disabled">
No hay equipos disponibles No hay resultados
</div> </div>
)} )}
{equipos.map((eq) => ( {equiposFiltrados.map((eq) => (
<div <div
key={eq.id_equipo} key={eq.id_equipo}
className={`option ${eq.plataforma?.nombre?.toUpperCase()}`} className={`option ${eq.plataforma?.nombre?.toUpperCase()}`}
onClick={() => { onClick={() => {
setEquipoSeleccionado(eq); setEquipoSeleccionado(eq);
setOpen(false); setOpen(false);
setSearch("");
}} }}
> >
{eq.ubicacion} {eq.nombre_equipo} {eq.ubicacion} {eq.nombre_equipo}
@@ -42,8 +42,8 @@ export default function BitacoraMesas() {
<> <>
<SearchDate /> <SearchDate />
<div> <div style={{ maxHeight: "400px", overflowY: "auto", border: "1px solid #ccc" }}>
<table> <table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead> <thead>
<tr> <tr>
<th>Mesa</th> <th>Mesa</th>
@@ -77,4 +77,3 @@ export default function BitacoraMesas() {
</> </>
); );
} }
//IO
@@ -10,11 +10,30 @@ export default function TableSanction({ alumnoSanciones, alumno }: Props) {
const calcularFechaFin = (fechaInicio: string, duracionSemanas: number) => { const calcularFechaFin = (fechaInicio: string, duracionSemanas: number) => {
const fecha = new Date(fechaInicio); const fecha = new Date(fechaInicio);
fecha.setDate(fecha.getDate() + duracionSemanas * 7); fecha.setDate(fecha.getDate() + duracionSemanas * 7);
return fecha.toLocaleDateString();
return fecha.toLocaleString("es-MX", {
timeZone: "America/Mexico_City",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
});
}; };
const formatearFechaMexico = (fechaISO: string) => {
return new Date(fechaISO).toLocaleString("es-MX", {
timeZone: "America/Mexico_City",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
};
return ( return (
<div style={{ margin: "1rem 0", maxWidth:"730px"}}> <div style={{ margin: "1rem 0", maxWidth: "730px" }}>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -32,8 +51,7 @@ export default function TableSanction({ alumnoSanciones, alumno }: Props) {
<td>{alumno?.id_cuenta}</td> <td>{alumno?.id_cuenta}</td>
<td>{item.sancion.sancion}</td> <td>{item.sancion.sancion}</td>
<td>{item.sancion.duracion}</td> <td>{item.sancion.duracion}</td>
<td>{new Date(item.fecha_inicio).toLocaleDateString()}</td> <td>{formatearFechaMexico(item.fecha_inicio)}</td> <td>
<td>
{calcularFechaFin(item.fecha_inicio, item.sancion.duracion)} {calcularFechaFin(item.fecha_inicio, item.sancion.duracion)}
</td> </td>
</tr> </tr>
+5
View File
@@ -84,6 +84,11 @@ export default function Equipos() {
if (!data) return; if (!data) return;
try { try {
console.log({
id_equipo: data.id_equipo,
id_plataforma: idPlataforma,
nombre
});
await axios.patch(`${envConfig.apiUrl}/equipo`, { await axios.patch(`${envConfig.apiUrl}/equipo`, {
id_equipo: data.id_equipo, id_equipo: data.id_equipo,
nombre_equipo: nombre, nombre_equipo: nombre,
@@ -14,7 +14,7 @@ export default function ProgramSelector({
ubicacion, ubicacion,
id, id,
}: { }: {
ubicacion?: string | null; ubicacion?: number | null;
id?: number | null; id?: number | null;
}) { }) {
const [programas, setProgramas] = useState<Programa[]>([]); const [programas, setProgramas] = useState<Programa[]>([]);
@@ -56,7 +56,7 @@ export default function ProgramSelector({
.map(Number); .map(Number);
try { try {
if (ubicacion && ubicacion !== "0") { if (ubicacion) {
await axios.patch( await axios.patch(
`${envConfig.apiUrl}/programa-equipo/equipo/${ubicacion}`, `${envConfig.apiUrl}/programa-equipo/equipo/${ubicacion}`,
{ {
@@ -121,7 +121,7 @@ export default function ProgramSelector({
try { try {
let res; let res;
if (ubicacion && ubicacion !== "0") { if (ubicacion) {
res = await axios.get( res = await axios.get(
`${envConfig.apiUrl}/programa-equipo/${ubicacion}`, `${envConfig.apiUrl}/programa-equipo/${ubicacion}`,
{ headers }, { headers },
@@ -21,7 +21,7 @@ interface Equipos {
} }
interface Props { interface Props {
onSearch: (ubicacion: string) => void; onSearch: (id_equipo: number) => void;
} }
export default function SelectorEquipo({ onSearch }: Props) { export default function SelectorEquipo({ onSearch }: Props) {
@@ -35,6 +35,7 @@ export default function SelectorEquipo({ onSearch }: Props) {
const [loadingEquipos, setLoadingEquipos] = useState(false); const [loadingEquipos, setLoadingEquipos] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const token = Cookies.get("token"); const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${token}` };
@@ -56,11 +57,20 @@ export default function SelectorEquipo({ onSearch }: Props) {
const eq = equipos.find(e => e.id_equipo === paramEquipo); const eq = equipos.find(e => e.id_equipo === paramEquipo);
if (eq) { if (eq) {
setSelected(eq); setSelected(eq);
onSearch(eq.ubicacion); onSearch(eq.id_equipo);
} }
} }
}, [paramEquipo, equipos]); }, [paramEquipo, equipos]);
const equiposFiltrados = equipos.filter((eq) => {
const texto = search.toLowerCase();
return (
eq.ubicacion?.toString().toLowerCase().includes(texto) ||
eq.plataforma?.nombre?.toLowerCase().includes(texto) ||
eq.nombre_equipo?.toLowerCase().includes(texto)
);
});
return ( return (
<form className="form-container"> <form className="form-container">
@@ -71,14 +81,20 @@ export default function SelectorEquipo({ onSearch }: Props) {
> >
<div <div
className={`selectHeader ${selected && selected.plataforma?.nombre?.toUpperCase()}`} className={`selectHeader ${selected && selected.plataforma?.nombre?.toUpperCase()}`}
onClick={() => !loadingEquipos && setOpen(!open)} onClick={() => !loadingEquipos && setOpen(!open)} >
> {open ? (
{selected ? ( <input
<span autoFocus
> type="text"
{selected.ubicacion}{" "} placeholder="Buscar equipo..."
{selected.nombre_equipo}{" "} value={search}
{selected.areaUbicacion.area} onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
className="inputSelect"
/>
) : selected ? (
<span>
{selected.ubicacion} {selected.nombre_equipo}
</span> </span>
) : ( ) : (
<span className="placeholder"> <span className="placeholder">
@@ -93,13 +109,13 @@ export default function SelectorEquipo({ onSearch }: Props) {
{open && ( {open && (
<div className="options" style={{ bottom: "auto" }}> <div className="options" style={{ bottom: "auto" }}>
{equipos.length === 0 && ( {equiposFiltrados.length === 0 && (
<div className="option disabled"> <div className="option disabled">
No hay equipos disponibles No hay equipos disponibles
</div> </div>
)} )}
{equipos.map((eq) => ( {equiposFiltrados.map((eq) => (
<div <div
key={eq.id_equipo} key={eq.id_equipo}
className={`option ${eq.plataforma?.nombre?.toUpperCase()}`} className={`option ${eq.plataforma?.nombre?.toUpperCase()}`}
@@ -115,6 +131,8 @@ export default function SelectorEquipo({ onSearch }: Props) {
} }
router.push(`${pathname}?${params.toString()}`); router.push(`${pathname}?${params.toString()}`);
setSearch("");
}} }}
> >
{eq.ubicacion} {eq.nombre_equipo} {eq.areaUbicacion.area} {eq.ubicacion} {eq.nombre_equipo} {eq.areaUbicacion.area}
@@ -0,0 +1,75 @@
"use client";
import { useEffect, useState } from "react";
interface Props {
onChange: (desde: string, hasta: string) => void;
initialYear?: number;
}
export default function MonthYearPicker({
onChange,
initialYear,
}: Props) {
const currentYear = new Date().getFullYear();
const [mes, setMes] = useState<number | null>(null);
const [anio, setAnio] = useState<number>(
initialYear || currentYear
);
// Generar rango automáticamente
useEffect(() => {
if (mes !== null && anio !== null) {
const inicio = new Date(anio, mes, 1);
const fin = new Date(anio, mes + 1, 0);
const formato = (fecha: Date) =>
fecha.toISOString().split("T")[0];
onChange(formato(inicio), formato(fin));
}
}, [mes, anio, onChange]);
return (
<div className="flex gap-3 mb-5">
{/* MES */}
<select
className="border rounded-lg px-3 py-2"
onChange={(e) => setMes(Number(e.target.value))}
defaultValue=""
>
<option value="" disabled>
Selecciona mes
</option>
<option value={0}>Enero</option>
<option value={1}>Febrero</option>
<option value={2}>Marzo</option>
<option value={3}>Abril</option>
<option value={4}>Mayo</option>
<option value={5}>Junio</option>
<option value={6}>Julio</option>
<option value={7}>Agosto</option>
<option value={8}>Septiembre</option>
<option value={9}>Octubre</option>
<option value={10}>Noviembre</option>
<option value={11}>Diciembre</option>
</select>
<select
className="border rounded-lg px-3 py-2"
value={anio}
onChange={(e) => setAnio(Number(e.target.value))}
>
{Array.from(
{ length: currentYear - 2000 + 1 },
(_, i) => 2000 + i
).map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</div>
);
}
+1 -2
View File
@@ -46,5 +46,4 @@ export default function SearchDate() {
</form> </form>
); );
} }
//IO
//By Tyrannuss
@@ -8,6 +8,7 @@ const images = [
"/CEDETEC.jpg", "/CEDETEC.jpg",
"/FES-ACATLAN.jpg", "/FES-ACATLAN.jpg",
"/DSC.jpeg", "/DSC.jpeg",
"/Cedetec.jpeg",
]; ];
export default function BackgroundRotator() { export default function BackgroundRotator() {
@@ -109,6 +109,8 @@
color: #545454; color: #545454;
font-size: 1.4rem; font-size: 1.4rem;
padding: 1rem; padding: 1rem;
display: flex;
gap: 10px;
} }
.subMenu:hover>span, .subMenu:hover>span,
@@ -116,6 +118,17 @@
color: black; color: black;
} }
.subMenu.active span {
color: black;
font-weight: bold;
}
.subMenu.active {
border-radius: 8px;
background-color: #f3f4f6;
}
.containerLinks { .containerLinks {
padding: 0 !important; padding: 0 !important;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
@@ -150,7 +163,7 @@ tbody {
} }
} }
@media (max-width: 1000px) { @media (max-width: 1100px) {
.menuToggle { .menuToggle {
display: flex; display: flex;
position: relative; position: relative;
@@ -1,12 +1,21 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation";
import Image from "next/image";
import "./BarNavigation.css"; import "./BarNavigation.css";
export default function BarNavigation() { export default function BarNavigation() {
const [openMenu, setOpenMenu] = useState(false); const [openMenu, setOpenMenu] = useState(false);
const [openSubMenu, setOpenSubMenu] = useState<number | null>(null); const [openSubMenu, setOpenSubMenu] = useState<number | null>(null);
const pathname = usePathname();
const isActive = (paths: string[]) => {
return paths.some((path) => pathname.startsWith(path));
};
const toggleMenu = () => setOpenMenu(!openMenu); const toggleMenu = () => setOpenMenu(!openMenu);
const toggleSubMenu = (index: number) => { const toggleSubMenu = (index: number) => {
if (typeof window !== "undefined" && window.innerWidth <= 1000) { if (typeof window !== "undefined" && window.innerWidth <= 1000) {
@@ -28,8 +37,20 @@ export default function BarNavigation() {
</div> </div>
<ul className={openMenu ? "active" : ""}> <ul className={openMenu ? "active" : ""}>
<li className={`subMenu ${openSubMenu === 0 ? "open" : ""}`}> <li
<span onClick={() => toggleSubMenu(0)}>Inscripciones</span> className={`subMenu
${openSubMenu === 0 ? "open" : ""}
${isActive(["/Inscripcion", "/AgregarTiempo"]) ? "active" : ""}
`}>
<span onClick={() => toggleSubMenu(0)}>
<Image
src="/user-plus.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Inscripciones</span>
<ul onClick={toggleMenu}> <ul onClick={toggleMenu}>
<Link <Link
href="/Inscripcion" href="/Inscripcion"
@@ -54,8 +75,20 @@ export default function BarNavigation() {
</ul> </ul>
</li> </li>
<li className={`subMenu ${openSubMenu === 1 ? "open" : ""}`}> <li
<span onClick={() => toggleSubMenu(1)}>Servicios</span> className={`subMenu
${openSubMenu === 1 ? "open" : ""}
${isActive(["/Impresiones", "/AsignacionMesas", "/AsignacionEquipo", "/Monitor"]) ? "active" : ""}
`}>
<span onClick={() => toggleSubMenu(1)}>
<Image
src="/briefcase.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Servicios</span>
<ul onClick={toggleMenu}> <ul onClick={toggleMenu}>
<Link <Link
href="/Impresiones" href="/Impresiones"
@@ -100,8 +133,20 @@ export default function BarNavigation() {
</ul> </ul>
</li> </li>
<li className={`subMenu ${openSubMenu === 2 ? "open" : ""}`}> <li
<span onClick={() => toggleSubMenu(2)}>Equipo</span> className={`subMenu
${openSubMenu === 2 ? "open" : ""}
${isActive(["/InformacionEquipo", "/ActivosMantenimiento", "/Mensajes", "/Programas"]) ? "active" : ""}
`}>
<span onClick={() => toggleSubMenu(2)}>
<Image
src="/computer.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Equipo</span>
<ul onClick={toggleMenu}> <ul onClick={toggleMenu}>
<Link <Link
href="/InformacionEquipo" href="/InformacionEquipo"
@@ -146,8 +191,21 @@ export default function BarNavigation() {
</ul> </ul>
</li> </li>
<li className={`subMenu ${openSubMenu === 3 ? "open" : ""}`}> <li
<span onClick={() => toggleSubMenu(3)}>Reportes</span> className={`subMenu
${openSubMenu === 3 ? "open" : ""}
${isActive(["/Reportes", "/Inscritos"]) ? "active" : ""}
`}>
<span onClick={() => toggleSubMenu(3)}>
<Image
src="/chart.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Reportes</span>
<ul onClick={toggleMenu}> <ul onClick={toggleMenu}>
<Link <Link
href="/Reportes" href="/Reportes"
@@ -171,7 +229,11 @@ export default function BarNavigation() {
</Link> </Link>
</ul> </ul>
</li> </li>
<li className="subMenu" onClick={toggleMenu}> <li
className={`subMenu
${isActive(["/BitacoraSanciones"]) ? "active" : ""}
`}
onClick={toggleMenu}>
<Link <Link
href="/BitacoraSanciones" href="/BitacoraSanciones"
className="links" className="links"
@@ -180,10 +242,22 @@ export default function BarNavigation() {
closeAllMenus(); closeAllMenus();
}} }}
> >
<span>Bitácora y sanciones</span> <span>
<Image
src="/clipboard.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Bitácora y sanciones</span>
</Link> </Link>
</li> </li>
<li className="subMenu" onClick={toggleMenu}> <li
className={`subMenu
${isActive(["/CambiarPass"]) ? "active" : ""}
`}
onClick={toggleMenu}>
<Link <Link
href="/CambiarPass" href="/CambiarPass"
className="links" className="links"
@@ -192,7 +266,15 @@ export default function BarNavigation() {
closeAllMenus(); closeAllMenus();
}} }}
> >
<span> Cambiar contraseña</span> <span>
<Image
src="/key.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Cambiar contraseña</span>
</Link> </Link>
</li> </li>
</ul> </ul>
@@ -1,12 +1,21 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation";
import Image from "next/image";
import "./BarNavigation.css"; import "./BarNavigation.css";
export default function BarNavigationServicio() { export default function BarNavigationServicio() {
const [openMenu, setOpenMenu] = useState(false); const [openMenu, setOpenMenu] = useState(false);
const [openSubMenu, setOpenSubMenu] = useState<number | null>(null); const [openSubMenu, setOpenSubMenu] = useState<number | null>(null);
const pathname = usePathname();
const isActive = (paths: string[]) => {
return paths.some((path) => pathname.startsWith(path));
};
const toggleMenu = () => setOpenMenu(!openMenu); const toggleMenu = () => setOpenMenu(!openMenu);
const toggleSubMenu = (index: number) => { const toggleSubMenu = (index: number) => {
if (typeof window !== "undefined" && window.innerWidth <= 1000) { if (typeof window !== "undefined" && window.innerWidth <= 1000) {
@@ -28,8 +37,20 @@ export default function BarNavigationServicio() {
</div> </div>
<ul className={openMenu ? "active" : ""}> <ul className={openMenu ? "active" : ""}>
<li className={`subMenu ${openSubMenu === 0 ? "open" : ""}`}> <li
<span onClick={() => toggleSubMenu(0)}>Inscripciones</span> className={`subMenu
${openSubMenu === 0 ? "open" : ""}
${isActive(["/Inscripcion", "/AgregarTiempo"]) ? "active" : ""}
`}>
<span onClick={() => toggleSubMenu(0)}>
<Image
src="/user-plus.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Inscripciones</span>
<ul onClick={toggleMenu}> <ul onClick={toggleMenu}>
<Link <Link
href="/Inscripcion" href="/Inscripcion"
@@ -54,8 +75,20 @@ export default function BarNavigationServicio() {
</ul> </ul>
</li> </li>
<li className={`subMenu ${openSubMenu === 1 ? "open" : ""}`}> <li
<span onClick={() => toggleSubMenu(1)}>Servicios</span> className={`subMenu
${openSubMenu === 1 ? "open" : ""}
${isActive(["/Impresiones", "/AsignacionMesas", "/AsignacionEquipo", "/Monitor"]) ? "active" : ""}
`}>
<span onClick={() => toggleSubMenu(1)}>
<Image
src="/briefcase.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Servicios</span>
<ul onClick={toggleMenu}> <ul onClick={toggleMenu}>
<Link <Link
href="/Impresiones" href="/Impresiones"
@@ -113,7 +146,10 @@ export default function BarNavigationServicio() {
</Link> </Link>
</li> */} </li> */}
<li className="subMenu" onClick={toggleMenu}> <li
className={`subMenu
${isActive(["/ActivosMantenimiento"]) ? "active" : ""}
`} onClick={toggleMenu}>
<Link <Link
href="/ActivosMantenimiento" href="/ActivosMantenimiento"
className="links" className="links"
@@ -122,7 +158,15 @@ export default function BarNavigationServicio() {
closeAllMenus(); closeAllMenus();
}} }}
> >
<span>Activos y en Mantenimiento</span> <span>
<Image
src="/computer.svg"
alt="Logo FES"
width={16}
height={16}
color="#545454"
/>
Activos y en Mantenimiento</span>
</Link> </Link>
</li> </li>
@@ -1,7 +1,7 @@
.link { .link {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 10px 5px; margin: 5px 0px;
} }
.logo { .logo {
@@ -42,7 +42,7 @@
left: -20px; left: -20px;
width: 100%; width: 100%;
height: 30px; height: 30px;
background: linear-gradient(to right, #bd8c01 40%, #fff 100%); background: linear-gradient(to right, #015cb8 40%, #fff 100%);
transform: skew(-45deg); transform: skew(-45deg);
z-index: 0; z-index: 0;
} }
@@ -128,7 +128,7 @@
@media (max-width: 800px) { @media (max-width: 800px) {
.yellowPart { .yellowPart {
left: 0; left: 0;
background: #bd8c01; background: #015cb8;
transform: skew(0deg); transform: skew(0deg);
} }
+22
View File
@@ -31,6 +31,28 @@ body {
main { main {
position: relative; position: relative;
overflow: hidden;
}
main::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 200%;
background-image:
linear-gradient(rgba(0, 0, 0, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.025) 1px, transparent 1px);
background-size: 48px 48px;
pointer-events: none;
transform: translate(-50%, -50%) rotate(15deg);
transform-origin: center;
} }
footer { footer {
+4829
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.11.0", "axios": "^1.11.0",
"date": "^2.0.6",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jose": "^6.1.3", "jose": "^6.1.3",
@@ -19,8 +20,10 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"recharts": "^3.8.0",
"sweetalert2": "^11.26.18", "sweetalert2": "^11.26.18",
"sweetalert2-react-content": "^5.1.1" "sweetalert2-react-content": "^5.1.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
+2
View File
@@ -42,6 +42,7 @@ export async function proxy(request: NextRequest) {
"/Monitor", "/Monitor",
"/ActivosMantenimiento", "/ActivosMantenimiento",
"/Periodo", "/Periodo",
"/Usuarios",
]; ];
if (role === 5 && !onlyImpresiones.includes(pathname)) { if (role === 5 && !onlyImpresiones.includes(pathname)) {
@@ -67,6 +68,7 @@ export const config = {
matcher: [ matcher: [
"/", "/",
"/Reportes", "/Reportes",
"/Usuarios",
"/Periodo", "/Periodo",
"/Monitor", "/Monitor",
"/QuitarSancion", "/QuitarSancion",
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M264 112L376 112C380.4 112 384 115.6 384 120L384 160L256 160L256 120C256 115.6 259.6 112 264 112zM208 120L208 160L128 160C92.7 160 64 188.7 64 224L64 320L576 320L576 224C576 188.7 547.3 160 512 160L432 160L432 120C432 89.1 406.9 64 376 64L264 64C233.1 64 208 89.1 208 120zM576 368L384 368L384 384C384 401.7 369.7 416 352 416L288 416C270.3 416 256 401.7 256 384L256 368L64 368L64 480C64 515.3 92.7 544 128 544L512 544C547.3 544 576 515.3 576 480L576 368z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M96 96C113.7 96 128 110.3 128 128L128 464C128 472.8 135.2 480 144 480L544 480C561.7 480 576 494.3 576 512C576 529.7 561.7 544 544 544L144 544C99.8 544 64 508.2 64 464L64 128C64 110.3 78.3 96 96 96zM192 160C192 142.3 206.3 128 224 128L416 128C433.7 128 448 142.3 448 160C448 177.7 433.7 192 416 192L224 192C206.3 192 192 177.7 192 160zM224 240L352 240C369.7 240 384 254.3 384 272C384 289.7 369.7 304 352 304L224 304C206.3 304 192 289.7 192 272C192 254.3 206.3 240 224 240zM224 352L480 352C497.7 352 512 366.3 512 384C512 401.7 497.7 416 480 416L224 416C206.3 416 192 401.7 192 384C192 366.3 206.3 352 224 352z"/></svg>

After

Width:  |  Height:  |  Size: 839 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M439.4 96L448 96C483.3 96 512 124.7 512 160L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 160C128 124.7 156.7 96 192 96L200.6 96C211.6 76.9 232.3 64 256 64L384 64C407.7 64 428.4 76.9 439.4 96zM376 176C389.3 176 400 165.3 400 152C400 138.7 389.3 128 376 128L264 128C250.7 128 240 138.7 240 152C240 165.3 250.7 176 264 176L376 176zM256 320C256 302.3 241.7 288 224 288C206.3 288 192 302.3 192 320C192 337.7 206.3 352 224 352C241.7 352 256 337.7 256 320zM288 320C288 333.3 298.7 344 312 344L424 344C437.3 344 448 333.3 448 320C448 306.7 437.3 296 424 296L312 296C298.7 296 288 306.7 288 320zM288 448C288 461.3 298.7 472 312 472L424 472C437.3 472 448 461.3 448 448C448 434.7 437.3 424 424 424L312 424C298.7 424 288 434.7 288 448zM224 480C241.7 480 256 465.7 256 448C256 430.3 241.7 416 224 416C206.3 416 192 430.3 192 448C192 465.7 206.3 480 224 480z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M380.8 96C372.7 110.1 368 126.5 368 144L368 160L96 160L96 384L368 384L368 448L96 448C60.7 448 32 419.3 32 384L32 160C32 124.7 60.7 96 96 96L380.8 96zM368 496C368 513.5 372.7 529.9 380.8 544L152 544C138.7 544 128 533.3 128 520C128 506.7 138.7 496 152 496L368 496zM464 96L560 96C586.5 96 608 117.5 608 144L608 496C608 522.5 586.5 544 560 544L464 544C437.5 544 416 522.5 416 496L416 144C416 117.5 437.5 96 464 96zM488 160C474.7 160 464 170.7 464 184C464 197.3 474.7 208 488 208L536 208C549.3 208 560 197.3 560 184C560 170.7 549.3 160 536 160L488 160zM488 256C474.7 256 464 266.7 464 280C464 293.3 474.7 304 488 304L536 304C549.3 304 560 293.3 560 280C560 266.7 549.3 256 536 256L488 256zM544 400C544 382.3 529.7 368 512 368C494.3 368 480 382.3 480 400C480 417.7 494.3 432 512 432C529.7 432 544 417.7 544 400z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M400 416C497.2 416 576 337.2 576 240C576 142.8 497.2 64 400 64C302.8 64 224 142.8 224 240C224 258.7 226.9 276.8 232.3 293.7L71 455C66.5 459.5 64 465.6 64 472L64 552C64 565.3 74.7 576 88 576L168 576C181.3 576 192 565.3 192 552L192 512L232 512C245.3 512 256 501.3 256 488L256 448L296 448C302.4 448 308.5 445.5 313 441L346.3 407.7C363.2 413.1 381.3 416 400 416zM440 160C462.1 160 480 177.9 480 200C480 222.1 462.1 240 440 240C417.9 240 400 222.1 400 200C400 177.9 417.9 160 440 160z"/></svg>

After

Width:  |  Height:  |  Size: 710 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M285.7 368C384.2 368 464 447.8 464 546.3C464 562.7 450.7 576 434.3 576L77.7 576C61.3 576 48 562.7 48 546.3C48 447.8 127.8 368 226.3 368L285.7 368zM528 144C541.3 144 552 154.7 552 168L552 216L600 216C613.3 216 624 226.7 624 240C624 253.3 613.3 264 600 264L552 264L552 312C552 325.3 541.3 336 528 336C514.7 336 504 325.3 504 312L504 264L456 264C442.7 264 432 253.3 432 240C432 226.7 442.7 216 456 216L504 216L504 168C504 154.7 514.7 144 528 144zM256 312C189.7 312 136 258.3 136 192C136 125.7 189.7 72 256 72C322.3 72 376 125.7 376 192C376 258.3 322.3 312 256 312z"/></svg>

After

Width:  |  Height:  |  Size: 792 B