From 09fda8af1228e2c71ac9ecc54fe42c7a5dac9a72 Mon Sep 17 00:00:00 2001 From: Emilio Date: Thu, 16 Oct 2025 14:04:30 -0600 Subject: [PATCH] Se agregaron funciones y cambios en los dto --- package-lock.json | 76 +- package.json | 1 + src/auth/auth.service.ts | 11 +- .../dto/create-caso-especial.dto.ts | 143 ++- .../dto/update-caso-especial.dto.ts | 100 +- .../cuestionario-alumno2.service.ts | 2 +- .../cuestionario-programa2.service.ts | 2 +- src/helpers.services/dto/send-email.dto.ts | 47 +- src/helpers.services/gmail.service.ts | 113 +- src/programa/programa.controller.ts | 125 +- src/programa/programa.service.ts | 8 +- src/servicio/dto/create-servicio.dto.ts | 43 +- src/servicio/dto/registro-validado.dto.ts | 26 + src/servicio/dto/servicios-admin.dto.ts | 19 + src/servicio/servicio.module.ts | 16 +- src/servicio/servicio.service.ts | 1034 ++++++++++++++++- 16 files changed, 1470 insertions(+), 296 deletions(-) create mode 100644 src/servicio/dto/registro-validado.dto.ts create mode 100644 src/servicio/dto/servicios-admin.dto.ts diff --git a/package-lock.json b/package-lock.json index 09ae172..9ba315b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.6", "@nestjs/typeorm": "^11.0.0", + "argon2": "^0.44.0", "axios": "^1.12.2", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", @@ -794,6 +795,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2576,12 +2583,12 @@ } }, "node_modules/@nestjs/jwt": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", - "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", + "integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==", "license": "MIT", "dependencies": { - "@types/jsonwebtoken": "9.0.7", + "@types/jsonwebtoken": "9.0.10", "jsonwebtoken": "9.0.2" }, "peerDependencies": { @@ -2774,6 +2781,15 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3410,11 +3426,12 @@ "license": "MIT" }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", - "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { + "@types/ms": "*", "@types/node": "*" } }, @@ -3432,6 +3449,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/multer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", @@ -5167,6 +5190,22 @@ "devOptional": true, "license": "MIT" }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6165,6 +6204,23 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9928,9 +9984,9 @@ "license": "MIT" }, "node_modules/node-addon-api": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", - "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" diff --git a/package.json b/package.json index 50e9227..170f8b0 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.6", "@nestjs/typeorm": "^11.0.0", + "argon2": "^0.44.0", "axios": "^1.12.2", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 4eea4e4..7f3ab81 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -14,6 +14,7 @@ import { gmail } from 'src/helpers.services/gmail.service'; import { Servicio } from 'src/servicio/entities/servicio.entity'; import { Usuario } from 'src/usuario/entities/usuario.entity'; import { Like, Not, Repository } from 'typeorm'; +import * as argon2 from 'argon2'; @Injectable() export class AuthService { @@ -80,15 +81,15 @@ export class AuthService { } async comparar(password, dbPassword) { - if (!bcrypt.compareSync(password, dbPassword)) { - return false; - } else { + if (await argon2.verify(password, dbPassword)) { return true; + } else { + return false; } } async encriptar(password) { - return bcrypt.hashSync(password, Number(process.env.SALT_ROUNDS)); + return await argon2.hash(password); } async generarPassword() { @@ -107,7 +108,7 @@ export class AuthService { async login(usuario: string, password: string) { const user = await this.userRepo.findOne({ where: { usuario } }); if (!user) throw new UnauthorizedException('No existe este usuario.'); - const match = await bcrypt.compare(password, user.password); + const match = await this.comparar(password, user.password); if (!match) throw new UnauthorizedException('Credenciales inválidas'); if (!user.activo) throw new Error('Este usuario no esta activo.'); const token = this.jwtCreate( diff --git a/src/caso-especial/dto/create-caso-especial.dto.ts b/src/caso-especial/dto/create-caso-especial.dto.ts index 8dacdb4..5a2b525 100644 --- a/src/caso-especial/dto/create-caso-especial.dto.ts +++ b/src/caso-especial/dto/create-caso-especial.dto.ts @@ -1,73 +1,72 @@ import { - IsEmail, - IsNotEmpty, - IsNumberString, - IsOptional, - IsString, - Length, - IsDateString, - IsNumber, - IsIn, - IsInt, - Matches, - } from 'class-validator'; - - export class CreateCasoEspecialDto { - @IsNumber() - - idUsuario: number; - - @IsNumber() - @IsInt() - idCarrera: number; - - @IsNumber() - @Length(1, 11) - idStatus: number; - - @IsString() - @Length(9, 9) - @Matches(/^\d+$/, { message: 'solo puede contener números' }) - numeroCuenta: string; - - @IsEmail() - correo: string; - - @IsString() - @Length(1, 3) - @Matches(/^\d+$/, { message: 'solo puede contener números' }) - creditos : string; - - @IsString() - @Length(1, 15) - @Matches(/^\d+$/, { message: 'solo puede contener números' }) - telefono: string; - - @IsDateString() - fechaInicio: Date; - - @IsDateString() - fechaFin: Date; - - @IsDateString() - fechaNacimiento: Date; - - @IsString() - @Length(1, 200) - direccion: string; - - @IsOptional() - @IsString() - @Length(1, 200) - institucion?: string; - - @IsOptional() - @IsString() - @Length(1, 200) - dependencia?: string; - - @IsOptional() - @IsNumber() - @Length(1, 1) - motivo?: string; - } \ No newline at end of file + IsEmail, + IsNotEmpty, + IsNumberString, + IsOptional, + IsString, + Length, + IsDateString, + IsNumber, + IsIn, + IsInt, + Matches, +} from 'class-validator'; + +export class CreateCasoEspecialDto { + @IsNumber() + idUsuario: number; + + @IsNumber() + @IsInt() + idCarrera: number; + + @IsNumber() + @Length(1, 11) + idStatus: number; + + @IsString() + @Length(9, 9) + @Matches(/^\d+$/, { message: 'solo puede contener números' }) + numeroCuenta: string; + + @IsEmail() + correo: string; + + @IsString() + @Length(1, 3) + @Matches(/^\d+$/, { message: 'solo puede contener números' }) + creditos: string; + + @IsString() + @Length(1, 15) + @Matches(/^\d+$/, { message: 'solo puede contener números' }) + telefono: string; + + @IsDateString() + fechaInicio: Date; + + @IsDateString() + fechaFin: Date; + + @IsDateString() + fechaNacimiento: Date; + + @IsString() + @Length(1, 200) + direccion: string; + + @IsOptional() + @IsString() + @Length(1, 200) + institucion?: string; + + @IsOptional() + @IsString() + @Length(1, 200) + dependencia?: string; + + @IsOptional() // Permite que no se envíe + @IsString() // Debe ser string si se envía + @Length(1, 1) + motivo?: string; +} diff --git a/src/caso-especial/dto/update-caso-especial.dto.ts b/src/caso-especial/dto/update-caso-especial.dto.ts index ec2d368..234412d 100644 --- a/src/caso-especial/dto/update-caso-especial.dto.ts +++ b/src/caso-especial/dto/update-caso-especial.dto.ts @@ -1,51 +1,51 @@ import { - IsOptional, - IsString, - IsEmail, - Length, - IsDateString, - IsNumber, - } from 'class-validator'; - - export class UpdateCasoEspecialDto { - @IsOptional() - @IsEmail() - correo?: string; - - @IsOptional() - @IsDateString() - fechaInicio?: Date; - - @IsOptional() - @IsDateString() - fechaFin?: Date; - - @IsOptional() - @IsDateString() - fechaNacimiento?: Date; - - @IsOptional() - @IsString() - @Length(1, 200) - direccion?: string; - - @IsOptional() - @IsString() - @Length(1, 15) - telefono?: string; - - @IsOptional() - @IsString() - @Length(1, 200) - institucion?: string; - - @IsOptional() - @IsString() - @Length(1, 200) - dependencia?: string; - - @IsOptional() - @IsNumber() - motivo?: number; - } - \ No newline at end of file + IsOptional, + IsString, + IsEmail, + Length, + IsDateString, + IsNumber, +} from 'class-validator'; + +export class UpdateCasoEspecialDto { + @IsOptional() + @IsEmail() + correo?: string; + + @IsOptional() + @IsDateString() + fechaInicio?: Date; + + @IsOptional() + @IsDateString() + fechaFin?: Date; + + @IsOptional() + @IsDateString() + fechaNacimiento?: Date; + + @IsOptional() + @IsString() + @Length(1, 200) + direccion?: string; + + @IsOptional() + @IsString() + @Length(1, 15) + telefono?: string; + + @IsOptional() + @IsString() + @Length(1, 200) + institucion?: string; + + @IsOptional() + @IsString() + @Length(1, 200) + dependencia?: string; + + @IsOptional() // Permite que no se envíe + @IsString() // Debe ser string si se envía + @Length(1, 1) + motivo?: string; +} diff --git a/src/cuestionario-alumno2/cuestionario-alumno2.service.ts b/src/cuestionario-alumno2/cuestionario-alumno2.service.ts index 1047f14..76b977d 100644 --- a/src/cuestionario-alumno2/cuestionario-alumno2.service.ts +++ b/src/cuestionario-alumno2/cuestionario-alumno2.service.ts @@ -8,7 +8,7 @@ import { CuestionarioAlumno2 } from './entities/cuestionario-alumno2.entity'; import { messageByStatus } from 'src/messageByStatus'; import { ArchivoService } from 'src/helpers.services/archivo.service'; import { ValidacionService } from 'src/helpers.services/validacion.service'; -const { convertArrayToCSV } = require('convert-array-to-csv'); +import { convertArrayToCSV } from 'convert-array-to-csv'; @Injectable() export class CuestionarioAlumno2Service { diff --git a/src/cuestionario-programa2/cuestionario-programa2.service.ts b/src/cuestionario-programa2/cuestionario-programa2.service.ts index 9f7add1..ac79782 100644 --- a/src/cuestionario-programa2/cuestionario-programa2.service.ts +++ b/src/cuestionario-programa2/cuestionario-programa2.service.ts @@ -8,7 +8,7 @@ import { ArchivoService } from 'src/helpers.services/archivo.service'; import { ValidacionService } from 'src/helpers.services/validacion.service'; import { Servicio } from 'src/servicio/entities/servicio.entity'; import { messageByStatus } from 'src/messageByStatus'; -const { convertArrayToCSV } = require('convert-array-to-csv'); +import { convertArrayToCSV } from 'convert-array-to-csv'; @Injectable() export class CuestionarioPrograma2Service { diff --git a/src/helpers.services/dto/send-email.dto.ts b/src/helpers.services/dto/send-email.dto.ts index 70eeee1..b9cd4bf 100644 --- a/src/helpers.services/dto/send-email.dto.ts +++ b/src/helpers.services/dto/send-email.dto.ts @@ -1,27 +1,46 @@ -import { IsString, IsEmail, IsDate, IsOptional } from 'class-validator'; +import { + IsString, + IsArray, + IsOptional, + IsISO8601, + ValidateNested, +} from 'class-validator'; import { Type } from 'class-transformer'; -export class SendCorreoDto { - @IsEmail() +class AdjuntoDto { + @IsString() + filename: string; + + @IsString() + content: string; + + @IsString() + encoding: string; + + @IsOptional() + @IsString() + cid?: string; +} + +export class SendMailDto { + @IsString() to: string; @IsString() - @IsOptional() - subject?: string; + subject: string; @IsString() - @IsOptional() - text?: string; + text: string; @IsString() - @IsOptional() - html?: string; + html: string; - @IsString() - @IsOptional() - fileName?: string; + @IsISO8601() + fecha_recibido: string; - @IsString() @IsOptional() - filePath?: string; + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AdjuntoDto) + adjuntos?: AdjuntoDto[]; } diff --git a/src/helpers.services/gmail.service.ts b/src/helpers.services/gmail.service.ts index 0365eea..f4cc222 100644 --- a/src/helpers.services/gmail.service.ts +++ b/src/helpers.services/gmail.service.ts @@ -1,93 +1,34 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import * as nodemailer from 'nodemailer'; -import { SendCorreoDto } from './dto/send-email.dto'; - - - - +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import axios from 'axios'; +import { SendMailDto } from './dto/send-email.dto'; @Injectable() export class gmail { + private readonly baseUrl = process.env.MAIL_URL; + private readonly token = process.env.MAIL_TOKEN; // idealmente: process.env.MAIL_TOKEN - async sendMail(sendEmail: SendCorreoDto) { - - - - - - - const transporter = nodemailer.createTransport({ - - - service: 'gmail', - auth: { - type: 'OAuth2', - user: process.env.EMAIL_USER, // tu correo de Gmail - clientId: process.env.CLIENT_ID, // tu Client ID de OAuth2 - clientSecret: process.env.CLIENT_SECRET, // tu Client Secret de OAuth2 - refreshToken: process.env.REFRESH_TOKEN, // tu Refresh Token de OAuth2 - //accessToken: sistem.accessToken, // opcional - }, - pool: true, - maxConnections: 1, - maxMessages: 100, - rateDelta: 2000, - rateLimit: 1 - - - }); - - - - - - - const mailOptions = { - from: process.env.EMAIL_USER, // tu correo de Gmail - to: sendEmail.to, - subject: sendEmail.subject, - text: sendEmail.text, - html: sendEmail.html, - - attachments: [ - { - filename: '', - path: '', // ruta local + async enviarCorreo(data: SendMailDto): Promise { + try { + if (!this.baseUrl || !this.token) { + throw new HttpException( + 'Configuración de correo no encontrada', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const response = await axios.post(this.baseUrl, data, { + headers: { + 'Content-Type': 'application/json', + accept: '*/*', + token: this.token, }, - { - filename: '', - path: '', // adjuntar desde URL - }, - { - filename: '', - content: '', // adjunto generado en memoria - }, - ], + }); - }; - - - console.log(mailOptions); - - - - - let resMail = await transporter.sendMail(mailOptions); - if (!resMail) throw new Error("") - - - const statusTexto = resMail.accepted.length > 0 ? "Enviado" : "Fallido"; - - - - if (!resMail) { - throw new Error("fallo") - } - - - return (statusTexto); + return response.data; + } catch (error: any) { + throw new HttpException( + error.response?.data || 'Error al enviar correo', + error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } - - - -} \ No newline at end of file + } +} diff --git a/src/programa/programa.controller.ts b/src/programa/programa.controller.ts index b95e39a..fa0a421 100644 --- a/src/programa/programa.controller.ts +++ b/src/programa/programa.controller.ts @@ -1,34 +1,119 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { + Controller, + Post, + Put, + Get, + Body, + Query, + UploadedFile, + UseGuards, + UseInterceptors, + BadRequestException, + Param, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import * as fs from 'fs'; +import { AuthGuard } from '@nestjs/passport'; + import { ProgramaService } from './programa.service'; -import { CreateProgramaDto } from './dto/create-programa.dto'; -import { UpdateProgramaDto } from './dto/update-programa.dto'; +import { IsEmail, isEmail } from 'class-validator'; + +class Email { + @IsEmail() + correo: string; +} @Controller('programa') export class ProgramaController { constructor(private readonly programaService: ProgramaService) {} - @Post() - create(@Body() createProgramaDto: CreateProgramaDto) { - return this.programaService.create(createProgramaDto); + // ==================== POST /carga_masiva ==================== + @Post('carga_masiva') + @UseGuards(AuthGuard('jwt')) + @UseInterceptors( + FileInterceptor('csv', { + storage: diskStorage({ + destination: './server/uploads', + filename: (req, file, cb) => { + const uniqueName = `${Date.now()}${extname(file.originalname)}`; + cb(null, uniqueName); + }, + }), + }), + ) + async cargaMasiva(@UploadedFile() file: Express.Multer.File) { + if (!file) { + throw new BadRequestException( + 'No se envio un archivo csv para la carga masiva.', + ); + } + try { + const data = await this.programaService.cargaMasiva(file.filename); + return { + statusCode: 201, + message: 'Carga masiva realizada con éxito', + data, + }; + } catch (err) { + // Eliminar archivo si ocurre error + if (file && file.filename) { + fs.unlinkSync(`./server/uploads/${file.filename}`); + } + throw new BadRequestException(err.message); + } } - @Get() - findAll() { - return this.programaService.findAll(); + // ==================== PUT /reasignar_programas ==================== + @Put('reasignar_programas/:idUsuario') + @UseGuards(AuthGuard('jwt')) + async reasignarProgramas( + @Body() email: Email, + @Param('idUsuario') idUsuario: number, + ) { + try { + const data = await this.programaService.reasignarProgramas( + idUsuario, + email.correo, + ); + return { + statusCode: 200, + message: 'Programas reasignados con éxito', + data, + }; + } catch (err) { + throw new BadRequestException(err.message); + } } - @Get(':id') - findOne(@Param('id') id: string) { - return this.programaService.findOne(+id); + // ==================== GET /programas_admin ==================== + @Get('programas_admin/:idUsuario') + @UseGuards(AuthGuard('jwt')) + async programasAdmin(@Param('idUsuario') query: number) { + try { + const data = await this.programaService.programasAdmin(query); + return { + statusCode: 200, + data, + }; + } catch (err) { + throw new BadRequestException(err.message); + } } - @Patch(':id') - update(@Param('id') id: string, @Body() updateProgramaDto: UpdateProgramaDto) { - return this.programaService.update(+id, updateProgramaDto); - } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.programaService.remove(+id); + // ==================== GET /programas_responsable ==================== + @Get('programas_responsable/:idUsuario') + @UseGuards(AuthGuard('jwt')) + async programasResponsable(@Param(`idUsuario`) idUsuario: number) { + try { + const data = await this.programaService.programasResponsable(idUsuario); + return { + statusCode: 200, + data, + }; + } catch (err) { + throw new BadRequestException(err.message); + } } } diff --git a/src/programa/programa.service.ts b/src/programa/programa.service.ts index b22a744..a2f5e77 100644 --- a/src/programa/programa.service.ts +++ b/src/programa/programa.service.ts @@ -179,9 +179,9 @@ export class ProgramaService { }; } - async programasAdmin(body: { idUsuario: number }) { + async programasAdmin(idUser: number) { const idUsuario = this.validacionService.validarNumeroEntero( - body.idUsuario, + idUser, 'id usuario', ); @@ -201,9 +201,9 @@ export class ProgramaService { }); } - async programasResponsable(body: { idUsuario: number }) { + async programasResponsable(idUser: number) { const idUsuario = this.validacionService.validarNumeroEntero( - body.idUsuario, + idUser, 'id usuario', ); diff --git a/src/servicio/dto/create-servicio.dto.ts b/src/servicio/dto/create-servicio.dto.ts index edd74e9..5f17a67 100644 --- a/src/servicio/dto/create-servicio.dto.ts +++ b/src/servicio/dto/create-servicio.dto.ts @@ -1 +1,42 @@ -export class CreateServicioDto {} +import { + IsNumber, + IsOptional, + IsString, + IsDateString, + IsEmail, + IsDate, +} from 'class-validator'; + +export class CrearServicioDto { + @IsNumber() + idUsuario: number; + + @IsNumber() + idPrograma: number; + + @IsNumber() + idCarrera: number; + + @IsString() + numeroCuenta: string; + + @IsNumber() + creditos: number; + + @IsEmail() + correo: string; + + @IsDate() + fechaInicio: Date; + + @IsDate() + fechaFin: Date; + + @IsOptional() + @IsString() + programaInterno?: string; + + @IsOptional() + @IsString() + profesor?: string; +} diff --git a/src/servicio/dto/registro-validado.dto.ts b/src/servicio/dto/registro-validado.dto.ts new file mode 100644 index 0000000..1675974 --- /dev/null +++ b/src/servicio/dto/registro-validado.dto.ts @@ -0,0 +1,26 @@ +import { + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + MaxLength, +} from 'class-validator'; + +export class RegistroValidadoDto { + @IsNumber() + @IsNotEmpty() + idServicio: number; + + @IsOptional() + fechaNacimiento?: Date; + + @IsOptional() + @IsString() + @MaxLength(15) + telefono?: string; + + @IsOptional() + @IsString() + @MaxLength(200) + direccion?: string; +} diff --git a/src/servicio/dto/servicios-admin.dto.ts b/src/servicio/dto/servicios-admin.dto.ts new file mode 100644 index 0000000..6675aea --- /dev/null +++ b/src/servicio/dto/servicios-admin.dto.ts @@ -0,0 +1,19 @@ +import { IsOptional, IsNumber, IsString } from 'class-validator'; + +export class ServiciosAdminDto { + @IsOptional() + @IsNumber() + pagina?: number; + + @IsOptional() + @IsNumber() + idStatus?: number; + + @IsOptional() + @IsString() + nombre?: string; + + @IsOptional() + @IsString() + numeroCuenta?: string; +} diff --git a/src/servicio/servicio.module.ts b/src/servicio/servicio.module.ts index 7c691f3..a0e70fe 100644 --- a/src/servicio/servicio.module.ts +++ b/src/servicio/servicio.module.ts @@ -1,9 +1,23 @@ import { Module } from '@nestjs/common'; import { ServicioService } from './servicio.service'; import { ServicioController } from './servicio.controller'; +import { DriveService } from 'src/drive/drive.service'; +import { ValidacionService } from 'src/helpers.services/validacion.service'; +import { gmail } from 'src/helpers.services/gmail.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Servicio } from './entities/servicio.entity'; +import { Usuario } from 'src/usuario/entities/usuario.entity'; +import { ArchivoService } from 'src/helpers.services/archivo.service'; @Module({ + imports: [TypeOrmModule.forFeature([Servicio, Usuario])], controllers: [ServicioController], - providers: [ServicioService], + providers: [ + ServicioService, + DriveService, + ValidacionService, + gmail, + ArchivoService, + ], }) export class ServicioModule {} diff --git a/src/servicio/servicio.service.ts b/src/servicio/servicio.service.ts index 0325936..1623225 100644 --- a/src/servicio/servicio.service.ts +++ b/src/servicio/servicio.service.ts @@ -1,27 +1,53 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { CreateServicioDto } from './dto/create-servicio.dto'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CrearServicioDto } from './dto/create-servicio.dto'; import { UpdateServicioDto } from './dto/update-servicio.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Between, Not, Repository } from 'typeorm'; import { Servicio } from './entities/servicio.entity'; import { Usuario } from 'src/usuario/entities/usuario.entity'; import { ValidacionService } from 'src/helpers.services/validacion.service'; import { gmail } from 'src/helpers.services/gmail.service'; import { SendCorreoDto } from 'src/helpers.services/dto/send-email.dto'; +import { DriveService } from 'src/drive/drive.service'; +import moment from 'moment'; +import { ArchivoService } from 'src/helpers.services/archivo.service'; +import { convertArrayToCSV } from 'convert-array-to-csv'; +import { Programa } from 'src/programa/entities/programa.entity'; +import { Carrera } from 'src/carrera/entities/carrera.entity'; +import { AuthService } from 'src/auth/auth.service'; +import { RegistroValidadoDto } from './dto/registro-validado.dto'; +import * as fs from 'fs'; +import { ServiciosAdminDto } from './dto/servicios-admin.dto'; @Injectable() export class ServicioService { constructor( @InjectRepository(Servicio) - private servicioRepo:Repository, + private servicioRepo: Repository, @InjectRepository(Usuario) - private usuarioRepo:Repository, + private usuarioRepo: Repository, - private validacionService:ValidacionService, - - private gmailService:gmail - ){} + @InjectRepository(Programa) + private programaRepo: Repository, + + @InjectRepository(Carrera) + private carreraRepo: Repository, + + private validacionService: ValidacionService, + + private driveService: DriveService, + + private gmailService: gmail, + + private archivoService: ArchivoService, + + private authService: AuthService, + ) {} async obtenerServicioAlumno(idUsuario: number) { const usuario = await this.usuarioRepo.findOne({ @@ -81,12 +107,7 @@ export class ServicioService { return servicio; } - - - - - - async obtenerDetalleServicio(idServicio:number) { + async obtenerDetalleServicio(idServicio: number) { const servicio = await this.servicioRepo .createQueryBuilder('servicio') .leftJoinAndSelect('servicio.usuario', 'usuario') @@ -122,7 +143,7 @@ export class ServicioService { 'usuario.usuario', 'usuario.nombre', - 'tipoUsuario.idTipoUsuario', + 'tipoUsuario.idTipoUsuario', 'carrera.idCarrera', 'carrera.nombre', @@ -149,13 +170,20 @@ export class ServicioService { return servicio; } - - async cancelarServicio(id: number,) { - const idServicio = this.validacionService.validarNumeroEntero(body.idServicio, 'id servicio'); - const mensaje = this.validacionService.validarAlfanumerico(body.mensaje, 'mensaje', true, 800); + async cancelarServicio(body: { idServicio: number; mensaje: string }) { + const idServicio = this.validacionService.validarNumeroEntero( + body.idServicio, + 'id servicio', + ); + const mensaje = this.validacionService.validarAlfanumerico( + body.mensaje, + 'mensaje', + true, + 800, + ); const servicio = await this.servicioRepo.findOne({ - where:{ idServicio }, + where: { idServicio }, relations: [ 'usuario', // alumno 'programa', @@ -172,7 +200,9 @@ export class ServicioService { } if (servicio.status.idStatus === 6) { - throw new BadRequestException('Este Servicio Social ya fue finalizado, no se puede cancelar.'); + throw new BadRequestException( + 'Este Servicio Social ya fue finalizado, no se puede cancelar.', + ); } const alumno = servicio.usuario; @@ -187,7 +217,7 @@ export class ServicioService { fecha_recibido: new Date(), text: correoAlumno.msj, html: '', - adjuntos: undefined + adjuntos: undefined, }; const sendCorreoResponsableDto: SendCorreoDto = { @@ -196,20 +226,25 @@ export class ServicioService { fecha_recibido: new Date(), text: correoResponsable.msj, html: '', - adjuntos: undefined + adjuntos: undefined, }; - + if (!process.env.TOKEN_GMAIL) { throw new Error('No existe el token'); } - if (servicio.correo) { - await this.gmailService.send(sendCorreoAlumnoDto, process.env.TOKEN_GMAIL); + await this.gmailService.send( + sendCorreoAlumnoDto, + process.env.TOKEN_GMAIL, + ); } if (responsable.usuario) { - await this.gmailService.send(sendCorreoResponsableDto,process.env.TOKEN_GMAIL); + await this.gmailService.send( + sendCorreoResponsableDto, + process.env.TOKEN_GMAIL, + ); } // Desactivar al usuario (alumno) @@ -219,15 +254,952 @@ export class ServicioService { }); // Cambiar estado del servicio - await this.servicioRepo.update(idServicio, { status:{idStatus: 10} }); + await this.servicioRepo.update(idServicio, { status: { idStatus: 10 } }); return { message: 'Se canceló correctamente este Servicio Social.', }; } + async cartaTermino(idServ: number, file: Express.Multer.File) { + const idServicio = this.validacionService.validarNumeroEntero( + idServ, + 'id servicio', + ); + const path = `./server/uploads/${this.validacionService.validacionBasicaStr( + file, + 'archivo', + true, + 1000, + )}`; + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + }); + + if (!servicio) { + throw new NotFoundException('Este servicio no existe.'); + } + + if (servicio.cartaAceptacion) { + throw new BadRequestException('Ya se subió la Carta de Aceptación.'); + } + + // Aquí validas los estados igual que antes + switch (servicio.status.idStatus) { + case 7: + break; + case 1: + throw new BadRequestException( + 'Esta función solo puede usarse cuando el estatus del Servicio Social sea 7.', + ); + case 2: + case 3: + case 4: + case 5: + throw new BadRequestException( + 'Este Servicio Social ya pasó la fase de subir la Carta de Aceptación.', + ); + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + case 8: + case 9: + throw new BadRequestException( + 'Este Servicio Social se encuentra rechazado. No se puede avanzar hasta que se corrija lo necesario.', + ); + case 10: + throw new BadRequestException( + 'Este Servicio Social fue cancelado. Comunícate con COESI para solucionar tu problema.', + ); + default: + throw new BadRequestException('Id status no válido.'); + } + + // Subir archivo al drive + const uploadPath = await this.driveService.uploadFile( + path, + 'Carta_Aceptacion.pdf', + 'application/pdf', + servicio.carpeta, + ); + if (!uploadPath) { + throw new BadRequestException( + 'No se pudo subir el archivo, intenta más tarde.', + ); + } + // Actualizar servicio + await this.servicioRepo.update(idServicio, { + cartaAceptacion: uploadPath, + status: { idStatus: 1 }, + }); + + return { + message: 'Se subió la Carta de Aceptación correctamente.', + }; + } + + async subirCartaTermino( + idServicioParam: number, + archivo: Express.Multer.File, + ): Promise<{ message: string }> { + // 🔹 Validar idServicio + const idServicio: number = this.validacionService.validarNumeroEntero( + idServicioParam, + 'id servicio', + ); + + // 🔹 Validar archivo y generar path local + const rutaArchivo: string = `./server/uploads/${this.validacionService.validacionBasicaStr( + archivo, + 'archivo', + true, + 1000, + )}`; + + // 🔹 Buscar servicio + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + }); + + if (!servicio) { + throw new NotFoundException('Este servicio no existe.'); + } + + if (servicio.cartaTermino) { + throw new BadRequestException('Ya se subió la Carta de Termino.'); + } + + // 🔹 Validar status y subir archivo si corresponde + let archivoSubido; + + switch (servicio.status.idStatus) { + case 4: + case 8: + archivoSubido = await this.driveService.uploadFile( + rutaArchivo, + 'Carta_Termino.pdf', + 'application/pdf', + servicio.carpeta, + ); + break; + + case 1: + case 2: + case 3: + throw new BadRequestException( + 'Aún no se puede subir la Carta de Termino a este Servicio Social.', + ); + case 5: + throw new BadRequestException( + 'Este Servicio Social ya pasó la fase de subir la Carta de Termino.', + ); + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + case 7: + case 9: + throw new BadRequestException( + 'Este Servicio Social se encuentra rechazado. No se puede avanzar hasta que se corrija lo necesario.', + ); + case 10: + throw new BadRequestException( + 'Este Servicio Social está cancelado. Comunícate con COESI para solucionar tu problema.', + ); + default: + throw new BadRequestException('Id status no válido.'); + } + + // 🔹 Actualizar el servicio con la carta subida + await this.servicioRepo.update(idServicio, { cartaTermino: archivoSubido }); + + // 🔹 Ejecutar validaciones adicionales (pre-termino) + const resultadoPreTermino = + await this.validacionService.validarPreTermino(idServicio); + + // 🔹 Retornar mensaje + return { + message: `Se subió la Carta de Termino correctamente. ${resultadoPreTermino}`, + }; + } + + async generarGustavoBazPrada(yearParam: number): Promise { + // 🔹 Validar año + const year: number = this.validacionService.validarNumero( + yearParam, + 'año', + true, + 4, + ); + + const inicio = moment(`${year}-01-31`).toDate(); + const fin = moment(`${year + 1}-01-31`).toDate(); + const path: string = `server/uploads/${year}_gustavo_baz_prada.csv`; + const data: any[] = []; + + // 🔹 Buscar servicios liberados en el año + const servicios: Servicio[] = await this.servicioRepo.find({ + where: { + fechaLiberacion: Between(inicio, fin), + }, + relations: ['usuario', 'carrera'], + order: { fechaLiberacion: 'ASC' }, + }); + + // 🔹 Formatear datos + servicios.forEach((servicio) => { + data.push({ + numeroCuenta: servicio.usuario.usuario, + nombre: servicio.usuario.nombre, + licenciatura: servicio.carrera.carrera, + correo: servicio.correo, + fechaLiberacion: this.archivoService.crearDDMMAAAA( + servicio.fechaLiberacion, + ), + }); + }); + + // 🔹 Eliminar archivo existente y crear CSV + try { + await this.archivoService.eliminarArchivo(path); + } catch (err) { + console.log('Error eliminando archivo:', err); + } + + await this.archivoService.crearArchivo(path, convertArrayToCSV(data)); + + return path; + } + + async subirInformeGlobal( + idServicioParam: number, + archivo: Express.Multer.File, + ): Promise<{ message: string }> { + // 🔹 Validar idServicio + const idServicio: number = this.validacionService.validarNumeroEntero( + idServicioParam, + 'id servicio', + ); + + // 🔹 Validar archivo y generar path local + const rutaArchivo: string = `./server/uploads/${this.validacionService.validacionBasicaStr( + archivo, + 'archivo', + true, + 1000, + )}`; + + // 🔹 Buscar servicio + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + }); + + if (!servicio) { + throw new NotFoundException('Este servicio no existe.'); + } + + if (servicio.informeGlobal) { + throw new BadRequestException('Ya se subió el Informe Global.'); + } + + // 🔹 Validar status y subir archivo si corresponde + let archivoSubido; + + switch (servicio.status.idStatus) { + case 4: + case 9: + archivoSubido = await this.driveService.uploadFile( + rutaArchivo, + 'Informe_Global.pdf', + 'application/pdf', + servicio.carpeta, + ); + break; + + case 1: + case 2: + case 3: + throw new BadRequestException( + 'Aún no se puede subir el Informe Global a este Servicio Social.', + ); + case 5: + throw new BadRequestException( + 'Este Servicio Social ya pasó la fase de subir el Informe Global.', + ); + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + case 7: + case 8: + throw new BadRequestException( + 'Este Servicio Social se encuentra rechazado. No se puede avanzar hasta que se corrija lo necesario.', + ); + case 10: + throw new BadRequestException( + 'Este Servicio Social está cancelado. Comunícate con COESI para solucionar tu problema.', + ); + default: + throw new BadRequestException('Id status no válido.'); + } + + // 🔹 Actualizar el servicio con el informe global + await this.servicioRepo.update(idServicio, { + informeGlobal: archivoSubido, + }); + + // 🔹 Ejecutar validaciones adicionales (pre-termino) + const resultadoPreTermino = + await this.validacionService.validarPreTermino(idServicio); + + // 🔹 Retornar mensaje + return { + message: `Se subió el Informe Global correctamente. ${resultadoPreTermino}`, + }; + } + + async liberarServicio( + idServicioParam: number, + vistoBuenoAcatlan: boolean = false, + ): Promise<{ message: string }> { + // 🔹 Validar idServicio + const idServicio: number = this.validacionService.validarNumeroEntero( + idServicioParam, + 'id servicio', + ); + + // 🔹 Buscar servicio con relaciones + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + relations: ['usuario', 'programa'], + }); + + if (!servicio) { + throw new NotFoundException('No existe este Servicio Social.'); + } + + // 🔹 Validación para programas Acatlán + if (servicio.programa.acatlan && !vistoBuenoAcatlan) { + throw new BadRequestException( + 'El programa de este Servicio Social es Acatlán Contigo y no se envió la validación correspondiente.', + ); + } + + // 🔹 Enviar correo según status + switch (servicio.status.idStatus) { + case 5: + const correoAlumno = terminoValidado(servicio.usuario.nombre); + + if (servicio.correo) { + await this.gmailService.sendMail( + { + to: servicio.correo, + subject: correoAlumno.subject, + text: correoAlumno.msj, + html: '', + fecha_recibido: new Date(), + }, + process.env.TOKEN_GMAIL, + ); + } + break; + + case 1: + case 2: + case 3: + case 4: + throw new BadRequestException( + 'Este Servicio Social aún no puede pasar a Liberación.', + ); + case 6: + throw new BadRequestException( + 'Este Servicio Social ya se encuentra en Liberación.', + ); + case 7: + case 8: + case 9: + throw new BadRequestException( + 'Este Servicio Social se encuentra rechazado. No se puede aceptar hasta que se corrija lo necesario.', + ); + case 10: + throw new BadRequestException('Este Servicio Social está cancelado.'); + default: + throw new BadRequestException('Id status no válido.'); + } + + // 🔹 Actualizar el servicio + await this.servicioRepo.update(idServicio, { + status: { idStatus: 6 }, + fechaLiberacion: moment().toDate(), + vistoBuenoAcatlan: vistoBuenoAcatlan ? true : false, + }); + + return { + message: + 'Se cambió de estatus correctamente y se envió un correo al alumno informándole de su liberación.', + }; + } + + async registrarNuevoServicio( + dto: CrearServicioDto, + file: Express.Multer.File, + ): Promise<{ message: string }> { + // 🔹 Validaciones + + const archivoPath = `./server/uploads/${this.validacionService.validacionBasicaStr(file, 'archivo', true, 1000)}`; + const progInterno = dto.programaInterno + ? this.validacionService.validarAlfanumerico( + dto.programaInterno, + 'programa interno', + true, + 250, + ) + : ''; + const prof = dto.profesor + ? this.validacionService.validarTexto(dto.profesor, 'profesor', true, 50) + : ''; + + // 🔹 Validar Usuario + const usuario = await this.usuarioRepo.findOne({ + where: { idUsuario: dto.idUsuario }, + }); + if (!usuario) + throw new NotFoundException('Este alumno no existe en la db.'); + if (usuario.tipoUsuario.idTipoUsuario !== 3) + throw new BadRequestException('Este usuario no es de tipo Alumno.'); + + // 🔹 Validar Programa + const programa = await this.programaRepo.findOne({ + where: { idPrograma: dto.idPrograma }, + }); + if (!programa) + throw new NotFoundException('Este programa no existe en la db.'); + + // 🔹 Validar Carrera + const carrera = await this.carreraRepo.findOne({ + where: { idCarrera: dto.idCarrera }, + }); + if (!carrera) + throw new NotFoundException('Esta carrera no existe en la db.'); + + // 🔹 Verificar Servicio activo + const servicioExistente = await this.servicioRepo.findOne({ + where: { + usuario: { idUsuario: dto.idUsuario }, + status: { idStatus: Not(10) }, + }, + }); + if (servicioExistente) + throw new BadRequestException( + 'Este alumno ya tiene un Servicio Social activo.', + ); + + // 🔹 Crear Servicio + const nuevoServicio = this.servicioRepo.create({ + usuario, + programa, + carrera, + creditos: dto.creditos.toString(), + correo: dto.correo, + fechaInicio: dto.fechaInicio, + fechaFin: dto.fechaFin, + carpeta: '', + cartaAceptacion: '', + profesor: prof, + programaInterno: progInterno, + }); + + await this.servicioRepo.save(nuevoServicio); + + // 🔹 Crear carpeta y subir carta de aceptación + const carpeta = await this.driveService.mkDir(dto.numeroCuenta); + const cartaPath = await this.driveService.uploadFile( + archivoPath, + 'Carta_Aceptacion.pdf', + 'application/pdf', + carpeta, + ); + if (!carpeta || !cartaPath) { + throw new BadRequestException( + 'No se pudo crear la carpeta o subir la carta, intenta más tarde.', + ); + } + + // 🔹 Actualizar servicio con carpeta y carta + await this.servicioRepo.update(nuevoServicio.idServicio, { + carpeta, + cartaAceptacion: cartaPath, + }); + + return { message: 'Se pre-registró correctamente a este alumno.' }; + } + + async rechazar( + idServicio: number, + mensaje: string, + ): Promise<{ message: string }> { + let correoResponsable: any = {}; + let correoAlumno: any = {}; + let emailResponsable = ''; + + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + relations: ['usuario', 'programa', 'programa.usuario'], + }); + + if (!servicio) { + throw new NotFoundException('No existe este Servicio Social.'); + } + + switch (servicio.status?.idStatus) { + case 1: + correoAlumno = preRegistroRechazadoAlumno( + mensaje, + servicio.usuario.nombre, + ); + + correoResponsable = preRegistroRechazadoResponsable( + mensaje, + servicio.usuario.nombre, + ); + + emailResponsable = servicio.programa.usuario.usuario; // correo del responsable + + // Enviar correo al alumno + await this.gmailService.enviarCorreo( + correoAlumno.subject, + servicio.correo, + correoAlumno.msj, + ); + break; + + case 2: + case 3: + case 4: + case 5: + case 8: + case 9: + throw new BadRequestException( + 'No se puede rechazar la Carta de Aceptación en este punto del Servicio Social.', + ); + + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + + case 7: + throw new BadRequestException( + 'Este Servicio Social ya tiene la Carta de Aceptación rechazada.', + ); + + case 10: + throw new BadRequestException('Este Servicio Social fue cancelado.'); + + default: + throw new BadRequestException('Id status no válido.'); + } + + // Enviar correo al responsable + await this.gmailService.enviarCorreo( + correoResponsable.subject, + emailResponsable, + correoResponsable.msj, + ); + + // Actualizar estado del servicio + await this.servicioRepo.update(idServicio, { + status: { idStatus: 7 }, // si tienes relación con Status + cartaAceptacion: '', + }); + + return { message: 'Se rechazó correctamente la Carta de Aceptación.' }; + } + + async rechazarInforme( + idServicio: number, + mensaje: string, + ): Promise<{ message: string }> { + let correoResponsable: any = {}; + let correoAlumno: any = {}; + let emailResponsable = ''; + + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + relations: ['usuario', 'programa', 'programa.usuario'], + }); + + if (!servicio) + throw new NotFoundException('No existe este Servicio Social.'); + + switch (servicio.status?.idStatus) { + case 5: { + correoAlumno = terminoRechazadoAlumno(mensaje, servicio.usuario.nombre); + correoResponsable = terminoRechazadoResponsable( + mensaje, + servicio.usuario.nombre, + ); + emailResponsable = servicio.programa.usuario.usuario; // correo responsable + + await this.gmailService.enviarCorreo( + correoAlumno.subject, + servicio.correo, + correoAlumno.msj, + ); + break; + } + + case 1: + case 2: + case 3: + case 4: + throw new BadRequestException( + 'Aún no se puede rechazar el Informe Global.', + ); + + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + + case 7: + case 8: + throw new BadRequestException( + 'No se puede rechazar el Informe Global en este punto del Servicio Social.', + ); + + case 9: + throw new BadRequestException( + 'Este Servicio Social ya tiene el Informe Global rechazado.', + ); + + case 10: + throw new BadRequestException('Este Servicio Social fue cancelado.'); + + default: + throw new BadRequestException('Id status no válido.'); + } + + // Enviar correo al responsable + await this.gmailService.enviarCorreo( + correoResponsable.subject, + emailResponsable, + correoResponsable.msj, + ); + + // Actualizar estado del servicio + await this.servicioRepo.update(idServicio, { + status: { idStatus: 9 }, // nuevo estado (rechazado) + informeGlobal: '', + }); + + return { message: 'Se rechazó correctamente el Informe Global.' }; + } + + async rechazarTermino( + idServicio: number, + mensaje: string, + ): Promise<{ message: string }> { + let correoResponsable: any = {}; + let correoAlumno: any = {}; + let emailResponsable = ''; + + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + relations: ['usuario', 'programa', 'programa.usuario'], + }); + + if (!servicio) + throw new NotFoundException('No existe este Servicio Social.'); + + const idStatus = servicio.status?.idStatus; + + switch (idStatus) { + case 5: { + correoAlumno = terminoRechazadoAlumno(mensaje, servicio.usuario.nombre); + correoResponsable = terminoRechazadoResponsable( + mensaje, + servicio.usuario.nombre, + ); + emailResponsable = servicio.programa.usuario.usuario; + + await this.gmailService.enviarCorreo( + correoAlumno.subject, + servicio.correo, + correoAlumno.msj, + ); + break; + } + + case 1: + case 2: + case 3: + case 4: + throw new BadRequestException( + 'Aún no se puede rechazar la Carta de Término.', + ); + + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + + case 7: + case 9: + throw new BadRequestException( + 'No se puede rechazar la Carta de Término en este punto del Servicio Social.', + ); + + case 8: + throw new BadRequestException( + 'Este Servicio Social ya tiene la Carta de Término rechazada.', + ); + + case 10: + throw new BadRequestException('Este Servicio Social fue cancelado.'); + + default: + throw new BadRequestException('Id status no válido.'); + } + + // Enviar correo al responsable + await this.gmailService.enviarCorreo( + correoResponsable.subject, + emailResponsable, + correoResponsable.msj, + ); + + // Actualizar el servicio + await this.servicioRepo.update(idServicio, { + status: { idStatus: 8 }, // Rechazado (Carta de Término) + cartaTermino: '', + }); + + return { message: 'Se rechazó correctamente la Carta de Término.' }; + } + + async registro(idServicio: number): Promise<{ message: string }> { + let password = await this.authService.generarPassword(); + if (process.env.MODE === 'pruebas') password = 'qwertyui'; + + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + relations: ['usuario'], + }); + + if (!servicio) + throw new NotFoundException('No existe este Servicio Social.'); + + let correoData: any; + let idUsuario: number; + + switch (servicio.status.idStatus) { + case 1: + correoData = preRegistro(password, servicio.usuario.nombre); + idUsuario = servicio.usuario.idUsuario; + await this.gmailService.enviarCorreo( + correoData.subject, + servicio.correo, + correoData.msj, + ); + break; + + case 2: + throw new BadRequestException( + 'Este Servicio Social ya se encuentra en Registro.', + ); + case 3: + case 4: + case 5: + throw new BadRequestException( + 'Este Servicio Social ya paso por el Registro.', + ); + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + case 7: + case 8: + case 9: + throw new BadRequestException( + 'Este Servicio Social se encuentra rechazado. No se puede aceptar hasta que se corriga lo necesario.', + ); + case 10: + throw new BadRequestException('Este Servicio Social fue cancelado.'); + default: + throw new BadRequestException('Id status no válido.'); + } + + // Actualizar contraseña del alumno y activarlo + await this.usuarioRepo.update(idUsuario, { + password: await this.authService.encriptar(password), + activo: true, + }); + + // Cambiar estatus del Servicio Social + await this.servicioRepo.update(idServicio, { status: { idStatus: 2 } }); + + return { + message: + 'Se cambió de estatus correctamente y se envió un correo al alumno con sus credenciales.', + }; + } + + async registroValidado( + dto: RegistroValidadoDto, + ): Promise<{ message: string }> { + const idServicio = dto.idServicio; + const fechaNacimiento = dto.fechaNacimiento; + const telefono = dto.telefono; + const direccion = dto.direccion; + + const servicio = await this.servicioRepo.findOne({ + where: { idServicio }, + relations: ['usuario', 'programa', 'programa.usuario'], + }); + + if (!servicio) + throw new NotFoundException('No existe este Servicio Social.'); + + let correoAlumno: any; + let correoResponsable: any; + let emailResponsable: string; + + switch (servicio.status.idStatus) { + case 2: + correoAlumno = registroValidadoAlumno(servicio.usuario.nombre); + correoResponsable = registroValidadoResponsable( + servicio.usuario.nombre, + ); + emailResponsable = servicio.programa.usuario.usuario; + await this.gmailService.enviarCorreo( + correoAlumno.subject, + servicio.correo, + correoAlumno.msj, + ); + break; + + case 1: + throw new BadRequestException( + 'Este Servicio Social aún no puede pasar a Registro Validado.', + ); + case 3: + throw new BadRequestException( + 'Este servicio ya se encuentra en Registro Validado.', + ); + case 4: + case 5: + throw new BadRequestException( + 'Este Servicio Social ya pasó por el Registro Validado.', + ); + case 6: + throw new BadRequestException('Este Servicio Social ya finalizó.'); + case 7: + case 8: + case 9: + throw new BadRequestException( + 'Este Servicio Social se encuentra rechazado. Comunícate con COESI.', + ); + case 10: + throw new BadRequestException( + 'Este Servicio Social fue cancelado. Comunícate con COESI.', + ); + default: + throw new BadRequestException('Id status no válido.'); + } + + await this.gmailService.enviarCorreo( + correoResponsable.subject, + emailResponsable, + correoResponsable.msj, + ); + + // Determinar estatus según modo + const tempStatus = process.env.MODE === 'pruebas' ? 4 : 3; + + await this.servicioRepo.update(idServicio, { + status: { idStatus: tempStatus }, + direccion, + telefono, + fechaNacimiento, + }); + + return { + message: 'Haz terminado el registro de tu Servicio Social correctamente.', + }; + } + + async generarReporte(inicio: Date, fin: Date): Promise { + const path = `server/uploads/reporte.csv`; + const data: any[] = []; + + const servicios = await this.servicioRepo.find({ + where: { + fechaInicio: Between(inicio, fin), + }, + relations: ['usuario', 'programa', 'carrera', 'status'], + order: { fechaInicio: 'ASC' }, + }); + + servicios.forEach((s) => { + data.push({ + numeroCuenta: s.usuario.usuario, + nombre: s.usuario.nombre, + licenciatura: s.carrera.carrera, + correo: s.correo, + fechaInicio: this.archivoService.crearDDMMAAAA(s.fechaInicio), + fechaFin: this.archivoService.crearDDMMAAAA(s.fechaFin), + fechaLiberacion: s.fechaLiberacion + ? this.archivoService.crearDDMMAAAA(s.fechaLiberacion) + : '', + institucion: s.programa.institucion, + dependencia: s.programa.dependencia, + programa: s.programa.programa, + profesor: s.profesor, + clave: s.programa.clavePrograma, + status: s.status.status, + }); + }); + + // Eliminar archivo existente si existe + if (fs.existsSync(path)) { + fs.unlinkSync(path); + } + + // Crear CSV + fs.writeFileSync(path, convertArrayToCSV(data)); + + return path; + } + + async obtenerServiciosAdmin(dto: ServiciosAdminDto) { + const pagina = dto.pagina ? dto.pagina : 1; + const whereStatus = dto.idStatus ? { idStatus: dto.idStatus } : {}; + const nombre = dto.nombre ? dto.nombre : ''; + const numeroCuenta = dto.numeroCuenta ? dto.numeroCuenta : ''; + + const query = this.servicioRepo + .createQueryBuilder('servicio') + .leftJoinAndSelect('servicio.usuario', 'usuario') + .leftJoinAndSelect('servicio.carrera', 'carrera') + .leftJoinAndSelect('servicio.status', 'status') + .select([ + 'servicio.idServicio', + 'servicio.fechaInicio', + 'servicio.fechaFin', + 'servicio.createdAt', + ]) + .orderBy('servicio.updatedAt', 'DESC') + .skip(25 * (pagina - 1)) + .take(25); + + if (whereStatus?.idStatus) { + query.andWhere('status.idStatus = :status', { + status: whereStatus.idStatus, + }); + } + + if (nombre) { + query.andWhere('usuario.nombre LIKE :nombre', { nombre: `%${nombre}%` }); + } + + if (numeroCuenta) { + query.andWhere('usuario.usuario LIKE :numeroCuenta', { + numeroCuenta: `%${numeroCuenta}%`, + }); + } + + const [servicios, count] = await query.getManyAndCount(); + + return { count, serviciosAdmin: servicios }; + } } - -