This commit is contained in:
jalvarado
2025-04-07 11:17:30 -06:00
parent a0e86b3873
commit b2e639efcb
9 changed files with 520 additions and 91 deletions
+249
View File
@@ -0,0 +1,249 @@
{
"pollutionMeasurements": {
"city": "Ciudad de M\\u00e9xico",
"cityCode": "MEX",
"country": "M\\u00e9xico",
"mesurementAgency": "SIMAT",
"unit": { "RH": "%", "TMP": "C", "WSP": "m/s", "WDR": "o" },
"date": {
"2023-01-01 01:00": {
"RH": {
"ACO": "81",
"AJU": "100",
"MON": "56",
"CHO": "",
"CUA": "61",
"CUT": "73",
"FAC": "73",
"HGM": "58",
"MER": "66",
"NEZ": "57",
"PED": "63",
"SAG": "74",
"SFE": "",
"TAH": "65",
"TLA": "60",
"UAX": "71",
"UIZ": "74",
"VIF": "69",
"XAL": "",
"MGH": "",
"AJM": "64",
"MPA": "71",
"BJU": "66",
"INN": "87",
"GAM": "68",
"LAA": "70"
},
"TMP": {
"ACO": "9.3",
"AJU": "2.5",
"MON": "13.9",
"CHO": "",
"CUA": "10.9",
"CUT": "8.4",
"FAC": "8.5",
"HGM": "13.2",
"MER": "13.1",
"NEZ": "13.8",
"PED": "10.9",
"SAG": "13.7",
"SFE": "",
"TAH": "9.3",
"TLA": "12.2",
"UAX": "10",
"UIZ": "12.7",
"VIF": "9.4",
"XAL": "",
"MGH": "",
"AJM": "7.2",
"MPA": "",
"BJU": "12.2",
"INN": "3.2",
"GAM": "13.3",
"LAA": "12.5"
},
"WDR": {
"ACO": "319",
"AJU": "36",
"MON": "177",
"CHO": "",
"CUA": "258",
"CUT": "246",
"FAC": "245",
"HGM": "43",
"MER": "178",
"NEZ": "156",
"PED": "227",
"SAG": "247",
"SFE": "",
"TAH": "9",
"TLA": "347",
"UAX": "139",
"UIZ": "168",
"VIF": "269",
"XAL": "",
"MGH": "",
"AJM": "",
"MPA": "155",
"BJU": "170",
"INN": "105",
"GAM": "66",
"LAA": "58",
"FAR": "159",
"SAC": "141"
},
"WSP": {
"ACO": "1.3",
"AJU": "2.4",
"MON": "1.8",
"CHO": "",
"CUA": "1.7",
"CUT": "1.4",
"FAC": "0.8",
"HGM": "1.4",
"MER": "0.9",
"NEZ": "2.2",
"PED": "1.9",
"SAG": "0",
"SFE": "",
"TAH": "0.3",
"TLA": "0.9",
"UAX": "1.3",
"UIZ": "0.6",
"VIF": "0",
"XAL": "",
"MGH": "",
"AJM": "",
"MPA": "3.1",
"BJU": "0.8",
"INN": "0.5",
"GAM": "0.3",
"LAA": "0.6",
"FAR": "1.4",
"SAC": "2.4"
},
"PBa": { "MER": "" },
"2023-01-01 02:00": {
"RH": {
"ACO": "85",
"AJU": "100",
"MON": "63",
"CHO": "",
"CUA": "67",
"CUT": "73",
"FAC": "78",
"HGM": "61",
"MER": "67",
"NEZ": "60",
"PED": "58",
"SAG": "79",
"SFE": "",
"TAH": "67",
"TLA": "62",
"UAX": "73",
"UIZ": "75",
"VIF": "72",
"XAL": "",
"MGH": "",
"AJM": "61",
"MPA": "73",
"BJU": "67",
"INN": "87",
"GAM": "69",
"LAA": "74"
},
"TMP": {
"ACO": "8.2",
"AJU": "3.1",
"MON": "11.7",
"CHO": "",
"CUA": "10.6",
"CUT": "7.9",
"FAC": "7.2",
"HGM": "12.4",
"MER": "12.8",
"NEZ": "13.1",
"PED": "10.8",
"SAG": "12.7",
"SFE": "",
"TAH": "8.9",
"TLA": "11.1",
"UAX": "9.6",
"UIZ": "12.1",
"VIF": "8.5",
"XAL": "",
"MGH": "",
"AJM": "6.7",
"MPA": "",
"BJU": "11.7",
"INN": "3.2",
"GAM": "13",
"LAA": "11.6"
},
"WDR": {
"ACO": "355",
"AJU": "6",
"MON": "49",
"CHO": "",
"CUA": "265",
"CUT": "223",
"FAC": "235",
"HGM": "39",
"MER": "208",
"NEZ": "205",
"PED": "212",
"SAG": "92",
"SFE": "",
"TAH": "288",
"TLA": "318",
"UAX": "184",
"UIZ": "196",
"VIF": "312",
"XAL": "",
"MGH": "",
"AJM": "",
"MPA": "165",
"BJU": "169",
"INN": "66",
"GAM": "37",
"LAA": "277",
"FAR": "59",
"SAC": "153"
},
"WSP": {
"ACO": "1.3",
"AJU": "1.7",
"MON": "1.1",
"CHO": "",
"CUA": "2.4",
"CUT": "1.4",
"FAC": "1",
"HGM": "0.7",
"MER": "1.1",
"NEZ": "0.9",
"PED": "2.1",
"SAG": "0.1",
"SFE": "",
"TAH": "1.3",
"TLA": "1.3",
"UAX": "0.8",
"UIZ": "0.8",
"VIF": "0",
"XAL": "",
"MGH": "",
"AJM": "",
"MPA": "2",
"BJU": "0.9",
"INN": "0.2",
"GAM": "0",
"LAA": "0.2",
"FAR": "1.1",
"SAC": "1.7"
},
"PBa": { "MER": "" }
}
}
}
}
}
+17 -9
View File
@@ -18,24 +18,32 @@ export default function Page() {
const [enableScan, setEnableScan] = useState(true);
const [statusMessage, setStatusMessage] = useState('');
const handleScan = (rawValue: string) => {
const [participante, setParticipante] = useState('');
const handleScan = async (rawValue: string) => {
if (!enableScan) return;
try {
const data = JSON.parse(rawValue);
if (data.id_participante && data.id_evento) {
setScannedData({
id_participante: data.id_participante,
id_evento: data.id_evento,
});
setEnableScan(false);
setShowModal(true);
try {
const response = await axiosInstance.get(
`/participante-evento/${data.id_participante}/${data.id_evento}`
);
console.log(response.data);
} catch (err) {
console.warn('Participante no registrado en el evento:', err);
setStatusMessage(
'❌ El participante no está registrado en este evento.'
);
}
} else {
setStatusMessage('El QR no contiene los campos requeridos');
setStatusMessage('⚠️ El QR no contiene los campos requeridos');
}
} catch (err) {
console.error('QR malformado:', err);
setStatusMessage('Error al leer el QR: formato inválido');
setStatusMessage('Error al leer el QR: formato inválido');
}
};
+4 -4
View File
@@ -53,18 +53,18 @@ export default function Input(props: InputProps) {
{label}
</label>
)}
<div className='input-group'>
<div className="input-group">
<input
id={inputId}
name={name}
type={inputType}
className={`form-control bg-white ${className?.input || ''}`}
className={`form-control ${className?.input || ''}`}
{...rest}
/>
{isPassword && (
<button
type='button'
className='btn btn-light border'
type="button"
className="btn btn-light border"
onClick={togglePasswordVisibility}
aria-label={
showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'
+142 -34
View File
@@ -9,6 +9,15 @@ import { validarRespuesta } from '@/utils/validador';
import toast from 'react-hot-toast';
import axiosInstance from '@/utils/api-config';
import { useRouter } from 'next/navigation';
import { getAxiosError } from '@/utils/errors-utils';
interface DatosAlumno {
id_ncuenta: number;
nombre: string;
apellidos: string;
carrera: string;
genero: 'M' | 'F';
}
export default function Formulario({
id_formulario,
@@ -24,6 +33,7 @@ export default function Formulario({
nombre: string;
apellidos: string;
correo: string;
genero: 'M' | 'F';
}>(null);
const [respuestas, setRespuestas] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false); // New state for submission
@@ -34,16 +44,71 @@ export default function Formulario({
useEffect(() => {
if (esDeFES && cuenta.length === 9) {
fetch(`/datos/alumnos/${cuenta}`)
.then((res) => res.json())
.then((data) => setDatosAuto(data))
.catch((err) => {
console.error('Error al obtener datos del alumno:', err);
(async () => {
try {
setDatosAuto(null);
});
const { data } = await axiosInstance.get<DatosAlumno>(
`/alumnos/${cuenta}`
);
console.log('Datos del alumno:', data);
if (data) {
setDatosAuto({
nombre: data.nombre.toString(),
apellidos: data.apellidos.toString(),
correo: data.id_ncuenta + '@pcpuma.acatlan.unam.mx',
genero: data.genero,
});
}
} catch (err) {
const msg = getAxiosError(err);
toast.error(msg.message);
setDatosAuto(null);
}
})();
}
}, [cuenta, esDeFES]);
useEffect(() => {
if (esDeFES && datosAuto) {
const nuevasRespuestas: Record<string, string> = {};
secciones.forEach((seccion) => {
seccion.preguntas.forEach(({ pregunta }) => {
const id = pregunta.id_pregunta;
if (pregunta.validacion === 'nombre') {
nuevasRespuestas[`pregunta_${id}`] = pregunta.pregunta
.toLowerCase()
.includes('apellido')
? datosAuto.apellidos.toString()
: datosAuto.nombre.toString();
}
if (pregunta.validacion === 'correo') {
nuevasRespuestas[`pregunta_${id}`] = datosAuto.correo;
}
if (pregunta.validacion === 'cuenta_alumno') {
nuevasRespuestas[`pregunta_${id}`] = cuenta;
}
if (
pregunta.pregunta
.toLowerCase()
.includes('institución de procedencia')
) {
nuevasRespuestas[`pregunta_${id}`] = 'FES Acatlán';
}
});
});
setRespuestas((prev) => ({
...prev,
...nuevasRespuestas,
}));
}
}, [datosAuto, esDeFES, cuenta, secciones]);
// Encuentra preguntas por texto o validación
const todasPreguntas = secciones.flatMap((s) =>
s.preguntas.map((p) => p.pregunta)
@@ -123,6 +188,7 @@ export default function Formulario({
const validarRespuestasObligatorias = (): boolean => {
const faltantes: number[] = [];
const errores: { id: number; mensaje: string }[] = [];
console.log(secciones);
console.log('Validando respuestas:', respuestas);
secciones.forEach((seccion) => {
@@ -132,6 +198,14 @@ export default function Formulario({
const tipo = pregunta.tipo_pregunta.tipo_pregunta;
const validacion = pregunta.validacion;
console.log('Validando pregunta:', pregunta.pregunta);
console.table({
id,
valor,
tipo,
validacion,
});
const respondida =
(tipo === 'Abierta' &&
typeof valor === 'string' &&
@@ -183,19 +257,16 @@ export default function Formulario({
}))}
selectedValue={respuestaFES ? Number(respuestaFES) : undefined}
onChange={(opt) => {
const idSeleccionado = opt.value;
const opcionSeleccionada = preguntaFES.opciones.find(
(o) => o.id_opcion === idSeleccionado
);
const id = opt.value;
const value = opt?.label ?? '';
const valorTexto = opcionSeleccionada?.opcion.opcion ?? '';
setEsDeFES(valorTexto === 'Si'); // Se actualiza esDeFES con el texto
setEsDeFES(value === 'Si'); // Se actualiza esDeFES con el texto
setCuenta('');
setDatosAuto(null);
actualizarRespuesta(
`pregunta_${preguntaFES.id_pregunta}`,
String(idSeleccionado)
String(id)
);
}}
/>
@@ -209,6 +280,7 @@ export default function Formulario({
label={preguntaCuenta.pregunta}
name={`pregunta_${preguntaCuenta.id_pregunta}`}
value={cuenta}
maxLength={10}
onChange={(e) => {
const value = e.target.value;
setCuenta(value);
@@ -238,34 +310,70 @@ export default function Formulario({
defaultValue = datosAuto.correo;
disabled = true;
}
if (p.validacion === 'cuenta_alumno') {
defaultValue = cuenta;
disabled = true;
}
if (
p.pregunta.toLowerCase().includes('institución de procedencia')
) {
defaultValue = 'FES Acatlán';
disabled = true;
}
}
return (
<Input
key={p.id_pregunta}
label={p.pregunta}
name={`pregunta_${p.id_pregunta}`}
defaultValue={defaultValue}
disabled={disabled}
onChange={(e) =>
actualizarRespuesta(
`pregunta_${p.id_pregunta}`,
e.target.value
)
}
/>
<React.Fragment key={p.id_pregunta}>
<Input
label={p.pregunta}
name={`pregunta_${p.id_pregunta}`}
defaultValue={defaultValue}
disabled={disabled}
onChange={(e) =>
actualizarRespuesta(
`pregunta_${p.id_pregunta}`,
e.target.value
)
}
/>
{p.validacion === 'correo' && (
<div className="alert alert-info">
Este es el correo donde enviaremos la validación de registro{' '}
</div>
)}
</React.Fragment>
);
}
if (p.tipo_pregunta.tipo_pregunta === 'Cerrada') {
const opciones: RadioOption<number>[] = p.opciones.map(
(op) => ({
label: op.opcion.opcion,
value: op.id_opcion,
})
);
const opciones: RadioOption<number>[] = p.opciones.map((op) => ({
label: op.opcion.opcion,
value: op.id_opcion,
}));
const respuestaActual = respuestas[`pregunta_${p.id_pregunta}`];
let respuestaActual = respuestas[`pregunta_${p.id_pregunta}`];
// Si es de FES y hay datos automáticos, intentar preseleccionar género
if (
esDeFES &&
datosAuto &&
!respuestaActual &&
p.pregunta.toLowerCase().includes('género')
) {
const generoTexto =
datosAuto.genero === 'M' ? 'Masculino' : 'Femenino';
const opcionGenero = p.opciones.find(
(op) =>
op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
);
if (opcionGenero) {
respuestaActual = String(opcionGenero.id_opcion);
actualizarRespuesta(
`pregunta_${p.id_pregunta}`,
String(respuestaActual)
);
}
}
return (
<div key={p.id_pregunta}>
@@ -275,7 +383,7 @@ export default function Formulario({
options={opciones}
selectedValue={
respuestaActual ? Number(respuestaActual) : undefined
} // ✅ Aquí se fija la opción
}
onChange={(opt) =>
actualizarRespuesta(
`pregunta_${p.id_pregunta}`,
+7 -7
View File
@@ -32,13 +32,6 @@ export const plantillasDisponibles: Plantilla[] = [
opciones: [{ valor: 'Si' }, { valor: 'No' }],
obligatoria: true,
},
{
titulo: 'Correo electrónico',
tipo: 'Abierta',
obligatoria: true,
limite: 250,
validacion: 'correo',
},
{
titulo: 'Numero de cuenta',
tipo: 'Abierta',
@@ -46,6 +39,13 @@ export const plantillasDisponibles: Plantilla[] = [
limite: 250,
validacion: 'cuenta_alumno',
},
{
titulo: 'Correo electrónico',
tipo: 'Abierta',
obligatoria: true,
limite: 250,
validacion: 'correo',
},
{
titulo: 'Nombre(s)',
tipo: 'Abierta',
+5 -1
View File
@@ -120,4 +120,8 @@ ul.list-unstyled li {
.cursor-pointer {
cursor: pointer;
}
}
input {
--bs-body-bg: #fff;
}
+14
View File
@@ -0,0 +1,14 @@
interface User {
id_usuario: number;
id_tipo_usuario: number;
tipo_usuario: string;
nombre: string;
cuenta?: string;
carrera?: string;
}
interface ErrorState {
status: boolean;
message: string;
code: number;
}
+38
View File
@@ -0,0 +1,38 @@
import { AxiosError } from 'axios';
export const getAxiosError = (error: unknown): ErrorState => {
console.error('AXIOS ERROR', error);
if (error instanceof AxiosError) {
if (error.response?.data?.message) {
if (error.response.data.message === 'Internal server error') {
return {
code: 500,
message:
'Ocurrió un error inesperado, por favor intenta de nuevo más tarde',
status: true,
};
}
return {
code: error.response.status,
message: error.response.data.message,
status: true,
};
}
if (error.message === 'Network Error') {
return {
code: 0,
message: 'Error de conexión, por favor verifica tu conexión a internet',
status: true,
};
}
}
return {
code: 0,
message: 'Ocurrió un error inesperado',
status: true,
};
};
+44 -36
View File
@@ -20,18 +20,18 @@ export class ValidadorCorreo extends Validador {
if (!valor) {
return {
valido: false,
mensaje: 'El correo electrónico es requerido'
mensaje: 'El correo electrónico es requerido',
};
}
// Expresión regular para validar correos
// Valida el formato básico de correos: usuario@dominio.extensión
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!regex.test(valor)) {
return {
valido: false,
mensaje: 'El formato del correo electrónico no es válido'
mensaje: 'El formato del correo electrónico no es válido',
};
}
@@ -39,18 +39,19 @@ export class ValidadorCorreo extends Validador {
if (this.validarDominioInstitucional) {
const dominios = ['acatlan.unam.mx', 'unam.mx', 'comunidad.unam.mx'];
const dominio = valor.split('@')[1];
if (!dominios.includes(dominio)) {
return {
valido: false,
mensaje: 'Debe ser un correo institucional (@acatlan.unam.mx, @unam.mx, @comunidad.unam.mx)'
mensaje:
'Debe ser un correo institucional (@acatlan.unam.mx, @unam.mx, @comunidad.unam.mx)',
};
}
}
return {
valido: true,
mensaje: 'Correo electrónico válido'
mensaje: 'Correo electrónico válido',
};
}
@@ -66,32 +67,32 @@ export class ValidadorTelefono extends Validador {
if (!valor) {
return {
valido: false,
mensaje: 'El número telefónico es requerido'
mensaje: 'El número telefónico es requerido',
};
}
// Eliminar espacios, guiones y paréntesis para la validación
const numeroLimpio = valor.replace(/[\s\-()]/g, '');
// Verificar que solo contenga dígitos
if (!/^\d+$/.test(numeroLimpio)) {
return {
valido: false,
mensaje: 'El número telefónico solo debe contener dígitos'
mensaje: 'El número telefónico solo debe contener dígitos',
};
}
// Verificar longitud (para México, 10 dígitos)
if (numeroLimpio.length !== 10) {
return {
valido: false,
mensaje: 'El número telefónico debe tener 10 dígitos'
mensaje: 'El número telefónico debe tener 10 dígitos',
};
}
return {
valido: true,
mensaje: 'Número telefónico válido'
mensaje: 'Número telefónico válido',
};
}
}
@@ -99,10 +100,13 @@ export class ValidadorTelefono extends Validador {
// Clase para validar nombres
export class ValidadorNombre extends Validador {
validar(valor: string): ResultadoValidacion {
const regex = new RegExp(
'/^[A-Za-zÁÉÍÓÚáéíóúÑñ]+(?: [A-Za-zÁÉÍÓÚáéíóúÑñ]+)*$/'
);
if (!valor) {
return {
valido: false,
mensaje: 'El nombre es requerido'
mensaje: 'El nombre es requerido',
};
}
@@ -110,21 +114,21 @@ export class ValidadorNombre extends Validador {
if (valor.trim().length < 2) {
return {
valido: false,
mensaje: 'El nombre debe tener al menos 2 caracteres'
mensaje: 'El nombre debe tener al menos 2 caracteres',
};
}
// Verificar que solo contenga letras y espacios
if (!/^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\\s]+$/.test(valor)) {
if (regex.test(valor)) {
return {
valido: false,
mensaje: 'El nombre solo debe contener letras y espacios'
mensaje: 'El nombre solo debe contener letras y espacios',
};
}
return {
valido: true,
mensaje: 'Nombre válido'
mensaje: 'Nombre válido',
};
}
}
@@ -142,7 +146,7 @@ export class ValidadorEntero extends Validador {
if (!valor) {
return {
valido: false,
mensaje: 'El valor numérico es requerido'
mensaje: 'El valor numérico es requerido',
};
}
@@ -150,23 +154,23 @@ export class ValidadorEntero extends Validador {
if (!/^-?\d+$/.test(valor)) {
return {
valido: false,
mensaje: 'El valor debe ser un número entero'
mensaje: 'El valor debe ser un número entero',
};
}
const numero = parseInt(valor, 10);
// Verificar rango
if (numero < this.min || numero > this.max) {
return {
valido: false,
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`,
};
}
return {
valido: true,
mensaje: 'Número entero válido'
mensaje: 'Número entero válido',
};
}
}
@@ -185,7 +189,7 @@ export class ValidadorDecimal extends Validador {
if (!valor) {
return {
valido: false,
mensaje: 'El valor numérico es requerido'
mensaje: 'El valor numérico es requerido',
};
}
@@ -193,17 +197,17 @@ export class ValidadorDecimal extends Validador {
if (!/^-?\d+(\.\d+)?$/.test(valor)) {
return {
valido: false,
mensaje: 'El valor debe ser un número decimal válido'
mensaje: 'El valor debe ser un número decimal válido',
};
}
const numero = parseFloat(valor);
// Verificar rango
if (numero < this.min || numero > this.max) {
return {
valido: false,
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`,
};
}
@@ -212,13 +216,13 @@ export class ValidadorDecimal extends Validador {
if (partes.length > 1 && partes[1].length > this.decimales) {
return {
valido: false,
mensaje: `El valor debe tener máximo ${this.decimales} decimales`
mensaje: `El valor debe tener máximo ${this.decimales} decimales`,
};
}
return {
valido: true,
mensaje: 'Número decimal válido'
mensaje: 'Número decimal válido',
};
}
}
@@ -229,7 +233,7 @@ export class ValidadorCuentaAlumno extends Validador {
if (!valor) {
return {
valido: false,
mensaje: 'La cuenta de alumno es requerida'
mensaje: 'La cuenta de alumno es requerida',
};
}
@@ -237,13 +241,14 @@ export class ValidadorCuentaAlumno extends Validador {
if (!/^\d{9}$/.test(valor)) {
return {
valido: false,
mensaje: 'La cuenta de alumno debe tener exactamente 9 dígitos numéricos'
mensaje:
'La cuenta de alumno debe tener exactamente 9 dígitos numéricos',
};
}
return {
valido: true,
mensaje: 'Cuenta de alumno válida'
mensaje: 'Cuenta de alumno válida',
};
}
}
@@ -273,15 +278,18 @@ export class FabricaValidadores {
}
// Función auxiliar para validar respuestas basada en el tipo de validación
export function validarRespuesta(respuesta: string, tipoValidacion: string): ResultadoValidacion {
export function validarRespuesta(
respuesta: string,
tipoValidacion: string
): ResultadoValidacion {
const validador = FabricaValidadores.crear(tipoValidacion);
if (!validador) {
return {
valido: true, // Si no hay validador, consideramos válida la respuesta
mensaje: 'No se requiere validación específica'
mensaje: 'No se requiere validación específica',
};
}
return validador.validar(respuesta);
}
}