Se agregó carga masiva
This commit is contained in:
Generated
+1093
-259
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -38,14 +38,14 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
"mysql2": "^3.14.0",
|
"mysql2": "^3.14.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.21",
|
"typeorm": "^0.3.21"
|
||||||
"xlsx": "^0.18.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|||||||
@@ -9,15 +9,20 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
|
UnprocessableEntityException,
|
||||||
|
UploadedFiles,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CuestionarioService } from './cuestionario.service';
|
import { CuestionarioService } from './cuestionario.service';
|
||||||
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
||||||
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import { diskStorage } from 'multer';
|
import { diskStorage } from 'multer';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { CuestionarioApiDocumentation } from './docs/cuestionario.documentation';
|
import { CuestionarioApiDocumentation } from './docs/cuestionario.documentation';
|
||||||
import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
|
import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
|
||||||
|
import * as ExcelJS from 'exceljs';
|
||||||
|
import { TipoPreguntaEnum } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
|
||||||
|
|
||||||
@Controller('cuestionario')
|
@Controller('cuestionario')
|
||||||
@CuestionarioApiDocumentation.ApiController
|
@CuestionarioApiDocumentation.ApiController
|
||||||
@@ -30,6 +35,158 @@ export class CuestionarioController {
|
|||||||
return this.cuestionarioService.create(createCuestionarioDto);
|
return this.cuestionarioService.create(createCuestionarioDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private mapTipoPregunta(tipo: string): TipoPreguntaEnum {
|
||||||
|
switch (tipo.toLowerCase()) {
|
||||||
|
case 'cerrada': return TipoPreguntaEnum.Cerrada;
|
||||||
|
case 'multiple': return TipoPreguntaEnum.Multiple;
|
||||||
|
case 'abierta (parrafo)': return TipoPreguntaEnum.AbiertaParrafo;
|
||||||
|
case 'abierta (respuesta corta)': return TipoPreguntaEnum.AbiertaRespuestaCorta;
|
||||||
|
default: throw new Error(`Tipo de pregunta inválido: ${tipo}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('banners/bulk')
|
||||||
|
@UseInterceptors(
|
||||||
|
FilesInterceptor('banners', 20, { // puedes subir hasta 20 imágenes
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: './uploads/banners',
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
const allowedTypes = /jpeg|jpg|png|webp/;
|
||||||
|
const ext = extname(file.originalname).toLowerCase();
|
||||||
|
if (allowedTypes.test(ext)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Tipo de archivo no permitido'), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
async subirBanners(@UploadedFiles() files: Express.Multer.File[]) {
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
throw new UnprocessableEntityException('No se subió ningún archivo');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retornamos lista de filenames para asociarlos después
|
||||||
|
return files.map((file) => ({
|
||||||
|
originalName: file.originalname,
|
||||||
|
filename: file.filename,
|
||||||
|
path: file.path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear validación desde string a enum
|
||||||
|
private mapValidacion(validacion?: string): TiposValidacion | undefined {
|
||||||
|
if (!validacion) return undefined;
|
||||||
|
|
||||||
|
switch (validacion.trim().toLowerCase()) {
|
||||||
|
case 'correo': return TiposValidacion.CORREO;
|
||||||
|
case 'correo_institucional': return TiposValidacion.CORREO_INSTITUCIONAL;
|
||||||
|
case 'telefono': return TiposValidacion.TELEFONO;
|
||||||
|
case 'nombre': return TiposValidacion.NOMBRE;
|
||||||
|
case 'apellidos': return TiposValidacion.APELLIDOS;
|
||||||
|
case 'entero': return TiposValidacion.ENTERO;
|
||||||
|
case 'decimal': return TiposValidacion.DECIMAL;
|
||||||
|
case 'comunidad_alumno': return TiposValidacion.COMUNIDAD_ALUMNO;
|
||||||
|
case 'cuenta_alumno': return TiposValidacion.CUENTA_ALUMNO;
|
||||||
|
case 'comunidad_trabajador': return TiposValidacion.COMUNIDAD_TRABAJADOR;
|
||||||
|
case 'cuenta_trabajador': return TiposValidacion.CUENTA_TRABAJADOR;
|
||||||
|
case 'genero': return TiposValidacion.GENERO;
|
||||||
|
case 'carrera': return TiposValidacion.CARRERA;
|
||||||
|
case 'institucion': return TiposValidacion.INSTITUCION;
|
||||||
|
case 'rfc': return TiposValidacion.RFC;
|
||||||
|
default: return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función principal de carga masiva
|
||||||
|
async cargarEventosDesdeExcel(filePath: string) {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
await workbook.xlsx.readFile(filePath);
|
||||||
|
|
||||||
|
const eventosSheet = workbook.getWorksheet('Eventos');
|
||||||
|
const cuestionariosSheet = workbook.getWorksheet('Cuestionarios');
|
||||||
|
const seccionesSheet = workbook.getWorksheet('Secciones');
|
||||||
|
const preguntasSheet = workbook.getWorksheet('Preguntas');
|
||||||
|
const opcionesSheet = workbook.getWorksheet('Opciones');
|
||||||
|
|
||||||
|
if (!eventosSheet || !cuestionariosSheet || !seccionesSheet || !preguntasSheet || !opcionesSheet) {
|
||||||
|
throw new UnprocessableEntityException('El archivo Excel debe contener las hojas: Eventos, Cuestionarios, Secciones, Preguntas y Opciones.');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 2; i <= eventosSheet.rowCount; i++) {
|
||||||
|
const row = eventosSheet.getRow(i);
|
||||||
|
|
||||||
|
// Construir objeto evento
|
||||||
|
const eventoDto = {
|
||||||
|
tipo_evento: row.getCell(2).value as string,
|
||||||
|
nombre_evento: row.getCell(3).value as string,
|
||||||
|
descripcion_evento: row.getCell(4).value as string,
|
||||||
|
fecha_inicio: new Date(row.getCell(5).value as string),
|
||||||
|
fecha_fin: new Date(row.getCell(6).value as string),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filtrar cuestionarios de este evento
|
||||||
|
const cuestionarios = cuestionariosSheet.getRows(2, cuestionariosSheet.rowCount - 1)
|
||||||
|
?.filter(qRow => qRow.getCell(2).value === row.getCell(1).value) || [];
|
||||||
|
|
||||||
|
for (const qRow of cuestionarios) {
|
||||||
|
// Filtrar secciones de este cuestionario
|
||||||
|
const secciones = seccionesSheet.getRows(2, seccionesSheet.rowCount - 1)
|
||||||
|
?.filter(sRow => sRow.getCell(2).value === qRow.getCell(1).value)
|
||||||
|
.map(sRow => {
|
||||||
|
// Filtrar preguntas de esta sección
|
||||||
|
const preguntas = preguntasSheet.getRows(2, preguntasSheet.rowCount - 1)
|
||||||
|
?.filter(pRow => pRow.getCell(2).value === sRow.getCell(1).value)
|
||||||
|
.map(pRow => {
|
||||||
|
const opciones = opcionesSheet.getRows(2, opcionesSheet.rowCount - 1)
|
||||||
|
?.filter(oRow => oRow.getCell(2).value === pRow.getCell(1).value)
|
||||||
|
.map(oRow => ({ valor: oRow.getCell(3).value as string })) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
titulo: pRow.getCell(3).value as string,
|
||||||
|
tipo: this.mapTipoPregunta(pRow.getCell(4).value as string),
|
||||||
|
obligatoria: !!pRow.getCell(5).value,
|
||||||
|
validacion: this.mapValidacion(pRow.getCell(6).value as string),
|
||||||
|
opciones,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
titulo: sRow.getCell(3).value as string,
|
||||||
|
descripcion: sRow.getCell(4).value as string,
|
||||||
|
preguntas,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
// Construir DTO completo
|
||||||
|
const createDto: CreateEventoWithCuestionarioDto = {
|
||||||
|
evento: eventoDto,
|
||||||
|
cuestionario: {
|
||||||
|
nombre_form: qRow.getCell(3).value as string,
|
||||||
|
descripcion: qRow.getCell(4).value as string,
|
||||||
|
fecha_inicio: new Date(qRow.getCell(5).value as string),
|
||||||
|
fecha_fin: new Date(qRow.getCell(6).value as string),
|
||||||
|
id_tipo_cuestionario: Number(qRow.getCell(7).value),
|
||||||
|
id_tipo_evento: Number(qRow.getCell(8).value), // obligatorio
|
||||||
|
secciones,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Llamada a tu servicio para guardar en DB
|
||||||
|
await this.cuestionarioService.createCuestionarioEvento(createDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: 'Carga masiva completada' };
|
||||||
|
}
|
||||||
|
|
||||||
@Post('withEvento')
|
@Post('withEvento')
|
||||||
@CuestionarioApiDocumentation.ApiCreateWithEvento
|
@CuestionarioApiDocumentation.ApiCreateWithEvento
|
||||||
createCuestionarioEvento(
|
createCuestionarioEvento(
|
||||||
@@ -40,6 +197,9 @@ export class CuestionarioController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Post(':id/banner')
|
@Post(':id/banner')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('banner', {
|
FileInterceptor('banner', {
|
||||||
|
|||||||
@@ -23,4 +23,8 @@ export class CreateEventoDto {
|
|||||||
@IsDate({ message: 'La fecha de fin debe ser una fecha válida.' })
|
@IsDate({ message: 'La fecha de fin debe ser una fecha válida.' })
|
||||||
@IsNotEmpty({ message: 'La fecha de fin es obligatoria.' })
|
@IsNotEmpty({ message: 'La fecha de fin es obligatoria.' })
|
||||||
fecha_fin: Date;
|
fecha_fin: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
banner?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,8 +229,6 @@ export class ParticipanteEventoService {
|
|||||||
|
|
||||||
// Verificar si ya había registrado asistencia
|
// Verificar si ya había registrado asistencia
|
||||||
if (participanteEvento.asistio && participanteEvento.fecha_asistencia) {
|
if (participanteEvento.asistio && participanteEvento.fecha_asistencia) {
|
||||||
//Borarr
|
|
||||||
console.log("Usuario se registro exitosamente");
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
@@ -250,8 +248,7 @@ export class ParticipanteEventoService {
|
|||||||
const resultado =
|
const resultado =
|
||||||
await this.participanteEventoRepository.save(participanteEvento);
|
await this.participanteEventoRepository.save(participanteEvento);
|
||||||
|
|
||||||
//Borrar
|
|
||||||
console.log("El usaurio se registro exitosamente");
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Asistencia registrada exitosamente',
|
message: 'Asistencia registrada exitosamente',
|
||||||
|
|||||||
Reference in New Issue
Block a user