aceptado cambios auth

This commit is contained in:
2025-12-15 10:53:36 -04:00
32 changed files with 5522 additions and 113 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");

View File

@@ -0,0 +1,7 @@
ALTER TABLE "training_surveys" ADD COLUMN "state" integer;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "municipality" integer;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "parish" integer;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "osp_responsible_email" text NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_state_states_id_fk" FOREIGN KEY ("state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_municipality_municipalities_id_fk" FOREIGN KEY ("municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_parish_parishes_id_fk" FOREIGN KEY ("parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,6 +2,7 @@ import * as t from 'drizzle-orm/pg-core';
import { eq, lt, gte, ne, sql } from 'drizzle-orm';
import { timestamps } from '../timestamps';
import { users } from './auth';
import { states, municipalities, parishes } from './general';
// Tabla surveys
@@ -44,7 +45,57 @@ export const answersSurveys = t.pgTable(
}),
);
// Tabla training_surveys
export const trainingSurveys = t.pgTable(
'training_surveys',
{
// Datos basicos
id: t.serial('id').primaryKey(),
firstname: t.text('firstname').notNull(),
lastname: t.text('lastname').notNull(),
visitDate: t.timestamp('visit_date').notNull(),
// ubicacion
state: t.integer('state').references(() => states.id, { onDelete: 'set null' }),
municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }),
parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }),
siturCodeCommune: t.text('situr_code_commune').notNull(),
communalCouncil: t.text('communal_council').notNull(),
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
// datos del OSP (ORGANIZACIÓN SOCIOPRODUCTIVA)
ospName: t.text('osp_name').notNull(),
ospAddress: t.text('osp_address').notNull(),
ospRif: t.text('osp_rif').notNull(),
ospType: t.text('osp_type').notNull(),
productiveActivity: t.text('productive_activity').notNull(),
financialRequirementDescription: t.text('financial_requirement_description').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(),
// datos del responsable
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(),
ospResponsibleEmail: t.text('osp_responsible_email').notNull(),
civilState: t.text('civil_state').notNull(),
familyBurden: t.integer('family_burden').notNull(),
numberOfChildren: t.integer('number_of_children').notNull(),
// datos adicionales
generalObservations: t.text('general_observations').notNull(),
paralysisReason: t.text('paralysis_reason').notNull(),
// fotos
photo1: t.text('photo1').notNull(),
photo2: t.text('photo2').notNull(),
photo3: t.text('photo3').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
@@ -294,15 +294,18 @@ export class AuthService {
// 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)
@@ -367,7 +370,31 @@ export class AuthService {
isTwoFactorEnabled: false,
})
.returning();
// 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;
// check if user role is admin
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;

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,140 @@
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: string;
@ApiProperty()
@IsString()
productiveActivity: string;
@ApiProperty()
@IsString()
financialRequirementDescription: string;
@ApiProperty()
@IsInt()
state: number;
@ApiProperty()
@IsInt()
municipality: number;
@ApiProperty()
@IsInt()
parish: number;
@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()
ospResponsibleEmail: 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,38 @@
import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class TrainingStatisticsFilterDto {
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
startDate?: string;
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
endDate?: string;
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
stateId?: number;
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
municipalityId?: number;
@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsNumber()
parishId?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
ospType?: 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,68 @@
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';
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.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('statistics')
@ApiOperation({ summary: 'Get training statistics' })
@ApiResponse({ status: 200, description: 'Return training statistics.' })
async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) {
const data = await this.trainingService.getStatistics(filterDto);
return { message: 'Training statistics fetched successfully', data };
}
@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,223 @@
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, and, gte, lte, SQL, sql } from 'drizzle-orm';
import { CreateTrainingDto } from './dto/create-training.dto';
import { UpdateTrainingDto } from './dto/update-training.dto';
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
import { states } from 'src/database/index';
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 getStatistics(filterDto: TrainingStatisticsFilterDto) {
const { startDate, endDate, stateId, municipalityId, parishId, ospType } = filterDto;
const filters: SQL[] = [];
if (startDate) {
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
}
if (endDate) {
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
}
if (stateId) {
filters.push(eq(trainingSurveys.state, stateId));
}
if (municipalityId) {
filters.push(eq(trainingSurveys.municipality, municipalityId));
}
if (parishId) {
filters.push(eq(trainingSurveys.parish, parishId));
}
if (ospType) {
filters.push(eq(trainingSurveys.ospType, ospType));
}
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
const totalOspsResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(trainingSurveys)
.where(whereCondition);
const totalOsps = Number(totalOspsResult[0].count);
const totalProducersResult = await this.drizzle
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` })
.from(trainingSurveys)
.where(whereCondition);
const totalProducers = Number(totalProducersResult[0].sum || 0);
const statusDistribution = await this.drizzle
.select({
name: trainingSurveys.currentStatus,
value: sql<number>`count(*)`
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.currentStatus);
const activityDistribution = await this.drizzle
.select({
name: trainingSurveys.productiveActivity,
value: sql<number>`count(*)`
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.productiveActivity);
const typeDistribution = await this.drizzle
.select({
name: trainingSurveys.ospType,
value: sql<number>`count(*)`
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.ospType);
// New Aggregations
const stateDistribution = await this.drizzle
.select({
name: states.name,
value: sql<number>`count(${trainingSurveys.id})`
})
.from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id))
.where(whereCondition)
.groupBy(states.name);
const yearDistribution = await this.drizzle
.select({
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
value: sql<number>`count(*)`
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.companyConstitutionYear)
.orderBy(trainingSurveys.companyConstitutionYear);
return {
totalOsps,
totalProducers,
statusDistribution: statusDistribution.map(item => ({ ...item, value: Number(item.value) })),
activityDistribution: activityDistribution.map(item => ({ ...item, value: Number(item.value) })),
typeDistribution: typeDistribution.map(item => ({ ...item, value: Number(item.value) })),
stateDistribution: stateDistribution.map(item => ({ ...item, value: Number(item.value) })),
yearDistribution: yearDistribution.map(item => ({ ...item, value: Number(item.value) })),
};
}
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 };
}
}