Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2afe92991 | |||
| edc56138d5 | |||
| 97f2a8851d | |||
| 1bdde40409 | |||
| 286075da19 | |||
| 3dfc092e98 | |||
| c2e392f3d8 | |||
| 35245a9195 | |||
| e271e2414e | |||
| 84e29d4a20 | |||
| 10d92b97cd | |||
| 78e62295e1 | |||
| f5a4f66715 | |||
| c4f68aa1e0 | |||
| f7297a2f64 | |||
| bb90e40906 | |||
| 4ab004c5ea | |||
| 4070d73eae | |||
| 6a5f15e247 | |||
| 45c3ba3076 | |||
| c4c06d9ed6 | |||
| 1f29e72ad2 |
@@ -0,0 +1,85 @@
|
|||||||
|
name: Despliegue Automatizado Universal
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-context:
|
||||||
|
runs-on: host
|
||||||
|
outputs:
|
||||||
|
node_version: ${{ steps.setup.outputs.version }}
|
||||||
|
target_app: ${{ steps.setup.outputs.app }}
|
||||||
|
steps:
|
||||||
|
- name: Resolver Aplicación desde apps.conf
|
||||||
|
id: setup
|
||||||
|
run: |
|
||||||
|
# 1. Obtener el nombre del repositorio actual en Gitea (ej: front-Censo)
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
|
# 2. Determinar si buscamos un entorno -dev (develop) o producción (master)
|
||||||
|
if [ "${{ github.ref_name }}" = "develop" ]; then
|
||||||
|
SUFFIX="-dev"
|
||||||
|
else
|
||||||
|
SUFFIX=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Buscando qué sección de apps.conf tiene REPO=$REPO_NAME y termina en '$SUFFIX'..."
|
||||||
|
|
||||||
|
# 3. Buscar en /etc/apps.conf el bloque que contenga REPO=nombre_del_repo
|
||||||
|
TARGET_APP=$(awk -v repo="REPO=$REPO_NAME" -v suf="$SUFFIX" '
|
||||||
|
/^\[/ { gsub(/[\[\]]/, "", $0); current_box=$0; next }
|
||||||
|
$0 == repo {
|
||||||
|
if ((suf == "-dev" && current_box ~ /-dev$/) || (suf == "" && current_box !~ /-dev$/)) {
|
||||||
|
print current_box;
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' /etc/apps.conf)
|
||||||
|
|
||||||
|
if [ -z "$TARGET_APP" ]; then
|
||||||
|
echo "ERROR: No se encontró ninguna sección en /etc/apps.conf para el repositorio $REPO_NAME con el sufijo correcto."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Leer la versión de Node del bloque encontrado
|
||||||
|
VERSION=$(awk -v app="$TARGET_APP" '
|
||||||
|
$0=="["app"]" { found=1; next }
|
||||||
|
/^\[/ && found { exit }
|
||||||
|
found && /^NODE=/ { split($0, a, "="); print a[2]; exit }
|
||||||
|
' /etc/apps.conf)
|
||||||
|
|
||||||
|
MAJOR_VERSION=$(echo "$VERSION" | tr -d '[:space:]' | grep -oE '[0-9]+' | head -n1)
|
||||||
|
|
||||||
|
echo "-> ID de Aplicación encontrado: $TARGET_APP"
|
||||||
|
echo "-> Versión Mayor de Node resuelta: $MAJOR_VERSION"
|
||||||
|
|
||||||
|
echo "app=${TARGET_APP}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${MAJOR_VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: get-context
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configurar Node.js dinámicamente
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ needs.get-context.outputs.node_version }}
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: [get-context, build]
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: Ejecutar Despliegue en Servidor Físico
|
||||||
|
run: |
|
||||||
|
TARGET_APP="${{ needs.get-context.outputs.target_app }}"
|
||||||
|
BRANCH_NAME="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
sudo -u deploy /home/deploy/scripts/deploy-front.sh "$TARGET_APP" "$BRANCH_NAME"
|
||||||
@@ -16,7 +16,7 @@ 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">
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ 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;
|
||||||
profesor: string;
|
profesor: string;
|
||||||
femenino: string;
|
femenino: string;
|
||||||
@@ -19,11 +22,13 @@ interface ApiResponse {
|
|||||||
total: string;
|
total: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Tipo = "periodo" | "mes" | "anio";
|
||||||
|
|
||||||
type TipoConteo = "WINDOWS" | "MACINTOSH" | "LINUX" | "PROFESORES";
|
type TipoConteo = "WINDOWS" | "MACINTOSH" | "LINUX" | "PROFESORES";
|
||||||
|
|
||||||
interface TablaRow {
|
interface TablaRow {
|
||||||
carrera: string;
|
carrera: string;
|
||||||
|
semestre: string;
|
||||||
WINDOWS: number;
|
WINDOWS: number;
|
||||||
MACINTOSH: number;
|
MACINTOSH: number;
|
||||||
LINUX: number;
|
LINUX: number;
|
||||||
@@ -35,13 +40,22 @@ interface TablaRow {
|
|||||||
|
|
||||||
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 [periodosUnicos, setPeriodosUnicos] = useState<string[]>([]);
|
||||||
|
const [tablaResumen, setTablaResumen] = useState<any[]>([]);
|
||||||
|
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 });
|
||||||
@@ -50,29 +64,59 @@ export default function Inscripciones() {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = `${envConfig.apiUrl}/alumno-inscrito/inscritos/periodo/${periodoInicio}/${periodoFin}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tipo === "anio") {
|
||||||
|
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;
|
const data: ApiResponse[] = response.data;
|
||||||
|
|
||||||
|
setRawData(data);
|
||||||
|
|
||||||
const agrupado: Record<string, TablaRow> = {};
|
const agrupado: Record<string, TablaRow> = {};
|
||||||
|
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
const carrera = item.carrera;
|
const key = `${item.carrera}-${item.semestre}`;
|
||||||
|
|
||||||
|
if (!agrupado[key]) {
|
||||||
if (!agrupado[carrera]) {
|
agrupado[key] = {
|
||||||
agrupado[carrera] = {
|
carrera: item.carrera,
|
||||||
carrera: carrera,
|
semestre: item.semestre,
|
||||||
WINDOWS: 0,
|
WINDOWS: 0,
|
||||||
MACINTOSH: 0,
|
MACINTOSH: 0,
|
||||||
LINUX: 0,
|
LINUX: 0,
|
||||||
@@ -83,20 +127,105 @@ export default function Inscripciones() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fila = agrupado[carrera];
|
const fila = agrupado[key];
|
||||||
const tipoProfesor = item.profesor as TipoConteo;
|
const tipoProfesor = item.profesor as TipoConteo;
|
||||||
const totalPlataforma = Number(item.total);
|
|
||||||
|
const total = Number(item.total);
|
||||||
const fem = Number(item.femenino);
|
const fem = Number(item.femenino);
|
||||||
const masc = Number(item.masculino);
|
const masc = Number(item.masculino);
|
||||||
|
|
||||||
|
fila[tipoProfesor] += total;
|
||||||
fila[tipoProfesor] += totalPlataforma;
|
fila.TOTAL += total;
|
||||||
fila.TOTAL += totalPlataforma;
|
|
||||||
fila.femenino += fem;
|
fila.femenino += fem;
|
||||||
fila.masculino += masc;
|
fila.masculino += masc;
|
||||||
});
|
});
|
||||||
|
|
||||||
setTablaData(Object.values(agrupado));
|
setTablaData(Object.values(agrupado));
|
||||||
|
|
||||||
|
const periodosUnicosList = Array.from(new Set(data.map((t) => t.semestre)));
|
||||||
|
setPeriodosUnicos(periodosUnicosList);
|
||||||
|
|
||||||
|
const carrerasUnicas = Array.from(new Set(data.map((t) => t.carrera)));
|
||||||
|
|
||||||
|
const resumen = carrerasUnicas.map((carrera) => {
|
||||||
|
const fila: any = { Carrera: carrera };
|
||||||
|
let totalFila = 0;
|
||||||
|
periodosUnicosList.forEach((periodo) => {
|
||||||
|
const items = data.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;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTablaResumen(resumen);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al obtener inscritos", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportarExcel = () => {
|
||||||
|
if (!rawData.length || !tablaResumen.length) return;
|
||||||
|
|
||||||
|
// HOJA 1: Inscritos (resumen)
|
||||||
|
const worksheet1 = XLSX.utils.json_to_sheet(tablaResumen);
|
||||||
|
|
||||||
|
const carrerasUnicas = Array.from(new Set(rawData.map((t) => t.carrera)));
|
||||||
|
|
||||||
|
// HOJA 2: genero
|
||||||
|
const dataGenero = carrerasUnicas.map((carrera) => {
|
||||||
|
const fila: any = { Carrera: carrera };
|
||||||
|
periodosUnicos.forEach((periodo) => {
|
||||||
|
const items = rawData.filter((t) => t.carrera === carrera && t.semestre === periodo);
|
||||||
|
const masc = items.reduce((acc, item) => acc + Number(item.masculino), 0);
|
||||||
|
const fem = items.reduce((acc, item) => acc + Number(item.femenino), 0);
|
||||||
|
fila[`Masculino - ${periodo}`] = masc;
|
||||||
|
fila[`Femenino - ${periodo}`] = fem;
|
||||||
|
});
|
||||||
|
return fila;
|
||||||
|
});
|
||||||
|
const worksheet2 = XLSX.utils.json_to_sheet(dataGenero);
|
||||||
|
|
||||||
|
// HOJA 3: areas
|
||||||
|
const dataAreas = carrerasUnicas.map((carrera) => {
|
||||||
|
const fila: any = { Carrera: carrera };
|
||||||
|
periodosUnicos.forEach((periodo) => {
|
||||||
|
const items = rawData.filter((t) => t.carrera === carrera && t.semestre === periodo);
|
||||||
|
let windows = 0, mac = 0, linux = 0, profes = 0;
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.profesor === "WINDOWS") windows += Number(item.total);
|
||||||
|
if (item.profesor === "MACINTOSH") mac += Number(item.total);
|
||||||
|
if (item.profesor === "LINUX") linux += Number(item.total);
|
||||||
|
if (item.profesor === "PROFESORES") profes += Number(item.total);
|
||||||
|
});
|
||||||
|
|
||||||
|
fila[`WINDOWS - ${periodo}`] = windows;
|
||||||
|
fila[`MACINTOSH - ${periodo}`] = mac;
|
||||||
|
fila[`LINUX - ${periodo}`] = linux;
|
||||||
|
fila[`PROFESORES - ${periodo}`] = profes;
|
||||||
|
});
|
||||||
|
return fila;
|
||||||
|
});
|
||||||
|
const worksheet3 = XLSX.utils.json_to_sheet(dataAreas);
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet1, "Inscritos");
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet2, "genero");
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet3, "areas");
|
||||||
|
|
||||||
|
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 (
|
||||||
@@ -104,15 +233,20 @@ 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
|
|
||||||
</option>
|
|
||||||
{periodo.map((p) => (
|
{periodo.map((p) => (
|
||||||
<option key={p.id_periodo} value={p.id_periodo}>
|
<option key={p.id_periodo} value={p.id_periodo}>
|
||||||
{p.semestre}
|
{p.semestre}
|
||||||
@@ -120,17 +254,56 @@ export default function Inscripciones() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select onChange={(e) => setPeriodoFin(Number(e.target.value))}>
|
||||||
|
<option value="">Fin</option>
|
||||||
|
{periodo.map((p) => (
|
||||||
|
<option key={p.id_periodo} value={p.id_periodo}>
|
||||||
|
{p.semestre}
|
||||||
|
</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>Periodo</th>
|
||||||
<th>Femenino</th>
|
<th>Femenino</th>
|
||||||
<th>Masculino</th>
|
<th>Masculino</th>
|
||||||
<th>WINDOWS</th>
|
<th>WINDOWS</th>
|
||||||
@@ -144,7 +317,8 @@ 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.semestre}</td>
|
||||||
<td>{row.femenino}</td>
|
<td>{row.femenino}</td>
|
||||||
<td>{row.masculino}</td>
|
<td>{row.masculino}</td>
|
||||||
<td>{row.WINDOWS}</td>
|
<td>{row.WINDOWS}</td>
|
||||||
|
|||||||
@@ -1,272 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { envConfig } from "@/app/lib/config";
|
|
||||||
import axios from "axios";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import * as XLSX from "xlsx";
|
|
||||||
import { saveAs } from "file-saver";
|
|
||||||
|
|
||||||
import {
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Line,
|
|
||||||
LineChart,
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
interface TotalServicioPeriodo {
|
|
||||||
nombre_servicio: string;
|
|
||||||
periodo: string;
|
|
||||||
monto_total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Servicio {
|
|
||||||
id_servicio: number;
|
|
||||||
servicio: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Periodo {
|
|
||||||
id_periodo: number;
|
|
||||||
semestre: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ReporteTotalesPage() {
|
|
||||||
const [totales, setTotales] = useState<TotalServicioPeriodo[]>([]);
|
|
||||||
const [servicios, setServicios] = useState<Servicio[]>([]);
|
|
||||||
const [periodos, setPeriodos] = useState<Periodo[]>([]);
|
|
||||||
const [cargando, setCargando] = useState(true);
|
|
||||||
const [periodoInicio, setPeriodoInicio] = useState<string>("");
|
|
||||||
const [periodoFin, setPeriodoFin] = useState<string>("");
|
|
||||||
const [servicioId, setServicioId] = useState<string>("");
|
|
||||||
|
|
||||||
const token = Cookies.get("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_pivot.xlsx");
|
|
||||||
};
|
|
||||||
|
|
||||||
// FETCH
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const [serviciosRes, periodosRes] = await Promise.all([
|
|
||||||
axios.get(`${envConfig.apiUrl}/servicio`, { headers }),
|
|
||||||
axios.get(`${envConfig.apiUrl}/periodo`, { headers }),
|
|
||||||
]);
|
|
||||||
setServicios(serviciosRes.data);
|
|
||||||
setPeriodos(periodosRes.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error al cargar datos", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const obtenerTotales = async () => {
|
|
||||||
setCargando(true);
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (periodoInicio) params.append("periodoInicio", periodoInicio);
|
|
||||||
if (periodoFin) params.append("periodoFin", periodoFin);
|
|
||||||
if (servicioId) params.append("servicioId", servicioId);
|
|
||||||
|
|
||||||
const url = `${envConfig.apiUrl}/detalle-servicio/totales-por-periodo?${params.toString()}`;
|
|
||||||
const res = await axios.get(url, { headers });
|
|
||||||
setTotales(res.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error al obtener totales", error);
|
|
||||||
} finally {
|
|
||||||
setCargando(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const aplicarFiltros = () => {
|
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
<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" }}>
|
|
||||||
<select value={periodoInicio} onChange={(e) => setPeriodoInicio(e.target.value)}>
|
|
||||||
<option value="">Periodo inicio</option>
|
|
||||||
{periodos.map((p) => (
|
|
||||||
<option key={p.id_periodo} value={p.id_periodo}>
|
|
||||||
{p.semestre}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select value={periodoFin} onChange={(e) => setPeriodoFin(e.target.value)}>
|
|
||||||
<option value="">Periodo fin</option>
|
|
||||||
{periodos.map((p) => (
|
|
||||||
<option key={p.id_periodo} value={p.id_periodo}>
|
|
||||||
{p.semestre}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select value={servicioId} onChange={(e) => setServicioId(e.target.value)}>
|
|
||||||
<option value="">Todos los servicios</option>
|
|
||||||
{servicios.map((s) => (
|
|
||||||
<option key={s.id_servicio} value={s.id_servicio}>
|
|
||||||
{s.servicio}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button onClick={aplicarFiltros}>Filtrar</button>
|
|
||||||
<button onClick={exportarExcelPivot}>Exportar Excel</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{cargando ? (
|
|
||||||
<p>Cargando...</p>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: "flex", gap: "50px", flexWrap: "wrap" }}>
|
|
||||||
|
|
||||||
{/* TABLA */}
|
|
||||||
<div style={{ flex: 1, minWidth: "500px", maxHeight: "500px", overflow: "auto" }}>
|
|
||||||
<table style={{ width: "100%" }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Servicio</th>
|
|
||||||
<th>Periodo</th>
|
|
||||||
<th>Monto</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
import {
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
} from "recharts";
|
||||||
|
import PeriodoPicker from "@/app/Components/PeriodoPicker/periodoPicker";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
|
||||||
|
interface Periodo {
|
||||||
|
id_periodo: number;
|
||||||
|
semestre: string;
|
||||||
|
fecha_inicio_servicio: string;
|
||||||
|
fecha_fin_servicio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function PorServicio() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [periodos, setPeriodos] = useState<Periodo[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [serviciosSeleccionados, setServiciosSeleccionados] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [periodo1, setPeriodo1] = useState<Periodo | null>(null);
|
||||||
|
const [periodo2, setPeriodo2] = useState<Periodo | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!periodo1 || !periodo2) return;
|
||||||
|
|
||||||
|
const fetchServicios = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = Cookies.get("token");
|
||||||
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
|
// 🔥 1. traer TODOS los periodos en rango
|
||||||
|
const resPeriodos = await axios.get(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/periodo/range?p1=${periodo1.id_periodo}&p2=${periodo2.id_periodo}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
const periodosRango = resPeriodos.data;
|
||||||
|
setPeriodos(periodosRango);
|
||||||
|
|
||||||
|
// 🔥 2. hacer requests por cada periodo
|
||||||
|
const resultados = await Promise.all(
|
||||||
|
periodosRango.map((p: Periodo) =>
|
||||||
|
axios.post(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/detalle-servicio/rango`,
|
||||||
|
{
|
||||||
|
desde: p.fecha_inicio_servicio,
|
||||||
|
hasta: p.fecha_fin_servicio,
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔥 3. merge dinámico
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
periodosRango.forEach((p: Periodo, index: number) => {
|
||||||
|
resultados[index].data.forEach((item: any) => {
|
||||||
|
if (!map.has(item.servicio)) {
|
||||||
|
map.set(item.servicio, {
|
||||||
|
servicio: item.servicio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
map.get(item.servicio)[p.semestre] = Number(item.total);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalData = Array.from(map.values());
|
||||||
|
setData(finalData);
|
||||||
|
setServiciosSeleccionados(finalData.map((item: any) => item.servicio));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error comparando servicios", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchServicios();
|
||||||
|
}, [periodo1, periodo2]);
|
||||||
|
|
||||||
|
// 🔥 TRANSFORMAR DATA PARA GRAFICA
|
||||||
|
const transformDataForChart = () => {
|
||||||
|
if (!data.length || !periodos.length) return [];
|
||||||
|
|
||||||
|
return periodos.map((p) => {
|
||||||
|
const row: any = { periodo: p.semestre };
|
||||||
|
|
||||||
|
data.forEach((servicio) => {
|
||||||
|
row[servicio.servicio] = servicio[p.semestre] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// EXPORTAR A EXCEL
|
||||||
|
const exportToExcel = () => {
|
||||||
|
if (!data || data.length === 0) return;
|
||||||
|
|
||||||
|
const exportData = data.map((row) => {
|
||||||
|
const newRow: any = { Servicio: row.servicio };
|
||||||
|
periodos.forEach((p) => {
|
||||||
|
newRow[p.semestre] = row[p.semestre] || 0;
|
||||||
|
});
|
||||||
|
return newRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Ingresos");
|
||||||
|
|
||||||
|
XLSX.writeFile(workbook, "Reporte_Ingresos_Servicio.xlsx");
|
||||||
|
};
|
||||||
|
|
||||||
|
// FORMATO DE NUMEROS
|
||||||
|
const formatNumber = (value: number) => {
|
||||||
|
return value.toLocaleString("en-US", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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}: ${formatNumber(entry.value)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const chartData = transformDataForChart();
|
||||||
|
|
||||||
|
const colors = ["black", "red", "blue", "orange", "purple", "green"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "5px", overflow: "auto" }}>
|
||||||
|
|
||||||
|
<PeriodoPicker
|
||||||
|
onChange={(p1, p2) => {
|
||||||
|
setPeriodo1(p1);
|
||||||
|
setPeriodo2(p2);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!periodo1 || !periodo2 ? (
|
||||||
|
<p>Seleccione un rango de periodos</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* CONTROLES SUPERIORES */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
padding: "10px 0",
|
||||||
|
position: "absolute",
|
||||||
|
top: "34px",
|
||||||
|
right: "0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={exportToExcel}
|
||||||
|
disabled={data.length === 0}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px",
|
||||||
|
backgroundColor:
|
||||||
|
data.length === 0 ? "#ccc" : "#10b981",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor:
|
||||||
|
data.length === 0
|
||||||
|
? "not-allowed"
|
||||||
|
: "pointer",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Excel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{data.map((servicio) => (
|
||||||
|
<label
|
||||||
|
key={servicio.servicio}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontSize:"9px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={serviciosSeleccionados.includes(
|
||||||
|
servicio.servicio
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setServiciosSeleccionados((prev) => [
|
||||||
|
...prev,
|
||||||
|
servicio.servicio,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setServiciosSeleccionados((prev) =>
|
||||||
|
prev.filter(
|
||||||
|
(s) =>
|
||||||
|
s !==
|
||||||
|
servicio.servicio
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{servicio.servicio}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TABLA Y GRÁFICA */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "30px",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* COLUMNA IZQUIERDA: TABLA */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: "1 1 300px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: "400px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading && <p>Cargando...</p>}
|
||||||
|
|
||||||
|
<table style={{ width: "100%" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Servicio</th>
|
||||||
|
|
||||||
|
{periodos.map((p) => (
|
||||||
|
<th key={p.id_periodo}>
|
||||||
|
{p.semestre}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{!loading && data.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={
|
||||||
|
periodos.length + 1
|
||||||
|
}
|
||||||
|
>
|
||||||
|
No hay datos en este rango
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.map((row) => (
|
||||||
|
<tr key={row.servicio}>
|
||||||
|
<td>{row.servicio}</td>
|
||||||
|
|
||||||
|
{periodos.map((p) => (
|
||||||
|
<td
|
||||||
|
key={p.id_periodo}
|
||||||
|
>
|
||||||
|
$
|
||||||
|
{formatNumber(
|
||||||
|
row[
|
||||||
|
p.semestre
|
||||||
|
] || 0
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* COLUMNA DERECHA: GRÁFICA */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: "2 1 500px",
|
||||||
|
minWidth: "500px",
|
||||||
|
background: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chartData.length > 0 && (
|
||||||
|
<ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height={400}
|
||||||
|
>
|
||||||
|
<LineChart data={chartData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
|
||||||
|
<XAxis dataKey="periodo" />
|
||||||
|
|
||||||
|
<YAxis
|
||||||
|
tickFormatter={(value) =>
|
||||||
|
value.toLocaleString()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={<CustomTooltip />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Legend />
|
||||||
|
|
||||||
|
{data
|
||||||
|
.filter((servicio) =>
|
||||||
|
serviciosSeleccionados.includes(
|
||||||
|
servicio.servicio
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((servicio, i) => (
|
||||||
|
<Line
|
||||||
|
key={
|
||||||
|
servicio.servicio
|
||||||
|
}
|
||||||
|
dataKey={
|
||||||
|
servicio.servicio
|
||||||
|
}
|
||||||
|
stroke={
|
||||||
|
colors[
|
||||||
|
i %
|
||||||
|
colors.length
|
||||||
|
]
|
||||||
|
}
|
||||||
|
type="monotone"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,17 +5,25 @@ 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";
|
||||||
|
import PeriodoPicker from "@/app/Components/PeriodoPicker/periodoPicker";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import PorServicio from "../Reportes-graficas/page";
|
||||||
|
|
||||||
export default function ReportesClient({
|
export default function ReportesClient({
|
||||||
defaultKey,
|
defaultKey,
|
||||||
}: {
|
}: {
|
||||||
defaultKey?: string;
|
defaultKey?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [desde, setDesde] = useState<string | null>(null);
|
const [desde, setDesde] = useState<string | null>(null);
|
||||||
const [hasta, setHasta] = useState<string | null>(null);
|
const [hasta, setHasta] = useState<string | null>(null);
|
||||||
|
const [periodo1, setPeriodo1] = useState<any>(null);
|
||||||
|
const [periodo2, setPeriodo2] = useState<any>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="containerSection">
|
<section className="containerSection" style={{ overflow: "visible", }}>
|
||||||
<h2 className="title">REPORTES</h2>
|
<h2 className="title">REPORTES</h2>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -26,8 +34,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);
|
||||||
}}
|
}}
|
||||||
@@ -39,20 +47,13 @@ export default function ReportesClient({
|
|||||||
{
|
{
|
||||||
key: "Por Servicio",
|
key: "Por Servicio",
|
||||||
label: "Por Servicio",
|
label: "Por Servicio",
|
||||||
content: (
|
content: null,
|
||||||
<>
|
}
|
||||||
<SearchDateBetween
|
|
||||||
onSearch={(d, h) => {
|
|
||||||
setDesde(d);
|
|
||||||
setHasta(h);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PorServicios desde={desde} hasta={hasta} />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{defaultKey == "Por Servicio" &&
|
||||||
|
<PorServicio/>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface ToggleOption {
|
|||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
content: ReactNode | (() => ReactNode);
|
content: ReactNode | (() => ReactNode);
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToggleProps {
|
interface ToggleProps {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -57,7 +57,7 @@ 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]);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { envConfig } from "@/app/lib/config";
|
||||||
|
interface Periodo {
|
||||||
|
id_periodo: number;
|
||||||
|
semestre: string;
|
||||||
|
fecha_inicio_servicio: string;
|
||||||
|
fecha_fin_servicio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PeriodoPicker({
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
onChange: (p1: Periodo | null, p2: Periodo | null) => void;
|
||||||
|
}) {
|
||||||
|
const [periodos, setPeriodos] = useState<Periodo[]>([]);
|
||||||
|
const [p1, setP1] = useState<number | null>(null);
|
||||||
|
const [p2, setP2] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
const token = Cookies.get("token");
|
||||||
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPeriodos = async () => {
|
||||||
|
const res = await axios.get(`${envConfig.apiUrl}/periodo`, { headers });
|
||||||
|
setPeriodos(res.data);
|
||||||
|
};
|
||||||
|
fetchPeriodos();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const periodo1 = periodos.find(p => p.id_periodo === p1) || null;
|
||||||
|
const periodo2 = periodos.find(p => p.id_periodo === p2) || null;
|
||||||
|
|
||||||
|
onChange(periodo1, periodo2);
|
||||||
|
}, [p1, p2]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{/* Periodo 1 */}
|
||||||
|
<select onChange={(e) => setP1(Number(e.target.value))}>
|
||||||
|
<option value="">Periodo 1</option>
|
||||||
|
{periodos.map(p => (
|
||||||
|
<option key={p.id_periodo} value={p.id_periodo}>
|
||||||
|
{p.semestre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Periodo 2 */}
|
||||||
|
<select onChange={(e) => setP2(Number(e.target.value))}>
|
||||||
|
<option value="">Periodo 2</option>
|
||||||
|
{periodos.map(p => (
|
||||||
|
<option key={p.id_periodo} value={p.id_periodo}>
|
||||||
|
{p.semestre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,84 +4,269 @@ import { useEffect, useState } from "react";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
interface ServicioReporte {
|
import {
|
||||||
id_servicio: number;
|
XAxis,
|
||||||
servicio: string;
|
YAxis,
|
||||||
total: string;
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
|
interface Periodo {
|
||||||
|
id_periodo: number;
|
||||||
|
semestre: string;
|
||||||
|
fecha_inicio_servicio: string;
|
||||||
|
fecha_fin_servicio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
desde: string | null;
|
periodo1: Periodo | null;
|
||||||
hasta: string | null;
|
periodo2: Periodo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PorServicio({ desde, hasta }: Props) {
|
export default function PorServicio({ periodo1, periodo2 }: Props) {
|
||||||
const [servicios, setServicios] = useState<ServicioReporte[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [periodos, setPeriodos] = useState<Periodo[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [serviciosSeleccionados, setServiciosSeleccionados] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!desde || !hasta) return;
|
if (!periodo1 || !periodo2) return;
|
||||||
|
|
||||||
const fetchServicios = async () => {
|
const fetchServicios = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = Cookies.get("token");
|
const token = Cookies.get("token");
|
||||||
const headers = { Authorization: `Bearer ${token}` };
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
const res = await axios.post(
|
// 🔥 1. traer TODOS los periodos en rango
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/detalle-servicio/rango`,
|
const resPeriodos = await axios.get(
|
||||||
{ desde, hasta },
|
`${process.env.NEXT_PUBLIC_API_URL}/periodo/range?p1=${periodo1.id_periodo}&p2=${periodo2.id_periodo}`,
|
||||||
{ headers },
|
{ headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
setServicios(res.data);
|
const periodosRango = resPeriodos.data;
|
||||||
|
setPeriodos(periodosRango);
|
||||||
|
|
||||||
|
// 🔥 2. hacer requests por cada periodo
|
||||||
|
const resultados = await Promise.all(
|
||||||
|
periodosRango.map((p: Periodo) =>
|
||||||
|
axios.post(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/detalle-servicio/rango`,
|
||||||
|
{
|
||||||
|
desde: p.fecha_inicio_servicio,
|
||||||
|
hasta: p.fecha_fin_servicio,
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔥 3. merge dinámico
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
periodosRango.forEach((p: Periodo, index: number) => {
|
||||||
|
resultados[index].data.forEach((item: any) => {
|
||||||
|
if (!map.has(item.servicio)) {
|
||||||
|
map.set(item.servicio, {
|
||||||
|
servicio: item.servicio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
map.get(item.servicio)[p.semestre] = Number(item.total);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalData = Array.from(map.values());
|
||||||
|
setData(finalData);
|
||||||
|
setServiciosSeleccionados(finalData.map((item: any) => item.servicio));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al obtener reporte por servicio", error);
|
console.error("Error comparando servicios", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchServicios();
|
fetchServicios();
|
||||||
}, [desde, hasta]);
|
}, [periodo1, periodo2]);
|
||||||
|
|
||||||
|
// 🔥 TRANSFORMAR DATA PARA GRAFICA
|
||||||
|
const transformDataForChart = () => {
|
||||||
|
if (!data.length || !periodos.length) return [];
|
||||||
|
|
||||||
|
return periodos.map((p) => {
|
||||||
|
const row: any = { periodo: p.semestre };
|
||||||
|
|
||||||
|
data.forEach((servicio) => {
|
||||||
|
row[servicio.servicio] = servicio[p.semestre] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// FORMATO DE NUMEROS
|
||||||
|
const formatNumber = (value: number) => {
|
||||||
|
return value.toLocaleString("en-US", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
overflow: "auto",
|
background: "white",
|
||||||
height: "270px",
|
padding: "10px",
|
||||||
scrollbarColor: "#2563eb white",
|
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}: ${formatNumber(entry.value)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartData = transformDataForChart();
|
||||||
|
|
||||||
|
const colors = ["black", "red", "blue", "orange", "purple", "green"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "30px" }}>
|
||||||
|
|
||||||
|
{/* TABLA */}
|
||||||
|
<div style={{ overflow: "auto", maxHeight: "300px" }}>
|
||||||
{loading && <p>Cargando...</p>}
|
{loading && <p>Cargando...</p>}
|
||||||
|
|
||||||
<table>
|
<table style={{ width: "100%" }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Servicio</th>
|
<th>Servicio</th>
|
||||||
<th>Monto total</th>
|
|
||||||
|
{periodos.map((p) => (
|
||||||
|
<th key={p.id_periodo}>{p.semestre}</th>
|
||||||
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{!loading && servicios.length === 0 && (
|
{!loading && data.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2}>No hay servicios en este rango</td>
|
<td colSpan={periodos.length + 1}>
|
||||||
|
No hay datos en este rango
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{servicios.map((servicio) => (
|
{data.map((row) => (
|
||||||
<tr key={servicio.id_servicio}>
|
<tr key={row.servicio}>
|
||||||
<td>{servicio.servicio}</td>
|
<td>{row.servicio}</td>
|
||||||
<td>${Number(servicio.total).toFixed(2)}</td>
|
|
||||||
|
{periodos.map((p) => (
|
||||||
|
<td key={p.id_periodo}>
|
||||||
|
${formatNumber(row[p.semestre] || 0)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "10px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.map((servicio) => (
|
||||||
|
<label
|
||||||
|
key={servicio.servicio}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={serviciosSeleccionados.includes(servicio.servicio)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setServiciosSeleccionados((prev) => [
|
||||||
|
...prev,
|
||||||
|
servicio.servicio,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setServiciosSeleccionados((prev) =>
|
||||||
|
prev.filter((s) => s !== servicio.servicio)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{servicio.servicio}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* GRAFICA */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: "700px",
|
||||||
|
background: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chartData.length > 0 && (
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<LineChart data={chartData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="periodo" />
|
||||||
|
<YAxis tickFormatter={(value) => value.toLocaleString()} />
|
||||||
|
|
||||||
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
|
||||||
|
<Legend />
|
||||||
|
|
||||||
|
{data
|
||||||
|
.filter((servicio) =>
|
||||||
|
serviciosSeleccionados.includes(servicio.servicio)
|
||||||
|
)
|
||||||
|
.map((servicio, i) => (
|
||||||
|
<Line
|
||||||
|
key={servicio.servicio}
|
||||||
|
dataKey={servicio.servicio}
|
||||||
|
stroke={colors[i % colors.length]}
|
||||||
|
type="monotone"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//IO
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -99,6 +121,7 @@ footer p {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerForm {
|
.containerForm {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
|||||||
authors: [{ name: "FES Acatlán" }],
|
authors: [{ name: "FES Acatlán" }],
|
||||||
creator: "Lino,Carlos,Axel",
|
creator: "Lino,Carlos,Axel",
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/icon.svg",
|
icon: [{ url: "/icon.ico" }, { url: "/icon.png" }, { url: "/icon.svg" }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@
|
|||||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.8.1",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -559,15 +559,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz",
|
||||||
"integrity": "sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==",
|
"integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz",
|
||||||
"integrity": "sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==",
|
"integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -581,9 +581,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz",
|
||||||
"integrity": "sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==",
|
"integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -597,9 +597,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz",
|
||||||
"integrity": "sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==",
|
"integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -613,9 +613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz",
|
||||||
"integrity": "sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==",
|
"integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -629,9 +629,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz",
|
||||||
"integrity": "sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==",
|
"integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -645,9 +645,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz",
|
||||||
"integrity": "sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==",
|
"integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -661,9 +661,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz",
|
||||||
"integrity": "sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==",
|
"integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -677,9 +677,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||||
"integrity": "sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==",
|
"integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1305,14 +1305,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.5",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
||||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -1393,9 +1393,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@@ -2139,9 +2139,9 @@
|
|||||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -3150,12 +3150,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz",
|
||||||
"integrity": "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==",
|
"integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.2.0",
|
"@next/env": "16.2.4",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -3169,14 +3169,14 @@
|
|||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "16.2.0",
|
"@next/swc-darwin-arm64": "16.2.4",
|
||||||
"@next/swc-darwin-x64": "16.2.0",
|
"@next/swc-darwin-x64": "16.2.4",
|
||||||
"@next/swc-linux-arm64-gnu": "16.2.0",
|
"@next/swc-linux-arm64-gnu": "16.2.4",
|
||||||
"@next/swc-linux-arm64-musl": "16.2.0",
|
"@next/swc-linux-arm64-musl": "16.2.4",
|
||||||
"@next/swc-linux-x64-gnu": "16.2.0",
|
"@next/swc-linux-x64-gnu": "16.2.4",
|
||||||
"@next/swc-linux-x64-musl": "16.2.0",
|
"@next/swc-linux-x64-musl": "16.2.4",
|
||||||
"@next/swc-win32-arm64-msvc": "16.2.0",
|
"@next/swc-win32-arm64-msvc": "16.2.4",
|
||||||
"@next/swc-win32-x64-msvc": "16.2.0",
|
"@next/swc-win32-x64-msvc": "16.2.4",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -3304,9 +3304,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3360,10 +3360,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
@@ -3460,9 +3463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
|
|||||||
@@ -87,3 +87,4 @@ export const config = {
|
|||||||
"/ActivosMantenimiento",
|
"/ActivosMantenimiento",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
//IO
|
||||||
|
After Width: | Height: | Size: 168 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -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 |
@@ -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 |