Add xlsx support and enhance Trabajadores module with file upload functionality
- Updated package.json and package-lock.json to include xlsx dependency. - Enhanced AlumnosService to return UsuarioData type. - Modified CuestionarioRespondidoService to include event start and end dates in responses. - Updated TrabajadoresController to add file upload endpoint for processing Excel files. - Implemented file processing logic in TrabajadoresService to handle worker registration from Excel. - Created TrabajadorApiDocumentation for API documentation of the new file upload feature. - Refactored RegistroTrabajador entity to include carrera field and changed num_trabajador type.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
|
||||
import { AlumnosService } from './alumnos.service';
|
||||
import { AlumnosService, UsuarioData } from './alumnos.service';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||
import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
|
||||
|
||||
@@ -14,7 +14,7 @@ export class AlumnosController {
|
||||
@ApiResponse(ALUMNO_RESPONSES[200])
|
||||
@ApiResponse(ALUMNO_RESPONSES[404])
|
||||
@ApiResponse(ALUMNO_RESPONSES[500])
|
||||
async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise<AlumnoDto | null> {
|
||||
async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise<UsuarioData | null> {
|
||||
const alumno = await this.alumnosService.findByCuenta(cuenta);
|
||||
if (!alumno) {
|
||||
throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`);
|
||||
|
||||
@@ -3,6 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
||||
|
||||
export type UsuarioData = {
|
||||
cuenta: string | null;
|
||||
nombre: string | null;
|
||||
apellidos: string | null;
|
||||
carrera: string | null;
|
||||
genero: string | null;
|
||||
rfc?: string | null; // Solo para trabajadores
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AlumnosService {
|
||||
constructor(
|
||||
@@ -10,9 +19,17 @@ export class AlumnosService {
|
||||
private registroAlumnoRepository: Repository<RegistroAlumno>,
|
||||
) {}
|
||||
|
||||
async findByCuenta(cuenta: string): Promise<RegistroAlumno | null> {
|
||||
return this.registroAlumnoRepository.findOne({
|
||||
async findByCuenta(cuenta: string): Promise<UsuarioData | null> {
|
||||
const alumno = await this.registroAlumnoRepository.findOne({
|
||||
where: { id_ncuenta: parseInt(cuenta, 10) },
|
||||
});
|
||||
|
||||
return {
|
||||
cuenta: alumno?.id_ncuenta.toString() || null,
|
||||
nombre: alumno?.nombre || null,
|
||||
apellidos: alumno?.apellidos || null,
|
||||
carrera: alumno?.carrera || null,
|
||||
genero: alumno?.genero || null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,12 @@ export class CuestionarioRespondidoService {
|
||||
nombreEvento: cuestionario.evento?.nombre_evento,
|
||||
correo: participante.correo,
|
||||
fechaRegistro: new Date().toLocaleString(),
|
||||
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
||||
? new Date(cuestionario.evento.fecha_inicio).toLocaleString()
|
||||
: undefined,
|
||||
fechaFinEvento: cuestionario.evento?.fecha_fin
|
||||
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
||||
: undefined,
|
||||
}),
|
||||
adjuntos: [
|
||||
{
|
||||
|
||||
@@ -3,12 +3,27 @@ export function generarHtmlCorreoAsistencia({
|
||||
nombreEvento,
|
||||
correo,
|
||||
fechaRegistro,
|
||||
fechaInicioEvento,
|
||||
fechaFinEvento,
|
||||
}: {
|
||||
nombreForm: string;
|
||||
nombreEvento: string;
|
||||
correo: string;
|
||||
fechaRegistro: string;
|
||||
fechaInicioEvento?: string;
|
||||
fechaFinEvento?: string;
|
||||
}) {
|
||||
const horarioEvento = fechaInicioEvento && fechaFinEvento
|
||||
? `
|
||||
<div style="margin: 20px 0; padding: 12px; background-color: #e8f0fe; border-left: 4px solid #003d79;">
|
||||
<p style="margin: 0; font-size: 15px; color: #003d79;">
|
||||
<strong>🕒 Horario del evento:</strong><br/>
|
||||
${fechaInicioEvento} <strong>al</strong> ${fechaFinEvento}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 24px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9;">
|
||||
<h1 style="color: #003d79; border-bottom: 2px solid #003d79; padding-bottom: 10px;">🎓 ¡Gracias por registrarte!</h1>
|
||||
@@ -32,6 +47,8 @@ export function generarHtmlCorreoAsistencia({
|
||||
<li><strong>Fecha de registro:</strong> ${fechaRegistro}</li>
|
||||
</ul>
|
||||
|
||||
${horarioEvento}
|
||||
|
||||
<p style="margin-top: 30px; font-size: 14px; color: #666;">
|
||||
Si tienes alguna duda, acude al módulo de información durante el evento.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiConsumes,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
export class TrabajadorApiDocumentation {
|
||||
static ApiController = ApiTags('Trabajador');
|
||||
|
||||
static ApiCargarArchivo = applyDecorators(
|
||||
ApiOperation({ summary: 'Cargar archivo Excel con trabajadores' }),
|
||||
ApiConsumes('multipart/form-data'),
|
||||
ApiBody({
|
||||
description: 'Archivo Excel (.xls o .xlsx) con columnas: rfc, num_empleado, nombre, adscripcion, correo, AP1, AP2, Nombres, sexo',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'Archivo Excel con trabajadores a registrar',
|
||||
},
|
||||
},
|
||||
required: ['file'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ export class RegistroTrabajador {
|
||||
@PrimaryGeneratedColumn()
|
||||
id_trabajador: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, unique: true, nullable: true })
|
||||
numero_trabajador: string;
|
||||
@Column({ type: 'int', unique: true, nullable: true })
|
||||
num_trabajador: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 13, unique: true })
|
||||
rfc: string;
|
||||
@@ -19,4 +19,7 @@ export class RegistroTrabajador {
|
||||
|
||||
@Column({ type: 'char', length: 1 })
|
||||
genero: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
carrera: string;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { TrabajadoresService } from './trabajadores.service';
|
||||
import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
|
||||
import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
import { TrabajadorApiDocumentation } from './docs/trabajadores.docs';
|
||||
|
||||
@Controller('trabajadores')
|
||||
@TrabajadorApiDocumentation.ApiController
|
||||
export class TrabajadoresController {
|
||||
constructor(private readonly trabajadoresService: TrabajadoresService) {}
|
||||
|
||||
@Post('cargar')
|
||||
@TrabajadorApiDocumentation.ApiCargarArchivo
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: diskStorage({
|
||||
destination: './uploads',
|
||||
filename: (_, file, callback) => {
|
||||
const uniqueName = `${Date.now()}${extname(file.originalname)}`;
|
||||
callback(null, uniqueName);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
async cargarArchivo(@UploadedFile() file: Express.Multer.File) {
|
||||
return this.trabajadoresService.procesarArchivo(file.path);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() createTrabajadoreDto: CreateTrabajadoreDto) {
|
||||
return this.trabajadoresService.create(createTrabajadoreDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.trabajadoresService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.trabajadoresService.findOne(+id);
|
||||
@Get(':rfc')
|
||||
findByRfc(@Param('rfc') rfc: string) {
|
||||
const trabajador = this.trabajadoresService.findByRfc(rfc);
|
||||
if (!trabajador) {
|
||||
throw new NotFoundException(`No se encontró trabajador con RFC ${rfc}`);
|
||||
}
|
||||
return trabajador;
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateTrabajadoreDto: UpdateTrabajadoreDto) {
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateTrabajadoreDto: UpdateTrabajadoreDto,
|
||||
) {
|
||||
return this.trabajadoresService.update(+id, updateTrabajadoreDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { RegistroTrabajador } from './entities/trabajadore.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection')],
|
||||
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection'),
|
||||
],
|
||||
controllers: [TrabajadoresController],
|
||||
providers: [TrabajadoresService],
|
||||
exports: [TrabajadoresService],
|
||||
})
|
||||
export class TrabajadoresModule {}
|
||||
|
||||
@@ -1,9 +1,91 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
|
||||
import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { RegistroTrabajador } from './entities/trabajadore.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as fs from 'fs';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { UsuarioData } from 'src/alumnos/alumnos.service';
|
||||
|
||||
@Injectable()
|
||||
export class TrabajadoresService {
|
||||
constructor(
|
||||
@InjectRepository(RegistroTrabajador, 'alumnosConnection')
|
||||
private readonly trabajadorRepo: Repository<RegistroTrabajador>,
|
||||
) {}
|
||||
|
||||
async procesarArchivo(
|
||||
filePath: string,
|
||||
): Promise<{ insertados: number; omitidos: number }> {
|
||||
const workbook = XLSX.readFile(filePath);
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const data: any[] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
|
||||
|
||||
let insertados = 0;
|
||||
let omitidos = 0;
|
||||
|
||||
for (const row of data) {
|
||||
const rfc = (row['rfc'] || '').trim();
|
||||
|
||||
if (!rfc) {
|
||||
omitidos++;
|
||||
continue; // RFC es obligatorio
|
||||
}
|
||||
|
||||
const numTrabajadorRaw = row['num_empleado'];
|
||||
const numTrabajador = Number.isFinite(Number(numTrabajadorRaw))
|
||||
? Number(numTrabajadorRaw)
|
||||
: undefined;
|
||||
|
||||
const where: any[] = [{ rfc }];
|
||||
if (numTrabajador !== undefined) {
|
||||
where.push({ num_trabajador: numTrabajador });
|
||||
}
|
||||
|
||||
const existe = await this.trabajadorRepo.findOne({ where });
|
||||
|
||||
if (existe) {
|
||||
omitidos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nuevo = this.trabajadorRepo.create({
|
||||
rfc,
|
||||
num_trabajador: numTrabajador,
|
||||
nombre: (row['Nombres'] || '').trim(),
|
||||
apellidos:
|
||||
`${(row['AP1'] || '').trim()} ${(row['AP2'] || '').trim()}`.trim(),
|
||||
genero: (row['sexo'] || '').trim().substring(0, 1).toUpperCase(),
|
||||
carrera: (row['adscripcion'] || '').trim(),
|
||||
});
|
||||
|
||||
await this.trabajadorRepo.save(nuevo);
|
||||
insertados++;
|
||||
}
|
||||
|
||||
fs.unlinkSync(filePath); // Limpia archivo temporal
|
||||
|
||||
return { insertados, omitidos };
|
||||
}
|
||||
|
||||
async findByRfc(rfc: string): Promise<UsuarioData | null> {
|
||||
const trabajador = await this.trabajadorRepo.findOne({
|
||||
where: { rfc: rfc.trim().toUpperCase() },
|
||||
});
|
||||
|
||||
return {
|
||||
cuenta: trabajador?.num_trabajador?.toString() || null,
|
||||
nombre: trabajador?.nombre || null,
|
||||
apellidos: trabajador
|
||||
? `${trabajador.apellidos}`.trim()
|
||||
: null,
|
||||
carrera: trabajador?.carrera || null,
|
||||
genero: trabajador?.genero || null,
|
||||
rfc: trabajador?.rfc || null, // Incluye RFC en el retorno
|
||||
}
|
||||
}
|
||||
|
||||
create(createTrabajadoreDto: CreateTrabajadoreDto) {
|
||||
return 'This action adds a new trabajadore';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user