formulario de capacitacion

This commit is contained in:
2025-12-01 18:23:18 -04:00
parent 6f8a55b8fd
commit efa1726223
25 changed files with 3165 additions and 181 deletions

View File

@@ -13,13 +13,13 @@ import { ThrottlerGuard } from '@nestjs/throttler';
import { DrizzleModule } from './database/drizzle.module';
import { AuthModule } from './features/auth/auth.module';
import { ConfigurationsModule } from './features/configurations/configurations.module';
import { LocationModule} from './features/location/location.module'
import { LocationModule } from './features/location/location.module'
import { MailModule } from './features/mail/mail.module';
import { RolesModule } from './features/roles/roles.module';
import { UserRolesModule } from './features/user-roles/user-roles.module';
import { SurveysModule } from './features/surveys/surveys.module';
import {InventoryModule} from './features/inventory/inventory.module'
import { PicturesModule } from './features/pictures/pictures.module';
import { InventoryModule } from './features/inventory/inventory.module';
import { TrainingModule } from './features/training/training.module';
@Module({
providers: [
@@ -61,7 +61,7 @@ import { PicturesModule } from './features/pictures/pictures.module';
SurveysModule,
LocationModule,
InventoryModule,
PicturesModule
TrainingModule
],
})
export class AppModule {}
export class AppModule { }

View File

@@ -0,0 +1,37 @@
CREATE TABLE "training_surveys" (
"id" serial PRIMARY KEY NOT NULL,
"firstname" text NOT NULL,
"lastname" text NOT NULL,
"visit_date" timestamp NOT NULL,
"productive_activity" text NOT NULL,
"financial_requirement_description" text NOT NULL,
"situr_code_commune" text NOT NULL,
"communal_council" text NOT NULL,
"situr_code_communal_council" text NOT NULL,
"osp_name" text NOT NULL,
"osp_address" text NOT NULL,
"osp_rif" text NOT NULL,
"osp_type" text NOT NULL,
"current_status" text NOT NULL,
"company_constitution_year" integer NOT NULL,
"producer_count" integer NOT NULL,
"product_description" text NOT NULL,
"installed_capacity" text NOT NULL,
"operational_capacity" text NOT NULL,
"osp_responsible_fullname" text NOT NULL,
"osp_responsible_cedula" text NOT NULL,
"osp_responsible_rif" text NOT NULL,
"osp_responsible_phone" text NOT NULL,
"civil_state" text NOT NULL,
"family_burden" integer NOT NULL,
"number_of_children" integer NOT NULL,
"general_observations" text NOT NULL,
"photo1" text NOT NULL,
"photo2" text NOT NULL,
"photo3" text NOT NULL,
"paralysis_reason" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE INDEX "training_surveys_index_00" ON "training_surveys" USING btree ("firstname");

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,13 @@
"when": 1754420096323,
"tag": "0007_curved_fantastic_four",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1764623430844,
"tag": "0008_plain_scream",
"breakpoints": true
}
]
}

View File

@@ -44,7 +44,47 @@ export const answersSurveys = t.pgTable(
}),
);
// Tabla training_surveys
export const trainingSurveys = t.pgTable(
'training_surveys',
{
id: t.serial('id').primaryKey(),
firstname: t.text('firstname').notNull(),
lastname: t.text('lastname').notNull(),
visitDate: t.timestamp('visit_date').notNull(),
productiveActivity: t.text('productive_activity').notNull(),
financialRequirementDescription: t.text('financial_requirement_description').notNull(),
siturCodeCommune: t.text('situr_code_commune').notNull(),
communalCouncil: t.text('communal_council').notNull(),
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
ospName: t.text('osp_name').notNull(),
ospAddress: t.text('osp_address').notNull(),
ospRif: t.text('osp_rif').notNull(),
ospType: t.text('osp_type').notNull(),
currentStatus: t.text('current_status').notNull(),
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
producerCount: t.integer('producer_count').notNull(),
productDescription: t.text('product_description').notNull(),
installedCapacity: t.text('installed_capacity').notNull(),
operationalCapacity: t.text('operational_capacity').notNull(),
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
civilState: t.text('civil_state').notNull(),
familyBurden: t.integer('family_burden').notNull(),
numberOfChildren: t.integer('number_of_children').notNull(),
generalObservations: t.text('general_observations').notNull(),
photo1: t.text('photo1').notNull(),
photo2: t.text('photo2').notNull(),
photo3: t.text('photo3').notNull(),
paralysisReason: t.text('paralysis_reason').notNull(),
...timestamps,
},
(trainingSurveys) => ({
trainingSurveysIndex: t.index('training_surveys_index_00').on(trainingSurveys.firstname),
})
);
export const viewSurveys = t.pgView('v_surveys', {
surverId: t.integer('survey_id'),

View File

@@ -40,7 +40,7 @@ export class AuthService {
private readonly config: ConfigService<Env>,
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private readonly mailService: MailService,
) {}
) { }
//Decode Tokens
// Método para decodificar el token y obtener los datos completos
@@ -89,7 +89,7 @@ export class AuthService {
},
{
secret: envs.access_token_secret,
expiresIn: envs.access_token_expiration,
expiresIn: envs.access_token_expiration as any,
},
),
this.jwtService.signAsync(
@@ -99,7 +99,7 @@ export class AuthService {
},
{
secret: envs.refresh_token_secret,
expiresIn: envs.refresh_token_expiration,
expiresIn: envs.refresh_token_expiration as any,
},
),
]);
@@ -197,7 +197,7 @@ export class AuthService {
//Sign In User Account
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
const user = await this.validateUser(dto);
const tokens = await this.generateTokens(user);
const decodeAccess = this.decodeToken(tokens.access_token);
@@ -267,7 +267,7 @@ export class AuthService {
// const user_id = 1;
const validation = await this.jwtService.verifyAsync(refresh_token, {
secret: envs.refresh_token_secret,
secret: envs.refresh_token_secret,
});
if (!validation) throw new UnauthorizedException('Invalid refresh token');
@@ -278,21 +278,21 @@ export class AuthService {
.where(
and(
eq(sessions.userId, user_id) &&
eq(sessions.sessionToken, dto.refresh_token),
eq(sessions.sessionToken, dto.refresh_token),
),
);
// console.log(session.length);
if (session.length === 0) throw new NotFoundException('session not found');
const user = await this.findUserById(user_id);
if (!user) throw new NotFoundException('User not found');
// Genera token
const tokens = await this.generateTokens(user);
const decodeAccess = this.decodeToken(tokens.access_token);
const decodeRefresh = this.decodeToken(tokens.refresh_token);
// Actualiza session
await this.drizzle
.update(sessions)
@@ -308,75 +308,75 @@ export class AuthService {
}
async singUp(createUserDto: SingUpUserDto): Promise<User> {
// Check if username or email exists
const data = await this.drizzle
// Check if username or email exists
const data = await this.drizzle
.select({
id: users.id,
username: users.username,
email: users.email
})
.from(users)
.where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email)));
if (data.length > 0) {
if (data[0].username === createUserDto.username) {
throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST);
}
if (data[0].email === createUserDto.email) {
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
}
}
// Hash the password
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// Start a transaction
return await this.drizzle.transaction(async (tx) => {
// Create the user
const [newUser] = await tx
.insert(users)
.values({
username: createUserDto.username,
email: createUserDto.email,
password: hashedPassword,
fullname: createUserDto.fullname,
isActive: true,
state: createUserDto.state,
municipality: createUserDto.municipality,
parish: createUserDto.parish,
phone: createUserDto.phone,
isEmailVerified: false,
isTwoFactorEnabled: false,
})
.returning();
// check if user role is admin
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
// Assign role to user
await tx.insert(usersRole).values({
userId: newUser.id,
roleId: role,
});
// Return the created user with role
const [userWithRole] = await tx
.select({
id: users.id,
username: users.username,
email: users.email
email: users.email,
fullname: users.fullname,
phone: users.phone,
isActive: users.isActive,
role: roles.name,
})
.from(users)
.where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email)));
if (data.length > 0) {
if (data[0].username === createUserDto.username) {
throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST);
}
if (data[0].email === createUserDto.email) {
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
}
}
.leftJoin(usersRole, eq(usersRole.userId, users.id))
.leftJoin(roles, eq(roles.id, usersRole.roleId))
.where(eq(users.id, newUser.id));
// Hash the password
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// Start a transaction
return await this.drizzle.transaction(async (tx) => {
// Create the user
const [newUser] = await tx
.insert(users)
.values({
username: createUserDto.username,
email: createUserDto.email,
password: hashedPassword,
fullname: createUserDto.fullname,
isActive: true,
state: createUserDto.state,
municipality: createUserDto.municipality,
parish: createUserDto.parish,
phone: createUserDto.phone,
isEmailVerified: false,
isTwoFactorEnabled: false,
})
.returning();
return userWithRole;
})
// check if user role is admin
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
// Assign role to user
await tx.insert(usersRole).values({
userId: newUser.id,
roleId: role,
});
// Return the created user with role
const [userWithRole] = await tx
.select({
id: users.id,
username: users.username,
email: users.email,
fullname: users.fullname,
phone: users.phone,
isActive: users.isActive,
role: roles.name,
})
.from(users)
.leftJoin(usersRole, eq(usersRole.userId, users.id))
.leftJoin(roles, eq(roles.id, usersRole.roleId))
.where(eq(users.id, newUser.id));
return userWithRole;
})
}
}

View File

@@ -1,19 +0,0 @@
import { Controller, Post, UploadedFiles, UseInterceptors, Body } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { PicturesService } from './pictures.service';
@Controller('pictures')
export class PicturesController {
constructor(private readonly picturesService: PicturesService) {}
@Post('upload')
@UseInterceptors(FilesInterceptor('urlImg'))
async uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
// Aquí puedes acceder a los campos del formulario
// console.log('Archivos:', files);
// console.log('Otros campos del formulario:', body);
const result = await this.picturesService.saveImages(files);
return { data: result };
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { PicturesController } from './pictures.controller';
import { PicturesService } from './pictures.service';
@Module({
controllers: [PicturesController],
providers: [PicturesService],
})
export class PicturesModule {}

View File

@@ -1,41 +0,0 @@
import { Injectable } from '@nestjs/common';
import { writeFile } from 'fs/promises';
import { join } from 'path';
@Injectable()
export class PicturesService {
/**
* Guarda una imagen en el directorio de imágenes.
* @param file - El archivo de imagen a guardar.
* @returns La ruta de la imagen guardada.
*/
async saveImages(file: Express.Multer.File[]): Promise<string[]> {
// Construye la ruta al directorio de imágenes.
const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict');
console.log(picturesPath);
let images : string[] = [];
let count = 0;
file.forEach(async (file) => {
count++
// Crea un nombre de archivo único para la imagen.
const fileName = `${Date.now()}-${count}-${file.originalname}`;
images.push(fileName);
// console.log(fileName);
// Construye la ruta completa al archivo de imagen.
const filePath = join(picturesPath, fileName);
// Escribe el archivo de imagen en el disco.
await writeFile(filePath, file.buffer);
});
// Devuelve la ruta de la imagen guardada.
// return [file[0].originalname]
return images;
}
}

View File

@@ -0,0 +1,124 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsString, IsDateString, IsOptional } from 'class-validator';
export class CreateTrainingDto {
@ApiProperty()
@IsString()
firstname: string;
@ApiProperty()
@IsString()
lastname: string;
@ApiProperty()
@IsDateString()
visitDate: Date;
@ApiProperty()
@IsString()
productiveActivity: string;
@ApiProperty()
@IsString()
financialRequirementDescription: string;
@ApiProperty()
@IsString()
siturCodeCommune: string;
@ApiProperty()
@IsString()
communalCouncil: string;
@ApiProperty()
@IsString()
siturCodeCommunalCouncil: string;
@ApiProperty()
@IsString()
ospName: string;
@ApiProperty()
@IsString()
ospAddress: string;
@ApiProperty()
@IsString()
ospRif: string;
@ApiProperty()
@IsString()
ospType: string;
@ApiProperty()
@IsString()
currentStatus: string;
@ApiProperty()
@IsInt()
companyConstitutionYear: number;
@ApiProperty()
@IsInt()
producerCount: number;
@ApiProperty()
@IsString()
productDescription: string;
@ApiProperty()
@IsString()
installedCapacity: string;
@ApiProperty()
@IsString()
operationalCapacity: string;
@ApiProperty()
@IsString()
ospResponsibleFullname: string;
@ApiProperty()
@IsString()
ospResponsibleCedula: string;
@ApiProperty()
@IsString()
ospResponsibleRif: string;
@ApiProperty()
@IsString()
ospResponsiblePhone: string;
@ApiProperty()
@IsString()
civilState: string;
@ApiProperty()
@IsInt()
familyBurden: number;
@ApiProperty()
@IsInt()
numberOfChildren: number;
@ApiProperty()
@IsString()
generalObservations: string;
@ApiProperty()
@IsString()
photo1: string;
@ApiProperty()
@IsString()
photo2: string;
@ApiProperty()
@IsString()
photo3: string;
@ApiProperty()
@IsString()
paralysisReason: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateTrainingDto } from './create-training.dto';
export class UpdateTrainingDto extends PartialType(CreateTrainingDto) { }

View File

@@ -0,0 +1,58 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { TrainingService } from './training.service';
import { CreateTrainingDto } from './dto/create-training.dto';
import { UpdateTrainingDto } from './dto/update-training.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { PaginationDto } from '../../common/dto/pagination.dto';
@ApiTags('training')
@Controller('training')
export class TrainingController {
constructor(private readonly trainingService: TrainingService) { }
@Get()
@ApiOperation({ summary: 'Get all training records with pagination and filters' })
@ApiResponse({ status: 200, description: 'Return paginated training records.' })
async findAll(@Query() paginationDto: PaginationDto) {
const result = await this.trainingService.findAll(paginationDto);
return {
message: 'Training records fetched successfully',
data: result.data,
meta: result.meta
};
}
@Get(':id')
@ApiOperation({ summary: 'Get a training record by ID' })
@ApiResponse({ status: 200, description: 'Return the training record.' })
@ApiResponse({ status: 404, description: 'Training record not found.' })
async findOne(@Param('id') id: string) {
const data = await this.trainingService.findOne(+id);
return { message: 'Training record fetched successfully', data };
}
@Post()
@ApiOperation({ summary: 'Create a new training record' })
@ApiResponse({ status: 201, description: 'Training record created successfully.' })
async create(@Body() createTrainingDto: CreateTrainingDto) {
const data = await this.trainingService.create(createTrainingDto);
return { message: 'Training record created successfully', data };
}
@Patch(':id')
@ApiOperation({ summary: 'Update a training record' })
@ApiResponse({ status: 200, description: 'Training record updated successfully.' })
@ApiResponse({ status: 404, description: 'Training record not found.' })
async update(@Param('id') id: string, @Body() updateTrainingDto: UpdateTrainingDto) {
const data = await this.trainingService.update(+id, updateTrainingDto);
return { message: 'Training record updated successfully', data };
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a training record' })
@ApiResponse({ status: 200, description: 'Training record deleted successfully.' })
@ApiResponse({ status: 404, description: 'Training record not found.' })
async remove(@Param('id') id: string) {
return await this.trainingService.remove(+id);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TrainingService } from './training.service';
import { TrainingController } from './training.controller';
@Module({
controllers: [TrainingController],
providers: [TrainingService],
exports: [TrainingService],
})
export class TrainingModule { }

View File

@@ -0,0 +1,118 @@
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { trainingSurveys } from 'src/database/index';
import { eq, like, or, SQL, sql } from 'drizzle-orm';
import { CreateTrainingDto } from './dto/create-training.dto';
import { UpdateTrainingDto } from './dto/update-training.dto';
import { PaginationDto } from '../../common/dto/pagination.dto';
@Injectable()
export class TrainingService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) { }
async findAll(paginationDto?: PaginationDto) {
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
const offset = (page - 1) * limit;
let searchCondition: SQL<unknown> | undefined;
if (search) {
searchCondition = or(
like(trainingSurveys.firstname, `%${search}%`),
like(trainingSurveys.lastname, `%${search}%`),
like(trainingSurveys.ospName, `%${search}%`),
like(trainingSurveys.ospRif, `%${search}%`)
);
}
const orderBy = sortOrder === 'asc'
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
const totalCountResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(trainingSurveys)
.where(searchCondition);
const totalCount = Number(totalCountResult[0].count);
const totalPages = Math.ceil(totalCount / limit);
const data = await this.drizzle
.select()
.from(trainingSurveys)
.where(searchCondition)
.orderBy(orderBy)
.limit(limit)
.offset(offset);
const meta = {
page,
limit,
totalCount,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
nextPage: page < totalPages ? page + 1 : null,
previousPage: page > 1 ? page - 1 : null,
};
return { data, meta };
}
async findOne(id: number) {
const find = await this.drizzle
.select()
.from(trainingSurveys)
.where(eq(trainingSurveys.id, id));
if (find.length === 0) {
throw new HttpException('Training record not found', HttpStatus.NOT_FOUND);
}
return find[0];
}
async create(createTrainingDto: CreateTrainingDto) {
const [newRecord] = await this.drizzle
.insert(trainingSurveys)
.values({
...createTrainingDto,
visitDate: new Date(createTrainingDto.visitDate),
})
.returning();
return newRecord;
}
async update(id: number, updateTrainingDto: UpdateTrainingDto) {
await this.findOne(id);
const updateData: any = { ...updateTrainingDto };
if (updateTrainingDto.visitDate) {
updateData.visitDate = new Date(updateTrainingDto.visitDate);
}
const [updatedRecord] = await this.drizzle
.update(trainingSurveys)
.set(updateData)
.where(eq(trainingSurveys.id, id))
.returning();
return updatedRecord;
}
async remove(id: number) {
await this.findOne(id);
const [deletedRecord] = await this.drizzle
.delete(trainingSurveys)
.where(eq(trainingSurveys.id, id))
.returning();
return { message: 'Training record deleted successfully', data: deletedRecord };
}
}