mejoras al formulario de registro organizaciones productivas
This commit is contained in:
3
apps/api/.gitignore
vendored
3
apps/api/.gitignore
vendored
@@ -54,3 +54,6 @@ pids
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Uploads
|
||||
/uploads/training/*
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
"pg": "8.13.3",
|
||||
"pino-pretty": "13.0.0",
|
||||
"reflect-metadata": "0.2.0",
|
||||
"rxjs": "7.8.1"
|
||||
"rxjs": "7.8.1",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
|
||||
36
apps/api/src/common/pipes/image-processing.pipe.ts
Normal file
36
apps/api/src/common/pipes/image-processing.pipe.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable, PipeTransform } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
@Injectable()
|
||||
export class ImageProcessingPipe implements PipeTransform {
|
||||
async transform(
|
||||
files: Express.Multer.File[] | Express.Multer.File,
|
||||
): Promise<Express.Multer.File[] | Express.Multer.File> {
|
||||
if (!files) return files;
|
||||
|
||||
const processItem = async (
|
||||
file: Express.Multer.File,
|
||||
): Promise<Express.Multer.File> => {
|
||||
const processedBuffer = await sharp(file.buffer)
|
||||
.webp({ quality: 80 })
|
||||
.toBuffer();
|
||||
|
||||
const originalName = path.parse(file.originalname).name;
|
||||
|
||||
return {
|
||||
...file,
|
||||
buffer: processedBuffer,
|
||||
originalname: `${originalName}.webp`,
|
||||
mimetype: 'image/webp',
|
||||
size: processedBuffer.length,
|
||||
};
|
||||
};
|
||||
|
||||
if (Array.isArray(files)) {
|
||||
return await Promise.all(files.map((file) => processItem(file)));
|
||||
}
|
||||
|
||||
return await processItem(files);
|
||||
}
|
||||
}
|
||||
4
apps/api/src/database/migrations/0010_dashing_bishop.sql
Normal file
4
apps/api/src/database/migrations/0010_dashing_bishop.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "current_status" SET DEFAULT 'ACTIVA';--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "photo2" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "photo3" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ADD COLUMN "product_count" integer DEFAULT 0 NOT NULL;
|
||||
1850
apps/api/src/database/migrations/meta/0010_snapshot.json
Normal file
1850
apps/api/src/database/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,13 @@
|
||||
"when": 1764883378610,
|
||||
"tag": "0009_eminent_ares",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1769097895095,
|
||||
"tag": "0010_dashing_bishop",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
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';
|
||||
|
||||
import { municipalities, parishes, states } from './general';
|
||||
|
||||
// Tabla surveys
|
||||
export const surveys = t.pgTable(
|
||||
@@ -19,9 +18,7 @@ export const surveys = t.pgTable(
|
||||
...timestamps,
|
||||
},
|
||||
(surveys) => ({
|
||||
surveysIndex: t
|
||||
.index('surveys_index_00')
|
||||
.on(surveys.title),
|
||||
surveysIndex: t.index('surveys_index_00').on(surveys.title),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -55,9 +52,15 @@ export const trainingSurveys = t.pgTable(
|
||||
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' }),
|
||||
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(),
|
||||
@@ -67,10 +70,13 @@ export const trainingSurveys = t.pgTable(
|
||||
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(),
|
||||
financialRequirementDescription: t
|
||||
.text('financial_requirement_description')
|
||||
.notNull(),
|
||||
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
||||
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||
producerCount: t.integer('producer_count').notNull(),
|
||||
productCount: t.integer('product_count').notNull().default(0),
|
||||
productDescription: t.text('product_description').notNull(),
|
||||
installedCapacity: t.text('installed_capacity').notNull(),
|
||||
operationalCapacity: t.text('operational_capacity').notNull(),
|
||||
@@ -88,13 +94,15 @@ export const trainingSurveys = t.pgTable(
|
||||
paralysisReason: t.text('paralysis_reason').notNull(),
|
||||
// fotos
|
||||
photo1: t.text('photo1').notNull(),
|
||||
photo2: t.text('photo2').notNull(),
|
||||
photo3: t.text('photo3').notNull(),
|
||||
photo2: t.text('photo2'),
|
||||
photo3: t.text('photo3'),
|
||||
...timestamps,
|
||||
},
|
||||
(trainingSurveys) => ({
|
||||
trainingSurveysIndex: t.index('training_surveys_index_00').on(trainingSurveys.firstname),
|
||||
})
|
||||
trainingSurveysIndex: t
|
||||
.index('training_surveys_index_00')
|
||||
.on(trainingSurveys.firstname),
|
||||
}),
|
||||
);
|
||||
|
||||
export const viewSurveys = t.pgView('v_surveys', {
|
||||
@@ -103,6 +111,7 @@ export const viewSurveys = t.pgView('v_surveys', {
|
||||
description: t.text('description'),
|
||||
created_at: t.timestamp('created_at'),
|
||||
closingDate: t.date('closing_date'),
|
||||
targetAudience: t.varchar('target_audience')
|
||||
}).as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
||||
where published = true`);
|
||||
targetAudience: t.varchar('target_audience'),
|
||||
})
|
||||
.as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
||||
where published = true`);
|
||||
|
||||
@@ -1,140 +1,149 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsInt, IsString, IsDateString, IsOptional } from 'class-validator';
|
||||
import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateTrainingDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
firstname: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
firstname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDateString()
|
||||
visitDate: string;
|
||||
@ApiProperty()
|
||||
@IsDateString()
|
||||
visitDate: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productiveActivity: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productiveActivity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
financialRequirementDescription: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
financialRequirementDescription: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
state: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
state: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
municipality: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
municipality: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
parish: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
parish: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommune: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommune: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncil: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncil: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommunalCouncil: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommunalCouncil: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospName: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospAddress: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospAddress: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospRif: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospType: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospType: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currentStatus: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currentStatus: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
companyConstitutionYear: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
companyConstitutionYear: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
producerCount: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
producerCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productDescription: string;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
productCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
installedCapacity: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productDescription: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
operationalCapacity: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
installedCapacity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleFullname: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
operationalCapacity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleCedula: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleFullname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleRif: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleCedula: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsiblePhone: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleEmail: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsiblePhone: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
civilState: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleEmail: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
familyBurden: number;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
civilState: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
numberOfChildren: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
familyBurden: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
generalObservations: string;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
numberOfChildren: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
photo1: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
generalObservations: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
photo2: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo1?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
photo3: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo2?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
paralysisReason: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo3?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
paralysisReason: string;
|
||||
}
|
||||
|
||||
@@ -1,68 +1,114 @@
|
||||
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 {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UploadedFiles,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import {
|
||||
ApiConsumes,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
|
||||
import { ImageProcessingPipe } from '../../common/pipes/image-processing.pipe';
|
||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||
import { TrainingService } from './training.service';
|
||||
|
||||
@ApiTags('training')
|
||||
@Controller('training')
|
||||
export class TrainingController {
|
||||
constructor(private readonly trainingService: TrainingService) { }
|
||||
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()
|
||||
@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('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 };
|
||||
}
|
||||
@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 };
|
||||
}
|
||||
@Post()
|
||||
@UseInterceptors(FilesInterceptor('files', 3))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Create a new training record' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Training record created successfully.',
|
||||
})
|
||||
async create(
|
||||
@Body() createTrainingDto: CreateTrainingDto,
|
||||
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||
) {
|
||||
const data = await this.trainingService.create(createTrainingDto, files);
|
||||
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 };
|
||||
}
|
||||
@Patch(':id')
|
||||
@UseInterceptors(FilesInterceptor('files', 3))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@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,
|
||||
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||
) {
|
||||
const data = await this.trainingService.update(
|
||||
+id,
|
||||
updateTrainingDto,
|
||||
files,
|
||||
);
|
||||
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);
|
||||
}
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,223 +1,324 @@
|
||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||
import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||
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 { states, trainingSurveys } from 'src/database/index';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TrainingService {
|
||||
constructor(
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
) { }
|
||||
constructor(
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
) {}
|
||||
|
||||
async findAll(paginationDto?: PaginationDto) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
search = '',
|
||||
sortBy = 'id',
|
||||
sortOrder = 'asc',
|
||||
} = paginationDto || {};
|
||||
|
||||
async findAll(paginationDto?: PaginationDto) {
|
||||
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
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 };
|
||||
let searchCondition: SQL<unknown> | undefined;
|
||||
if (search) {
|
||||
searchCondition = or(ilike(trainingSurveys.ospName, `%${search}%`));
|
||||
}
|
||||
|
||||
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||
const { startDate, endDate, stateId, municipalityId, parishId, ospType } = filterDto;
|
||||
const orderBy =
|
||||
sortOrder === 'asc'
|
||||
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
|
||||
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
|
||||
|
||||
const filters: SQL[] = [];
|
||||
const totalCountResult = await this.drizzle
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(trainingSurveys)
|
||||
.where(searchCondition);
|
||||
|
||||
if (startDate) {
|
||||
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||
}
|
||||
const totalCount = Number(totalCountResult[0].count);
|
||||
const totalPages = Math.ceil(totalCount / limit);
|
||||
|
||||
if (endDate) {
|
||||
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||
}
|
||||
const data = await this.drizzle
|
||||
.select()
|
||||
.from(trainingSurveys)
|
||||
.where(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
if (stateId) {
|
||||
filters.push(eq(trainingSurveys.state, stateId));
|
||||
}
|
||||
const meta = {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
previousPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
|
||||
if (municipalityId) {
|
||||
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||
}
|
||||
return { data, meta };
|
||||
}
|
||||
|
||||
if (parishId) {
|
||||
filters.push(eq(trainingSurveys.parish, parishId));
|
||||
}
|
||||
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
|
||||
filterDto;
|
||||
|
||||
if (ospType) {
|
||||
filters.push(eq(trainingSurveys.ospType, ospType));
|
||||
}
|
||||
const filters: SQL[] = [];
|
||||
|
||||
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) })),
|
||||
};
|
||||
if (startDate) {
|
||||
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const find = await this.drizzle
|
||||
.select()
|
||||
.from(trainingSurveys)
|
||||
.where(eq(trainingSurveys.id, id));
|
||||
if (endDate) {
|
||||
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||
}
|
||||
|
||||
if (find.length === 0) {
|
||||
throw new HttpException('Training record not found', HttpStatus.NOT_FOUND);
|
||||
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];
|
||||
}
|
||||
|
||||
private async saveFiles(files: Express.Multer.File[]): Promise<string[]> {
|
||||
if (!files || files.length === 0) return [];
|
||||
|
||||
const uploadDir = './uploads/training';
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const savedPaths: string[] = [];
|
||||
for (const file of files) {
|
||||
const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${path.extname(file.originalname)}`;
|
||||
const filePath = path.join(uploadDir, fileName);
|
||||
fs.writeFileSync(filePath, file.buffer);
|
||||
savedPaths.push(`/assets/training/${fileName}`);
|
||||
}
|
||||
return savedPaths;
|
||||
}
|
||||
|
||||
private deleteFile(assetPath: string) {
|
||||
if (!assetPath) return;
|
||||
// Map /assets/training/filename.webp back to ./uploads/training/filename.webp
|
||||
const relativePath = assetPath.replace('/assets/training/', '');
|
||||
const fullPath = path.join('./uploads/training', relativePath);
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
fs.unlinkSync(fullPath);
|
||||
} catch (err) {
|
||||
console.error(`Error deleting file ${fullPath}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
createTrainingDto: CreateTrainingDto,
|
||||
files: Express.Multer.File[],
|
||||
) {
|
||||
const photoPaths = await this.saveFiles(files);
|
||||
|
||||
const [newRecord] = await this.drizzle
|
||||
.insert(trainingSurveys)
|
||||
.values({
|
||||
...createTrainingDto,
|
||||
visitDate: new Date(createTrainingDto.visitDate),
|
||||
photo1: photoPaths[0] || '',
|
||||
photo2: photoPaths[1] || null,
|
||||
photo3: photoPaths[2] || null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
updateTrainingDto: UpdateTrainingDto,
|
||||
files: Express.Multer.File[],
|
||||
) {
|
||||
const currentRecord = await this.findOne(id);
|
||||
|
||||
const photoPaths = await this.saveFiles(files);
|
||||
|
||||
const updateData: any = { ...updateTrainingDto };
|
||||
|
||||
// Handle photo updates/removals
|
||||
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
||||
|
||||
// 1. If we have NEW files, they replace any old files or occupy empty slots
|
||||
if (photoPaths.length > 0) {
|
||||
photoPaths.forEach((newPath, idx) => {
|
||||
const fieldName = photoFields[idx];
|
||||
const oldPath = currentRecord[fieldName];
|
||||
if (oldPath && oldPath !== newPath) {
|
||||
this.deleteFile(oldPath);
|
||||
}
|
||||
|
||||
return find[0];
|
||||
updateData[fieldName] = newPath;
|
||||
});
|
||||
}
|
||||
|
||||
async create(createTrainingDto: CreateTrainingDto) {
|
||||
const [newRecord] = await this.drizzle
|
||||
.insert(trainingSurveys)
|
||||
.values({
|
||||
...createTrainingDto,
|
||||
visitDate: new Date(createTrainingDto.visitDate),
|
||||
})
|
||||
.returning();
|
||||
// 2. If the user explicitly cleared a photo field (updateData.photoX === '')
|
||||
photoFields.forEach((field) => {
|
||||
if (updateData[field] === '') {
|
||||
const oldPath = currentRecord[field];
|
||||
if (oldPath) this.deleteFile(oldPath);
|
||||
updateData[field] = null; // Set to null in DB
|
||||
}
|
||||
});
|
||||
|
||||
return newRecord;
|
||||
if (updateTrainingDto.visitDate) {
|
||||
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||
}
|
||||
|
||||
async update(id: number, updateTrainingDto: UpdateTrainingDto) {
|
||||
await this.findOne(id);
|
||||
const [updatedRecord] = await this.drizzle
|
||||
.update(trainingSurveys)
|
||||
.set(updateData)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
|
||||
const updateData: any = { ...updateTrainingDto };
|
||||
if (updateTrainingDto.visitDate) {
|
||||
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||
}
|
||||
return updatedRecord;
|
||||
}
|
||||
|
||||
const [updatedRecord] = await this.drizzle
|
||||
.update(trainingSurveys)
|
||||
.set(updateData)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
async remove(id: number) {
|
||||
const record = await this.findOne(id);
|
||||
|
||||
return updatedRecord;
|
||||
}
|
||||
// Delete associated files
|
||||
if (record.photo1) this.deleteFile(record.photo1);
|
||||
if (record.photo2) this.deleteFile(record.photo2);
|
||||
if (record.photo3) this.deleteFile(record.photo3);
|
||||
|
||||
async remove(id: number) {
|
||||
await this.findOne(id);
|
||||
const [deletedRecord] = await this.drizzle
|
||||
.delete(trainingSurveys)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
|
||||
const [deletedRecord] = await this.drizzle
|
||||
.delete(trainingSurveys)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
|
||||
return { message: 'Training record deleted successfully', data: deletedRecord };
|
||||
}
|
||||
return {
|
||||
message: 'Training record deleted successfully',
|
||||
data: deletedRecord,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user