base con autenticacion, registro, modulo encuestas

This commit is contained in:
2025-06-16 12:02:22 -04:00
commit 475e0754df
411 changed files with 26265 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { Public } from '@/common/decorators';
import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard';
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
import {
Body,
Controller,
HttpCode,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@HttpCode(200)
@Post('sing-up')
// @ApiOperation({ summary: 'Create a new user' })
// @ApiResponse({ status: 201, description: 'User created successfully.' })
async singUp(@Body() payload: SingUpUserDto) {
const data = await this.authService.singUp(payload)
return { message: 'User created successfully', data};
// return { message: 'User created successfully', data };
}
@Public()
@HttpCode(200)
@Post('sign-in')
async signIn(@Body() signInUserDto: SignInUserDto) {
return await this.authService.signIn(signInUserDto);
}
@Post('sign-out')
//@RequirePermissions('auth:sign-out')
async signOut(@Body() signOutUserDto: SignOutUserDto) {
await this.authService.signOut(signOutUserDto);
return { message: 'User signed out successfully' };
}
// @Post('forgot-password')
// async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
// await this.authService.forgotPassword(forgotPasswordDto);
// return { message: 'Password reset link sent to your email' };
// }
@UseGuards(JwtRefreshGuard)
@Patch('refresh-token')
//@RequirePermissions('auth:refresh-token')
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return await this.authService.refreshToken(refreshTokenDto);
}
}

View File

@@ -0,0 +1,12 @@
import { MailModule } from '@/features/mail/mail.module';
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { DrizzleModule } from '@/database/drizzle.module';
@Module({
imports: [DrizzleModule, MailModule],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,368 @@
import { envs } from '@/common/config/envs';
import { Env, validateString } from '@/common/utils';
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
import {
LoginUserInterface,
Roles,
} from '@/features/auth/interfaces/login-user.interface';
import RefreshTokenInterface from '@/features/auth/interfaces/refresh-token.interface';
import { MailService } from '@/features/mail/mail.service';
import { User } from '@/features/users/entities/user.entity';
import {
HttpException,
HttpStatus,
Inject,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import crypto from 'crypto';
import { and, eq, or } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { sessions, users, roles, usersRole } from 'src/database/index';
import { Session } from './interfaces/session.interface';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
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
private decodeToken(token: string): {
sub: number;
username?: string;
iat: number;
exp: number;
} {
try {
const decoded = this.jwtService.decode(token) as {
sub: number;
username?: string;
iat: number;
exp: number;
};
// Validar que contiene los datos esenciales
if (!decoded || !decoded.exp || !decoded.iat) {
throw new Error('Token lacks required fields');
}
return decoded;
} catch (error) {
// Manejo seguro del tipo unknown
let errorMessage = 'Failed to decode token';
if (error instanceof Error) {
errorMessage = error.message;
console.error('Error decoding token:', errorMessage);
} else {
console.error('Unknown error type:', error);
}
throw new HttpException(errorMessage, HttpStatus.UNAUTHORIZED);
}
}
//Generate Tokens
async generateTokens(user: User): Promise<AuthTokensInterface> {
const [access_token, refresh_token] = await Promise.all([
this.jwtService.signAsync(
{
sub: user.id,
username: user.username,
},
{
secret: envs.access_token_secret,
expiresIn: envs.access_token_expiration,
},
),
this.jwtService.signAsync(
{
sub: user.id,
username: user.username,
},
{
secret: envs.refresh_token_secret,
expiresIn: envs.refresh_token_expiration,
},
),
]);
return {
access_token,
refresh_token,
};
}
//Generate OTP Code For Email Confirmation
async generateOTP(length = 6): Promise<string> {
return crypto
.randomInt(0, 10 ** length)
.toString()
.padStart(length, '0');
}
// metodo para crear una session
private async createSession(sessionInput: Session): Promise<string> {
const { userId } = sessionInput;
const activeSessionsCount = await this.drizzle
.select()
.from(sessions)
.where(eq(sessions.userId, parseInt(userId)));
if (activeSessionsCount.length !== 0) {
// Elimina sessiones viejsas
await this.drizzle
.delete(sessions)
.where(eq(sessions.userId, parseInt(userId)));
}
const session = await this.drizzle.insert(sessions).values({
sessionToken: sessionInput.sessionToken,
userId: parseInt(userId),
expiresAt: sessionInput.expiresAt,
});
if (session.rowCount === 0) throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
return 'Session created successfully';
}
//Find User
async findUser(username: string): Promise<User | null> {
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.username, username));
return user[0];
}
//Find User
async findUserById(id: number): Promise<User | null> {
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.id, id));
return user[0];
}
//Check User Is Already Exists
async validateUser(dto: ValidateUserDto): Promise<User> {
const user = await this.findUser(dto.username);
if (!user) throw new NotFoundException('User not found');
const isValid = await validateString(
dto.password,
user?.password as string,
);
if (!isValid) throw new UnauthorizedException('Invalid credentials');
return user;
}
//Find rol user
async findUserRol(id: number): Promise<Roles[]> {
const roles = await this.drizzle
.select({
id: schema.roles.id,
role: schema.roles.name,
})
.from(schema.usersRole)
.leftJoin(schema.roles, eq(schema.roles.id, schema.usersRole.roleId))
.where(eq(schema.usersRole.userId, id));
if (roles.length === 0) {
throw new NotFoundException('User not found');
}
// Aseguramos que no haya valores nulos
return roles.map((role) => ({
id: role.id ?? 0, // Asignamos un valor por defecto (0) si es null
rol: role.role ?? '', // Asignamos un valor por defecto (cadena vacía) si es null
}));
}
//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);
const decodeRefresh = this.decodeToken(tokens.refresh_token);
const rol = await this.findUserRol(user?.id as number);
await this.createSession({
userId: String(user?.id), // Convert number to string
sessionToken: tokens.refresh_token,
expiresAt: decodeRefresh.exp,
});
return {
message: 'User signed in successfully',
user: {
id: user?.id as number,
username: user?.username,
fullname: user?.fullname,
email: user?.email,
rol: rol,
},
tokens: {
access_token: tokens.access_token,
access_expire_in: decodeAccess.exp,
refresh_token: tokens.refresh_token,
refresh_expire_in: decodeRefresh.exp,
},
};
}
// //Forgot Password
// async forgotPassword(dto: ForgotPasswordDto): Promise<void> {
// const user = await this.findUser(dto.username);
// if (!user) throw new NotFoundException('User not found');
// const passwordResetToken = await this.generateOTP();
// user.passwordResetToken = passwordResetToken;
// user.passwordResetTokenExpires = new Date(
// Date.now() + 1000 * 60 * 60 * 24, // 1 day
// );
// await this.UserRepository.save(user);
// await this.mailService.sendEmail({
// to: [user.email],
// subject: 'Reset Password',
// html: ForgotPasswordMail({
// name: user.name,
// code: passwordResetToken,
// }),
// });
// }
//Sign Out User Account
async signOut(dto: SignOutUserDto): Promise<void> {
const { user_id } = dto;
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.id, parseInt(user_id)));
if (!user) throw new NotFoundException('User not found');
await this.drizzle
.delete(sessions)
.where(eq(sessions.userId, parseInt(user_id)));
}
//Refresh User Access Token
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
const { user_id } = dto;
const session = await this.drizzle
.select()
.from(sessions)
.where(
and(
eq(sessions.userId, user_id) &&
eq(sessions.sessionToken, dto.refresh_token),
),
);
if (session.length === 0) throw new NotFoundException('session not found');
const user = await this.findUserById(dto.user_id);
if (!user) throw new NotFoundException('User not found');
const tokens = await this.generateTokens(user);
const decodeAccess = this.decodeToken(tokens.access_token);
const decodeRefresh = this.decodeToken(tokens.refresh_token);
await this.drizzle
.update(sessions)
.set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp })
.where(eq(sessions.userId, dto.user_id));
return {
access_token: tokens.access_token,
access_expire_in: decodeAccess.exp,
refresh_token: tokens.refresh_token,
refresh_expire_in: decodeRefresh.exp,
};
}
async singUp(createUserDto: SingUpUserDto): Promise<User> {
// 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,
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

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ChangePasswordDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
@ApiProperty()
@IsString({
message: 'New password must be a string',
})
newPassword: string;
}

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator';
export class ConfirmEmailDto {
@ApiProperty()
@IsString()
@MaxLength(6)
@MinLength(6)
code: string;
@ApiProperty()
@IsEmail()
email: string;
}

View File

@@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsOptional, IsString } from 'class-validator';
export class CreateUserDto {
@ApiProperty()
@IsEmail()
username: string;
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsEmail()
fullname: string;
@ApiProperty()
@IsString({
message: 'Phone must be a string',
})
@IsOptional()
phone: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ForgotPasswordDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
}

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class RefreshTokenDto {
@ApiProperty()
@IsString({
message: 'Refresh token must be a string',
})
refresh_token: string;
@ApiProperty()
@IsNumber()
user_id: number;
}

View File

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ResetPasswordDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Reset Token must be a string',
})
resetToken: string;
@ApiProperty()
@IsString({
message: 'New password must be a string',
})
newPassword: string;
}

View File

@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class SignInUserDto {
@ApiProperty()
@IsString({
message: 'Identifier must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class SignOutUserDto {
@ApiProperty()
@IsString({
message: 'User Id must be a string',
})
user_id: string;
}

View File

@@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator';
export class SingUpUserDto {
@ApiProperty()
@IsString()
username: string;
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsString()
fullname: string;
@ApiProperty()
@IsString({
message: 'Phone must be a string',
})
@IsOptional()
phone: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
@ApiProperty()
@IsInt()
state: number;
@ApiProperty()
@IsInt()
municipality: number;
@ApiProperty()
@IsInt()
parish: number;
@ApiProperty()
@IsInt()
role: number;
}

View File

@@ -0,0 +1,6 @@
import { User } from '@/features/users/entities/user.entity';
export class UpdateRefreshTokenDto {
user: User;
refresh_token: string;
}

View File

@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class ValidateUserDto {
@ApiProperty()
@IsString({
message: 'First name must be a string',
})
username: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
}

View File

@@ -0,0 +1,6 @@
interface AuthTokensInterface {
access_token: string;
refresh_token: string;
}
export default AuthTokensInterface;

View File

@@ -0,0 +1,25 @@
export interface LoginUserInterface {
message: string;
user: User;
tokens: Tokens;
}
interface User {
id: number;
username: string;
fullname: string;
email?: string;
rol: Roles[];
}
export interface Roles {
id: number;
rol: string;
}
interface Tokens {
access_token: string;
access_expire_in: number;
refresh_token: string;
refresh_expire_in: number;
}

View File

@@ -0,0 +1,8 @@
interface RefreshTokenInterface {
access_token: string;
access_expire_in: number;
refresh_token: string;
refresh_expire_in: number;
}
export default RefreshTokenInterface;

View File

@@ -0,0 +1,5 @@
export interface Session {
userId: string;
sessionToken: string;
expiresAt: number;
}

View File

@@ -0,0 +1,96 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CategoryTypesService } from './category-types.service';
import { CreateCategoryTypeDto } from './dto/create-category-type.dto';
import { UpdateCategoryTypeDto } from './dto/update-category-type.dto';
import { CategoryType } from './entities/category-type.entity';
@ApiTags('Category Types')
@Controller('configurations/category-types')
export class CategoryTypesController {
constructor(private readonly categoryTypesService: CategoryTypesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all category types' })
@ApiResponse({
status: 200,
description: 'Return all category types',
type: [CategoryType],
})
findAll() {
return this.categoryTypesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a category type by id' })
@ApiResponse({
status: 200,
description: 'Return a category type',
type: CategoryType,
})
@ApiResponse({ status: 404, description: 'Category type not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.categoryTypesService.findOne(id);
}
@Get('group/:group')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get category types by group' })
@ApiResponse({
status: 200,
description: 'Return category types by group',
type: [CategoryType],
})
findByGroup(@Param('group') group: string) {
return this.categoryTypesService.findByGroup(group);
}
@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create a new category type' })
@ApiResponse({
status: 201,
description: 'Category type created',
type: CategoryType,
})
create(@Body() createCategoryTypeDto: CreateCategoryTypeDto) {
return this.categoryTypesService.create(createCategoryTypeDto);
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update a category type' })
@ApiResponse({
status: 200,
description: 'Category type updated',
type: CategoryType,
})
@ApiResponse({ status: 404, description: 'Category type not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateCategoryTypeDto: UpdateCategoryTypeDto,
) {
return this.categoryTypesService.update(id, updateCategoryTypeDto);
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: 'Delete a category type' })
@ApiResponse({ status: 200, description: 'Category type deleted' })
@ApiResponse({ status: 404, description: 'Category type not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.categoryTypesService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { CategoryTypesController } from './category-types.controller';
import { CategoryTypesService } from './category-types.service';
@Module({
imports: [DrizzleModule],
controllers: [CategoryTypesController],
providers: [CategoryTypesService],
exports: [CategoryTypesService],
})
export class CategoryTypesModule {}

View File

@@ -0,0 +1,81 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { categoryType } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { CreateCategoryTypeDto } from './dto/create-category-type.dto';
import { UpdateCategoryTypeDto } from './dto/update-category-type.dto';
import { CategoryType } from './entities/category-type.entity';
@Injectable()
export class CategoryTypesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) {}
async findAll(): Promise<CategoryType[]> {
return await this.drizzle.select().from(categoryType);
}
async findOne(id: number): Promise<CategoryType> {
const category = await this.drizzle
.select()
.from(categoryType)
.where(eq(categoryType.id, id));
if (category.length === 0) {
throw new HttpException('Category type not found', HttpStatus.NOT_FOUND);
}
return category[0];
}
async findByGroup(group: string): Promise<CategoryType[]> {
return await this.drizzle
.select()
.from(categoryType)
.where(eq(categoryType.group, group));
}
async create(
createCategoryTypeDto: CreateCategoryTypeDto,
): Promise<CategoryType> {
const [category] = await this.drizzle
.insert(categoryType)
.values({
group: createCategoryTypeDto.group,
description: createCategoryTypeDto.description,
})
.returning();
return category;
}
async update(
id: number,
updateCategoryTypeDto: UpdateCategoryTypeDto,
): Promise<CategoryType> {
// Check if category type exists
await this.findOne(id);
await this.drizzle
.update(categoryType)
.set({
group: updateCategoryTypeDto.group,
description: updateCategoryTypeDto.description,
})
.where(eq(categoryType.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if category type exists
await this.findOne(id);
await this.drizzle.delete(categoryType).where(eq(categoryType.id, id));
return { message: 'Category type deleted successfully' };
}
}

View File

@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
export class CreateCategoryTypeDto {
@ApiProperty({
description: 'The group of the category type',
example: 'PAYROLL_TYPE',
})
@IsNotEmpty()
@IsString()
group: string;
@ApiProperty({
description: 'The description of the category type',
example: 'Quincenal',
})
@IsNotEmpty()
@IsString()
description: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCategoryTypeDto } from './create-category-type.dto';
export class UpdateCategoryTypeDto extends PartialType(CreateCategoryTypeDto) {}

View File

@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';
export class CategoryType {
@ApiProperty({
description: 'The unique identifier of the category type',
example: 1,
})
id: number;
@ApiProperty({
description: 'The group of the category type',
example: 'PAYROLL_TYPE',
})
group: string;
@ApiProperty({
description: 'The description of the category type',
example: 'Quincenal',
})
description: string;
@ApiProperty({
description: 'The date when the category type was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date;
@ApiProperty({
description: 'The date when the category type was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at?: Date | null;
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { CategoryTypesModule } from './category-types/category-types.module';
import { MunicipalitiesModule } from './municipalities/municipalities.module';
import { ParishesModule } from './parishes/parishes.module';
import { StatesModule } from './states/states.module';
@Module({
imports: [
StatesModule,
MunicipalitiesModule,
ParishesModule,
CategoryTypesModule,
],
})
export class ConfigurationsModule {}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreateMunicipalityDto {
@ApiProperty({
description: 'The name of the municipality',
example: 'Los Angeles',
})
@IsNotEmpty()
@IsString()
name: string;
@ApiProperty({
description: 'The ID of the state this municipality belongs to',
example: 1,
})
@IsNotEmpty()
@IsNumber()
stateId: number;
}

View File

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

View File

@@ -0,0 +1,39 @@
import { ApiProperty } from '@nestjs/swagger';
export class Municipality {
@ApiProperty({
description: 'The unique identifier of the municipality',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the municipality',
example: 'Los Angeles',
})
name: string;
@ApiProperty({
description: 'The ID of the state this municipality belongs to',
example: 1,
})
stateId: number;
@ApiProperty({
description: 'The name of the state this municipality belongs to',
example: 'California',
})
stateName?: string | null;
@ApiProperty({
description: 'The date when the municipality was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date | null;
@ApiProperty({
description: 'The date when the municipality was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at?: Date | null;
}

View File

@@ -0,0 +1,97 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateMunicipalityDto } from './dto/create-municipality.dto';
import { UpdateMunicipalityDto } from './dto/update-municipality.dto';
import { Municipality } from './entities/municipality.entity';
import { MunicipalitiesService } from './municipalities.service';
@ApiTags('Municipalities')
@Controller('configurations/municipalities')
export class MunicipalitiesController {
constructor(private readonly municipalitiesService: MunicipalitiesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all municipalities' })
@ApiResponse({
status: 200,
description: 'Return all municipalities',
type: [Municipality],
})
findAll() {
return this.municipalitiesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a municipality by id' })
@ApiResponse({
status: 200,
description: 'Return a municipality',
type: Municipality,
})
@ApiResponse({ status: 404, description: 'Municipality not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.municipalitiesService.findOne(id);
}
@Get('state/:stateId')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get municipalities by state id' })
@ApiResponse({
status: 200,
description: 'Return municipalities by state',
type: [Municipality],
})
@ApiResponse({ status: 404, description: 'State not found' })
findByState(@Param('stateId', ParseIntPipe) stateId: number) {
return this.municipalitiesService.findByState(stateId);
}
@Post()
@Roles('ADMIN')
@ApiOperation({ summary: 'Create a new municipality' })
@ApiResponse({
status: 201,
description: 'Municipality created',
type: Municipality,
})
create(@Body() createMunicipalityDto: CreateMunicipalityDto) {
return this.municipalitiesService.create(createMunicipalityDto);
}
@Patch(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Update a municipality' })
@ApiResponse({
status: 200,
description: 'Municipality updated',
type: Municipality,
})
@ApiResponse({ status: 404, description: 'Municipality not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateMunicipalityDto: UpdateMunicipalityDto,
) {
return this.municipalitiesService.update(id, updateMunicipalityDto);
}
@Delete(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Delete a municipality' })
@ApiResponse({ status: 200, description: 'Municipality deleted' })
@ApiResponse({ status: 404, description: 'Municipality not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.municipalitiesService.remove(id);
}
}

View File

@@ -0,0 +1,13 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { StatesModule } from '../states/states.module';
import { MunicipalitiesController } from './municipalities.controller';
import { MunicipalitiesService } from './municipalities.service';
@Module({
imports: [DrizzleModule, StatesModule],
controllers: [MunicipalitiesController],
providers: [MunicipalitiesService],
exports: [MunicipalitiesService],
})
export class MunicipalitiesModule {}

View File

@@ -0,0 +1,120 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { municipalities, states } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { StatesService } from '../states/states.service';
import { CreateMunicipalityDto } from './dto/create-municipality.dto';
import { UpdateMunicipalityDto } from './dto/update-municipality.dto';
import { Municipality } from './entities/municipality.entity';
@Injectable()
export class MunicipalitiesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private statesService: StatesService,
) {}
async findAll(): Promise<Municipality[]> {
return await this.drizzle
.select({
id: municipalities.id,
name: municipalities.name,
stateId: municipalities.stateId,
stateName: states.name,
created_at: municipalities.created_at,
updated_at: municipalities.updated_at,
})
.from(municipalities)
.leftJoin(states, eq(municipalities.stateId, states.id));
}
async findOne(id: number): Promise<Municipality> {
const municipality = await this.drizzle
.select({
id: municipalities.id,
name: municipalities.name,
stateId: municipalities.stateId,
stateName: states.name,
created_at: municipalities.created_at,
updated_at: municipalities.updated_at,
})
.from(municipalities)
.leftJoin(states, eq(municipalities.stateId, states.id))
.where(eq(municipalities.id, id));
if (municipality.length === 0) {
throw new HttpException('Municipality not found', HttpStatus.NOT_FOUND);
}
return municipality[0];
}
async findByState(stateId: number): Promise<Municipality[]> {
// Verify state exists
await this.statesService.findOne(stateId);
return await this.drizzle
.select({
id: municipalities.id,
name: municipalities.name,
stateId: municipalities.stateId,
stateName: states.name,
created_at: municipalities.created_at,
updated_at: municipalities.updated_at,
})
.from(municipalities)
.leftJoin(states, eq(municipalities.stateId, states.id))
.where(eq(municipalities.stateId, stateId));
}
async create(
createMunicipalityDto: CreateMunicipalityDto,
): Promise<Municipality> {
// Verify state exists
await this.statesService.findOne(createMunicipalityDto.stateId);
const [municipality] = await this.drizzle
.insert(municipalities)
.values({
name: createMunicipalityDto.name,
stateId: createMunicipalityDto.stateId,
})
.returning();
return this.findOne(municipality.id);
}
async update(
id: number,
updateMunicipalityDto: UpdateMunicipalityDto,
): Promise<Municipality> {
// Check if municipality exists
await this.findOne(id);
// If stateId is provided, verify it exists
if (updateMunicipalityDto.stateId) {
await this.statesService.findOne(updateMunicipalityDto.stateId);
}
await this.drizzle
.update(municipalities)
.set({
name: updateMunicipalityDto.name,
stateId: updateMunicipalityDto.stateId,
})
.where(eq(municipalities.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if municipality exists
await this.findOne(id);
await this.drizzle.delete(municipalities).where(eq(municipalities.id, id));
return { message: 'Municipality deleted successfully' };
}
}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreateParishDto {
@ApiProperty({
description: 'The name of the parish',
example: 'Downtown',
})
@IsNotEmpty()
@IsString()
name: string;
@ApiProperty({
description: 'The ID of the municipality this parish belongs to',
example: 1,
})
@IsNotEmpty()
@IsNumber()
municipalityId: number;
}

View File

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

View File

@@ -0,0 +1,39 @@
import { ApiProperty } from '@nestjs/swagger';
export class Parish {
@ApiProperty({
description: 'The unique identifier of the parish',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the parish',
example: 'Downtown',
})
name: string;
@ApiProperty({
description: 'The ID of the municipality this parish belongs to',
example: 1,
})
municipalityId: number;
@ApiProperty({
description: 'The name of the municipality this parish belongs to',
example: 'Los Angeles',
})
municipalityName?: string | null;
@ApiProperty({
description: 'The date when the parish was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date | null;
@ApiProperty({
description: 'The date when the parish was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at: Date | null;
}

View File

@@ -0,0 +1,99 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateParishDto } from './dto/create-parish.dto';
import { UpdateParishDto } from './dto/update-parish.dto';
import { Parish } from './entities/parish.entity';
import { ParishesService } from './parishes.service';
@ApiTags('Parishes')
@Controller('configurations/parishes')
export class ParishesController {
constructor(private readonly parishesService: ParishesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all parishes' })
@ApiResponse({
status: 200,
description: 'Return all parishes',
type: [Parish],
})
findAll() {
return this.parishesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a parish by id' })
@ApiResponse({
status: 200,
description: 'Return a parish',
type: Parish,
})
@ApiResponse({ status: 404, description: 'Parish not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.parishesService.findOne(id);
}
@Get('municipality/:municipalityId')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get parishes by municipality id' })
@ApiResponse({
status: 200,
description: 'Return parishes by municipality',
type: [Parish],
})
@ApiResponse({ status: 404, description: 'Municipality not found' })
findByMunicipality(
@Param('municipalityId', ParseIntPipe) municipalityId: number,
) {
return this.parishesService.findByMunicipality(municipalityId);
}
@Post()
@Roles('ADMIN')
@ApiOperation({ summary: 'Create a new parish' })
@ApiResponse({
status: 201,
description: 'Parish created',
type: Parish,
})
create(@Body() createParishDto: CreateParishDto) {
return this.parishesService.create(createParishDto);
}
@Patch(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Update a parish' })
@ApiResponse({
status: 200,
description: 'Parish updated',
type: Parish,
})
@ApiResponse({ status: 404, description: 'Parish not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateParishDto: UpdateParishDto,
) {
return this.parishesService.update(id, updateParishDto);
}
@Delete(':id')
@Roles('ADMIN')
@ApiOperation({ summary: 'Delete a parish' })
@ApiResponse({ status: 200, description: 'Parish deleted' })
@ApiResponse({ status: 404, description: 'Parish not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.parishesService.remove(id);
}
}

View File

@@ -0,0 +1,13 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { MunicipalitiesModule } from '../municipalities/municipalities.module';
import { ParishesController } from './parishes.controller';
import { ParishesService } from './parishes.service';
@Module({
imports: [DrizzleModule, MunicipalitiesModule],
controllers: [ParishesController],
providers: [ParishesService],
exports: [ParishesService],
})
export class ParishesModule {}

View File

@@ -0,0 +1,115 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { municipalities, parishes } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { MunicipalitiesService } from '../municipalities/municipalities.service';
import { CreateParishDto } from './dto/create-parish.dto';
import { UpdateParishDto } from './dto/update-parish.dto';
import { Parish } from './entities/parish.entity';
@Injectable()
export class ParishesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private municipalitiesService: MunicipalitiesService,
) {}
async findAll(): Promise<Parish[]> {
return await this.drizzle
.select({
id: parishes.id,
name: parishes.name,
municipalityId: parishes.municipalityId,
municipalityName: municipalities.name,
created_at: parishes.created_at,
updated_at: parishes.updated_at,
})
.from(parishes)
.leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id));
}
async findOne(id: number): Promise<Parish> {
const parish = await this.drizzle
.select({
id: parishes.id,
name: parishes.name,
municipalityId: parishes.municipalityId,
municipalityName: municipalities.name,
created_at: parishes.created_at,
updated_at: parishes.updated_at,
})
.from(parishes)
.leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id))
.where(eq(parishes.id, id));
if (parish.length === 0) {
throw new HttpException('Parish not found', HttpStatus.NOT_FOUND);
}
return parish[0];
}
async findByMunicipality(municipalityId: number): Promise<Parish[]> {
// Verify municipality exists
await this.municipalitiesService.findOne(municipalityId);
return await this.drizzle
.select({
id: parishes.id,
name: parishes.name,
municipalityId: parishes.municipalityId,
municipalityName: municipalities.name,
created_at: parishes.created_at,
updated_at: parishes.updated_at,
})
.from(parishes)
.leftJoin(municipalities, eq(parishes.municipalityId, municipalities.id))
.where(eq(parishes.municipalityId, municipalityId));
}
async create(createParishDto: CreateParishDto): Promise<Parish> {
// Verify municipality exists
await this.municipalitiesService.findOne(createParishDto.municipalityId);
const [parish] = await this.drizzle
.insert(parishes)
.values({
name: createParishDto.name,
municipalityId: createParishDto.municipalityId,
})
.returning();
return this.findOne(parish.id);
}
async update(id: number, updateParishDto: UpdateParishDto): Promise<Parish> {
// Check if parish exists
await this.findOne(id);
// If municipalityId is provided, verify it exists
if (updateParishDto.municipalityId) {
await this.municipalitiesService.findOne(updateParishDto.municipalityId);
}
await this.drizzle
.update(parishes)
.set({
name: updateParishDto.name,
municipalityId: updateParishDto.municipalityId,
})
.where(eq(parishes.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if parish exists
await this.findOne(id);
await this.drizzle.delete(parishes).where(eq(parishes.id, id));
return { message: 'Parish deleted successfully' };
}
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateStateDto {
@ApiProperty({
description: 'The name of the state',
example: 'California',
})
@IsNotEmpty()
@IsString()
name: string;
}

View File

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

View File

@@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger';
export class State {
@ApiProperty({
description: 'The unique identifier of the state',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the state',
example: 'California',
})
name: string;
@ApiProperty({
description: 'The date when the state was created',
example: '2023-01-01T00:00:00.000Z',
})
created_at?: Date | null;
@ApiProperty({
description: 'The date when the state was last updated',
example: '2023-01-01T00:00:00.000Z',
})
updated_at?: Date | null;
}

View File

@@ -0,0 +1,68 @@
import { Roles } from '@/common/decorators';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { UpdateStateDto } from './dto//update-state.dto';
import { CreateStateDto } from './dto/create-state.dto';
import { State } from './entities/state.entity';
import { StatesService } from './states.service';
@ApiTags('States')
@Controller('configurations/states')
export class StatesController {
constructor(private readonly statesService: StatesService) {}
@Get()
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get all states' })
@ApiResponse({ status: 200, description: 'Return all states', type: [State] })
findAll() {
return this.statesService.findAll();
}
@Get(':id')
@Roles('admin', 'user')
@ApiOperation({ summary: 'Get a state by id' })
@ApiResponse({ status: 200, description: 'Return a state', type: State })
@ApiResponse({ status: 404, description: 'State not found' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.statesService.findOne(id);
}
@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create a new state' })
@ApiResponse({ status: 201, description: 'State created', type: State })
create(@Body() createStateDto: CreateStateDto) {
return this.statesService.create(createStateDto);
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update a state' })
@ApiResponse({ status: 200, description: 'State updated', type: State })
@ApiResponse({ status: 404, description: 'State not found' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateStateDto: UpdateStateDto,
) {
return this.statesService.update(id, updateStateDto);
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: 'Delete a state' })
@ApiResponse({ status: 200, description: 'State deleted' })
@ApiResponse({ status: 404, description: 'State not found' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.statesService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { DrizzleModule } from '@/database/drizzle.module';
import { Module } from '@nestjs/common';
import { StatesController } from './states.controller';
import { StatesService } from './states.service';
@Module({
imports: [DrizzleModule],
controllers: [StatesController],
providers: [StatesService],
exports: [StatesService],
})
export class StatesModule {}

View File

@@ -0,0 +1,67 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import * as schema from '@/database/index';
import { states } from '@/database/schema/general';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { CreateStateDto } from './dto/create-state.dto';
import { UpdateStateDto } from './dto/update-state.dto';
import { State } from './entities/state.entity';
@Injectable()
export class StatesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) {}
async findAll(): Promise<State[]> {
return await this.drizzle.select().from(states);
}
async findOne(id: number): Promise<State> {
const state = await this.drizzle
.select()
.from(states)
.where(eq(states.id, id));
if (state.length === 0) {
throw new HttpException('State not found', HttpStatus.NOT_FOUND);
}
return state[0];
}
async create(createStateDto: CreateStateDto): Promise<State> {
const [state] = await this.drizzle
.insert(states)
.values({
name: createStateDto.name,
})
.returning();
return state;
}
async update(id: number, updateStateDto: UpdateStateDto): Promise<State> {
// Check if state exists
await this.findOne(id);
await this.drizzle
.update(states)
.set({
name: updateStateDto.name,
})
.where(eq(states.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if state exists
await this.findOne(id);
await this.drizzle.delete(states).where(eq(states.id, id));
return { message: 'State deleted successfully' };
}
}

View File

@@ -0,0 +1,16 @@
export class State {
id: number;
name: string;
}
export class Municipality {
id: number;
stateId: number;
name: string;
}
export class Parish {
id: number;
municipalityId: number;
name: string;
}

View File

@@ -0,0 +1,41 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { UsersService } from './location.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '@/common/decorators';
// import { Roles } from '../../common/decorators/roles.decorator';
// import { PaginationDto } from '../../common/dto/pagination.dto';
@Public()
@ApiTags('location')
@Controller('location')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get("state")
// @Roles('admin')
// @ApiOperation({ summary: 'Get all users with pagination and filters' })
// @ApiResponse({ status: 200, description: 'Return paginated users.' })
async findState() {
const data = await this.usersService.StateAll();
return { message: 'Data fetched successfully', data};
}
@Get('municipality/:id')
// @Roles('admin')
// @ApiOperation({ summary: 'Get a user by ID' })
// @ApiResponse({ status: 200, description: 'Return the user.' })
// @ApiResponse({ status: 404, description: 'User not found.' })
async findMunicipality(@Param('id') id: string) {
const data = await this.usersService.MunicioalityAll(id);
return { message: 'Data fetched successfully', data };
}
@Get('parish/:id')
// @Roles('admin')
// @ApiOperation({ summary: 'Get a user by ID' })
// @ApiResponse({ status: 200, description: 'Return the user.' })
// @ApiResponse({ status: 404, description: 'User not found.' })
async findParish(@Param('id') id: string) {
const data = await this.usersService.ParishAll(id);
return { message: 'Data fetched successfully', data };
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UsersController } from './location.controller';
import { UsersService } from './location.service';
import { DrizzleModule } from '@/database/drizzle.module';
@Module({
imports: [DrizzleModule],
controllers: [UsersController],
providers: [UsersService],
})
export class LocationModule {}

View File

@@ -0,0 +1,44 @@
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
// import { Env, validateString } from '@/common/utils';
import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { states, municipalities, parishes } from 'src/database/index';
import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm';
import * as bcrypt from 'bcryptjs';
import { State, Municipality, Parish } from './entities/user.entity';
// import { PaginationDto } from '../../common/dto/pagination.dto';
@Injectable()
export class UsersService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) { }
async StateAll(): Promise< State[]> {
const find = await this.drizzle
.select()
.from(states)
return find;
}
async MunicioalityAll(id: string): Promise< Municipality[]> {
const find = await this.drizzle
.select()
.from(municipalities)
.where(eq(municipalities.stateId, parseInt(id)));
return find;
}
async ParishAll(id: string): Promise< Parish[]> {
const find = await this.drizzle
.select()
.from(parishes)
.where(eq(parishes.municipalityId, parseInt(id)));
return find;
}
}

View File

@@ -0,0 +1,11 @@
import { MailerModule } from '@nestjs-modules/mailer';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MailService } from './mail.service';
@Module({
imports: [MailerModule, ConfigModule],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MailService } from './mail.service';
describe('MailService', () => {
let service: MailService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MailService],
}).compile();
service = module.get<MailService>(MailService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,18 @@
import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MailService {
constructor(
private readonly mailerService: MailerService,
private readonly config: ConfigService,
) {}
async sendEmail(mailOptions: ISendMailOptions): Promise<void> {
await this.mailerService.sendMail({
from: `Turbo NPN <${this.config.get('MAIL_USERNAME')}>`,
...mailOptions,
});
}
}

View File

@@ -0,0 +1,177 @@
const ChangePasswordMail = ({ name }: { name: string }) => {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Join Turbo NPN
</div>
<body
style="
background-color: rgb(255, 255, 255);
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
margin-right: auto;
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
padding-left: 0.5rem;
padding-right: 0.5rem;
"
>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
border-radius: 0.25rem;
margin-top: 40px;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
padding: 20px;
max-width: 465px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 32px"
>
<tbody>
<tr>
<td>
<img
alt="Vercel"
height="37"
src="https://react-email-demo-2jmpohtxd-resend.vercel.app/static/vercel-logo.png"
style="
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="40"
/>
</td>
</tr>
</tbody>
</table>
<h1
style="
color: rgb(0, 0, 0);
font-size: 24px;
font-weight: 400;
text-align: center;
padding: 0px;
margin-top: 30px;
margin-bottom: 30px;
margin-left: 0px;
margin-right: 0px;
"
>
<strong>Turbo NPN</strong>
</h1>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Hello, ${name}
</p>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Turbo NPN send your sign in notification email
</p>
<div style="display: flex;justify-content: center; align-items: center;">
<p
style="
background-color: rgb(0, 0, 0);
border-radius: 0.25rem;
color: rgb(255, 255, 255);
font-size: 12px;
font-weight: 600;
text-align: center;
padding: 0.75rem 1.25rem;
line-height: 100%;
text-decoration: none;
display: inline-block;
width: 100%;
"
>
New Sign In Detected
</p>
</div>
<hr
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
margin-top: 26px;
margin-bottom: 26px;
margin-left: 0px;
margin-right: 0px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<p
style="
color: rgb(102, 102, 102);
font-size: 12px;
line-height: 24px;
margin: 16px 0;
text-align: center;
"
>
Turbo NPN is a free and open source prodcut
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`;
};
export default ChangePasswordMail;

View File

@@ -0,0 +1,177 @@
const SignInMail = ({ name }: { name: string }) => {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Join Turbo NPN
</div>
<body
style="
background-color: rgb(255, 255, 255);
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
margin-right: auto;
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
padding-left: 0.5rem;
padding-right: 0.5rem;
"
>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
border-radius: 0.25rem;
margin-top: 40px;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
padding: 20px;
max-width: 465px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 32px"
>
<tbody>
<tr>
<td>
<img
alt="Vercel"
height="37"
src="https://react-email-demo-2jmpohtxd-resend.vercel.app/static/vercel-logo.png"
style="
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="40"
/>
</td>
</tr>
</tbody>
</table>
<h1
style="
color: rgb(0, 0, 0);
font-size: 24px;
font-weight: 400;
text-align: center;
padding: 0px;
margin-top: 30px;
margin-bottom: 30px;
margin-left: 0px;
margin-right: 0px;
"
>
<strong>Turbo NPN</strong>
</h1>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Hello, ${name}
</p>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Turbo NPN send your email verification success message
</p>
<div style="display: flex;justify-content: center; align-items: center;">
<p
style="
background-color: rgb(0, 0, 0);
border-radius: 0.25rem;
color: rgb(255, 255, 255);
font-size: 12px;
font-weight: 600;
text-align: center;
padding: 0.75rem 1.25rem;
line-height: 100%;
text-decoration: none;
display: inline-block;
width: 100%;
"
>
Success
</p>
</div>
<hr
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
margin-top: 26px;
margin-bottom: 26px;
margin-left: 0px;
margin-right: 0px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<p
style="
color: rgb(102, 102, 102);
font-size: 12px;
line-height: 24px;
margin: 16px 0;
text-align: center;
"
>
Turbo NPN is a free and open source prodcut
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`;
};
export default SignInMail;

View File

@@ -0,0 +1,183 @@
const ForgotPasswordMail = ({
name,
code,
}: {
name: string;
code: string | number;
}) => {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Join Turbo NPN
</div>
<body
style="
background-color: rgb(255, 255, 255);
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
margin-right: auto;
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
padding-left: 0.5rem;
padding-right: 0.5rem;
"
>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
border-radius: 0.25rem;
margin-top: 40px;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
padding: 20px;
max-width: 465px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 32px"
>
<tbody>
<tr>
<td>
<img
alt="Vercel"
height="37"
src="https://react-email-demo-2jmpohtxd-resend.vercel.app/static/vercel-logo.png"
style="
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="40"
/>
</td>
</tr>
</tbody>
</table>
<h1
style="
color: rgb(0, 0, 0);
font-size: 24px;
font-weight: 400;
text-align: center;
padding: 0px;
margin-top: 30px;
margin-bottom: 30px;
margin-left: 0px;
margin-right: 0px;
"
>
<strong>Turbo NPN</strong>
</h1>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Hello, ${name}
</p>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Turbo NPN send your password reset code
</p>
<div style="display: flex;justify-content: center; align-items: center;">
<p
style="
background-color: rgb(0, 0, 0);
border-radius: 0.25rem;
color: rgb(255, 255, 255);
font-size: 12px;
font-weight: 600;
text-align: center;
padding: 0.75rem 1.25rem;
line-height: 100%;
text-decoration: none;
display: inline-block;
width: 100%;
"
>
${code}
</p>
</div>
<hr
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
margin-top: 26px;
margin-bottom: 26px;
margin-left: 0px;
margin-right: 0px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<p
style="
color: rgb(102, 102, 102);
font-size: 12px;
line-height: 24px;
margin: 16px 0;
text-align: center;
"
>
Turbo NPN is a free and open source prodcut
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`;
};
export default ForgotPasswordMail;

View File

@@ -0,0 +1,195 @@
const EmailTemplate = ({
name = '',
action = 'sign in',
timestamp = new Date().toLocaleString(),
location = 'Unknown',
device = 'Unknown Device',
ipAddress = 'Unknown IP',
}) => {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<div style="display: none; overflow: hidden; line-height: 1px; opacity: 0; max-height: 0; max-width: 0;">
Security Alert - New ${action} detected for your Turbo NPN account
</div>
<body style="
background-color: #f5f5f5;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
padding: 1rem;
">
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="
background-color: #ffffff;
border: 1px solid #eaeaea;
border-radius: 8px;
margin: 40px auto;
padding: 30px;
max-width: 520px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
">
<tbody>
<tr style="width: 100%">
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>
<img
alt="Turbo NPN Logo"
height="45"
src="https://your-logo-url.com/logo.png"
style="
margin: 0 auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="48"
/>
</td>
</tr>
</tbody>
</table>
<h1 style="
color: #000000;
font-size: 24px;
font-weight: 600;
text-align: center;
margin: 30px 0;
">
Security Alert
</h1>
<p style="
color: #333333;
font-size: 16px;
line-height: 24px;
margin: 16px 0;
">
Hello ${name},
</p>
<p style="
color: #333333;
font-size: 16px;
line-height: 24px;
margin: 16px 0;
">
We detected a new ${action} to your Turbo NPN account. Here are the details:
</p>
<div style="
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
">
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 12px 0;">
<tr>
<td style="color: #666666; font-size: 14px; padding: 8px 0;">Time:</td>
<td style="color: #333333; font-size: 14px; text-align: right;">${timestamp}</td>
</tr>
<tr>
<td style="color: #666666; font-size: 14px; padding: 8px 0;">Location:</td>
<td style="color: #333333; font-size: 14px; text-align: right;">${location}</td>
</tr>
<tr>
<td style="color: #666666; font-size: 14px; padding: 8px 0;">Device:</td>
<td style="color: #333333; font-size: 14px; text-align: right;">${device}</td>
</tr>
<tr>
<td style="color: #666666; font-size: 14px; padding: 8px 0;">IP Address:</td>
<td style="color: #333333; font-size: 14px; text-align: right;">${ipAddress}</td>
</tr>
</table>
</div>
<div style="text-align: center; margin: 32px 0;">
<a href="https://turbo-npn.com/security-settings" style="
background-color: #000000;
border-radius: 6px;
color: #ffffff;
font-size: 14px;
font-weight: 600;
text-align: center;
padding: 12px 24px;
text-decoration: none;
display: inline-block;
">
Review Security Settings
</a>
</div>
<p style="
color: #666666;
font-size: 14px;
line-height: 24px;
margin: 24px 0;
">
If you don't recognize this activity, please <a href="https://turbo-npn.com/support" style="color: #000000; text-decoration: underline;">contact our support team</a> immediately and change your password.
</p>
<hr style="
border: none;
border-top: 1px solid #eaeaea;
margin: 26px 0;
width: 100%;
" />
<div style="text-align: center;">
<p style="
color: #666666;
font-size: 12px;
line-height: 24px;
margin: 16px 0;
">
Turbo NPN - Secure, Fast, Reliable
</p>
<div style="margin: 20px 0;">
<a href="https://twitter.com/turbonpn" style="text-decoration: none; margin: 0 8px;">
<img src="https://your-assets.com/twitter.png" alt="Twitter" width="20" height="20" />
</a>
<a href="https://facebook.com/turbonpn" style="text-decoration: none; margin: 0 8px;">
<img src="https://your-assets.com/facebook.png" alt="Facebook" width="20" height="20" />
</a>
<a href="https://linkedin.com/company/turbonpn" style="text-decoration: none; margin: 0 8px;">
<img src="https://your-assets.com/linkedin.png" alt="LinkedIn" width="20" height="20" />
</a>
</div>
<p style="
color: #666666;
font-size: 12px;
line-height: 24px;
">
© 2025 Turbo NPN. All rights reserved.
</p>
<p style="
color: #666666;
font-size: 12px;
line-height: 24px;
">
<a href="https://turbo-npn.com/privacy" style="color: #666666; text-decoration: underline;">Privacy Policy</a> •
<a href="https://turbo-npn.com/terms" style="color: #666666; text-decoration: underline;">Terms of Service</a> •
<a href="https://turbo-npn.com/unsubscribe" style="color: #666666; text-decoration: underline;">Unsubscribe</a>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`;
};
export default EmailTemplate;

View File

@@ -0,0 +1,183 @@
const RegisterMail = ({
name,
code,
}: {
name: string;
code: string | number;
}) => {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Join Turbo NPN
</div>
<body
style="
background-color: rgb(255, 255, 255);
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
margin-right: auto;
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
padding-left: 0.5rem;
padding-right: 0.5rem;
"
>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
border-radius: 0.25rem;
margin-top: 40px;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
padding: 20px;
max-width: 465px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 32px"
>
<tbody>
<tr>
<td>
<img
alt="Vercel"
height="37"
src="https://react-email-demo-2jmpohtxd-resend.vercel.app/static/vercel-logo.png"
style="
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="40"
/>
</td>
</tr>
</tbody>
</table>
<h1
style="
color: rgb(0, 0, 0);
font-size: 24px;
font-weight: 400;
text-align: center;
padding: 0px;
margin-top: 30px;
margin-bottom: 30px;
margin-left: 0px;
margin-right: 0px;
"
>
Join <strong>Turbo NPN</strong>
</h1>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Hello, ${name}
</p>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Turbo NPN send your email verification code
</p>
<div style="display: flex;justify-content: center; align-items: center;">
<p
style="
background-color: rgb(0, 0, 0);
border-radius: 0.25rem;
color: rgb(255, 255, 255);
font-size: 12px;
font-weight: 600;
text-align: center;
padding: 0.75rem 1.25rem;
line-height: 100%;
text-decoration: none;
display: inline-block;
width: 100%;
"
>
${code}
</p>
</div>
<hr
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
margin-top: 26px;
margin-bottom: 26px;
margin-left: 0px;
margin-right: 0px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<p
style="
color: rgb(102, 102, 102);
font-size: 12px;
line-height: 24px;
margin: 16px 0;
text-align: center;
"
>
Turbo NPN is a free and open source prodcut
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`;
};
export default RegisterMail;

View File

@@ -0,0 +1,177 @@
const SignInMail = ({ name }: { name: string }) => {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Join Turbo NPN
</div>
<body
style="
background-color: rgb(255, 255, 255);
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
margin-right: auto;
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
padding-left: 0.5rem;
padding-right: 0.5rem;
"
>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
border-radius: 0.25rem;
margin-top: 40px;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
padding: 20px;
max-width: 465px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 32px"
>
<tbody>
<tr>
<td>
<img
alt="Vercel"
height="37"
src="https://react-email-demo-2jmpohtxd-resend.vercel.app/static/vercel-logo.png"
style="
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="40"
/>
</td>
</tr>
</tbody>
</table>
<h1
style="
color: rgb(0, 0, 0);
font-size: 24px;
font-weight: 400;
text-align: center;
padding: 0px;
margin-top: 30px;
margin-bottom: 30px;
margin-left: 0px;
margin-right: 0px;
"
>
<strong>Turbo NPN</strong>
</h1>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Hello, ${name}
</p>
<p
style="
color: rgb(0, 0, 0);
font-size: 14px;
line-height: 24px;
margin: 16px 0;
"
>
Turbo NPN send your sign in notification email
</p>
<div style="display: flex;justify-content: center; align-items: center;">
<p
style="
background-color: rgb(0, 0, 0);
border-radius: 0.25rem;
color: rgb(255, 255, 255);
font-size: 12px;
font-weight: 600;
text-align: center;
padding: 0.75rem 1.25rem;
line-height: 100%;
text-decoration: none;
display: inline-block;
width: 100%;
"
>
New Sign In Detected
</p>
</div>
<hr
style="
border-width: 1px;
border-style: solid;
border-color: rgb(234, 234, 234);
margin-top: 26px;
margin-bottom: 26px;
margin-left: 0px;
margin-right: 0px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<p
style="
color: rgb(102, 102, 102);
font-size: 12px;
line-height: 24px;
margin: 16px 0;
text-align: center;
"
>
Turbo NPN is a free and open source prodcut
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`;
};
export default SignInMail;

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty } from 'class-validator';
export class CreateRoleDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
}

View File

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

View File

@@ -0,0 +1,6 @@
export class Role {
id: number;
name: string;
createdAt?: Date;
updatedAt?: Date | null;
}

View File

@@ -0,0 +1,59 @@
import { Roles } from '@/common/decorators/roles.decorator';
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { RolesService } from './roles.service';
@ApiTags('roles')
@Controller('roles')
export class RolesController {
constructor(private readonly rolesService: RolesService) {}
@Get()
@Roles('admin')
@ApiOperation({ summary: 'Get all roles' })
@ApiResponse({ status: 200, description: 'Return all roles.' })
async findAll() {
const data = await this.rolesService.findAll();
return { message: 'Roles fetched successfully', data };
}
@Get(':id')
@Roles('admin')
@ApiOperation({ summary: 'Get a role by ID' })
@ApiResponse({ status: 200, description: 'Return the role.' })
@ApiResponse({ status: 404, description: 'Role not found.' })
async findOne(@Param('id') id: string) {
const data = await this.rolesService.findOne(+id);
return { message: 'Role fetched successfully', data };
}
@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create a new role' })
@ApiResponse({ status: 201, description: 'Role created successfully.' })
async create(@Body() createRoleDto: CreateRoleDto) {
const data = await this.rolesService.create(createRoleDto);
return { message: 'Role created successfully', data };
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update a role' })
@ApiResponse({ status: 200, description: 'Role updated successfully.' })
@ApiResponse({ status: 404, description: 'Role not found.' })
async update(@Param('id') id: string, @Body() updateRoleDto: UpdateRoleDto) {
const data = await this.rolesService.update(+id, updateRoleDto);
return { message: 'Role updated successfully', data };
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: 'Delete a role' })
@ApiResponse({ status: 200, description: 'Role deleted successfully.' })
@ApiResponse({ status: 404, description: 'Role not found.' })
async remove(@Param('id') id: string) {
return await this.rolesService.remove(+id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { RolesController } from './roles.controller';
import { RolesService } from './roles.service';
import { DrizzleModule } from '@/database/drizzle.module';
@Module({
imports: [DrizzleModule],
controllers: [RolesController],
providers: [RolesService],
exports: [RolesService],
})
export class RolesModule {}

View File

@@ -0,0 +1,67 @@
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
import * as schema from 'src/database/index';
import { roles } from 'src/database/index';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { Role } from './entities/role.entity';
@Injectable()
export class RolesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) {}
async findAll(): Promise<Role[]> {
return await this.drizzle.select().from(roles);
}
async findOne(id: number): Promise<Role> {
const role = await this.drizzle
.select()
.from(roles)
.where(eq(roles.id, id));
if (role.length === 0) {
throw new HttpException('Role not found', HttpStatus.NOT_FOUND);
}
return role[0];
}
async create(createRoleDto: CreateRoleDto): Promise<Role> {
const [role] = await this.drizzle
.insert(roles)
.values({
name: createRoleDto.name,
})
.returning();
return role;
}
async update(id: number, updateRoleDto: UpdateRoleDto): Promise<Role> {
// Check if role exists
await this.findOne(id);
await this.drizzle
.update(roles)
.set({
name: updateRoleDto.name,
})
.where(eq(roles.id, id));
return this.findOne(id);
}
async remove(id: number): Promise<{ message: string }> {
// Check if role exists
await this.findOne(id);
await this.drizzle.delete(roles).where(eq(roles.id, id));
return { message: 'Role deleted successfully' };
}
}

View File

@@ -0,0 +1,76 @@
[
{
"id": "q-1",
"type": "simple",
"position": 0,
"question": "Pregunta N° 1 Simple",
"required": true
},
{
"id": "q-2",
"type": "multiple_choice",
"options": [
{
"id": "1",
"text": "Opcion Prueba 1"
},
{
"id": "2",
"text": "Opcion Prueba 2 "
},
{
"id": "3",
"text": "Opcion Prueba 3"
},
{
"id": "4",
"text": "Opcion Prueba 4"
}
],
"position": 1,
"question": "Pregunta de Multiples Opciones N°2 ",
"required": true
},
{
"id": "q-3",
"type": "single_choice",
"options": [
{
"id": "1",
"text": "Opcion Unica Prueba 1"
},
{
"id": "2",
"text": "Opcion Unica Prueba 2"
}
],
"position": 2,
"question": "Preguntas de una sola opcion N°3 ",
"required": true
},
{
"id": "q-4",
"type": "select",
"options": [
{
"id": "1",
"text": "Seleccion 1"
},
{
"id": "2",
"text": "Seleccion 2 "
},
{
"id": "3",
"text": "Seleccion 3"
},
{
"id": "4",
"text": "Seleccion 4"
}
],
"position": 3,
"question": "Pregunta seleccion N° 4",
"required": true
}
]

View File

@@ -0,0 +1,73 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
export class CreateSurveyDto {
@ApiProperty({ description: 'Survey title' })
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({ description: 'Survey description' })
@IsString()
@IsNotEmpty()
description: string;
@ApiProperty({ description: 'Target audience' })
@IsString()
@IsNotEmpty()
targetAudience: string;
@ApiProperty({ description: 'Closing date' })
@IsOptional()
@IsDate()
closingDate?: Date;
@ApiProperty({ description: 'Publication status' })
@IsBoolean()
published: boolean;
@ApiProperty({ description: 'Survey questions' })
@IsArray()
@ValidateNested({ each: true }) // Asegura que cada elemento sea validado individualmente
@Type(() => QuestionDto) // Evita que se envuelva en otro array
questions: any[];
}
class QuestionDto {
@IsString()
id: string;
@IsInt()
position: number;
@IsOptional()
@IsString()
question?: string;
@IsOptional()
@IsString()
content?: string;
@IsBoolean()
required: boolean;
@IsString()
type: string;
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => OptionsDto)
options?: OptionsDto[];
}
class OptionsDto {
@IsString()
id: string;
@IsString()
text: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty } from 'class-validator';
export class FindForUserDto {
@ApiProperty({ description: 'Survey rol' })
@IsArray()
@IsNotEmpty()
rol: any;
}

View File

@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsString } from 'class-validator';
export class AnswersSurveyDto {
@ApiProperty({ description: 'Survey id' })
@IsString()
@IsNotEmpty()
surveyId: string;
@ApiProperty({ description: 'Survey answers' })
@IsArray()
@IsNotEmpty()
answers: any;
}

View File

@@ -0,0 +1,63 @@
import { ApiProperty } from '@nestjs/swagger';
export class QuestionStatDto {
@ApiProperty({ description: 'Question identifier' })
questionId: string;
@ApiProperty({ description: 'Question label' })
label: string;
@ApiProperty({ description: 'Count of responses for this option' })
count: number;
}
export class SurveyDetailDto {
@ApiProperty({ description: 'Survey ID' })
id: number;
@ApiProperty({ description: 'Survey title' })
title: string;
@ApiProperty({ description: 'Survey description' })
description: string;
@ApiProperty({ description: 'Total responses received' })
totalResponses: number;
@ApiProperty({ description: 'Target audience' })
targetAudience: any;
@ApiProperty({ description: 'Creation date' })
createdAt: string;
@ApiProperty({ description: 'Closing date' })
closingDate?: string | null;
@ApiProperty({ description: 'Question statistics', type: [QuestionStatDto] })
// @ApiProperty({ description: 'Question statistics' })
questionStats: QuestionStatDto[];
// questionStats: any;
}
export class SurveyStatisticsResponseDto {
@ApiProperty({ description: 'Total number of surveys' })
totalSurveys: number;
@ApiProperty({ description: 'Total number of responses across all surveys' })
totalResponses: number;
@ApiProperty({ description: 'Completion rate percentage' })
completionRate: number;
@ApiProperty({ description: 'Surveys created by month' })
surveysByMonth: { month: string; count: number }[];
@ApiProperty({ description: 'Responses by audience type' })
responsesByAudience: { name: any; value: number }[];
@ApiProperty({ description: 'Response distribution by survey' })
responseDistribution: { title: string; responses: number }[];
@ApiProperty({ description: 'Detailed statistics for each survey', type: [SurveyDetailDto] })
surveyDetails: SurveyDetailDto[];
}

View File

@@ -0,0 +1,5 @@
import { PartialType } from '@nestjs/swagger';
import { CreateSurveyDto } from './create-survey.dto';
export class UpdateSurveyDto extends PartialType(CreateSurveyDto) {}

View File

@@ -0,0 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
export class Survey {
@ApiProperty({ description: 'Survey ID' })
id: number;
@ApiProperty({ description: 'Survey title' })
title: string;
@ApiProperty({ description: 'Survey description' })
description: string;
@ApiProperty({ description: 'Target audience for the survey' })
targetAudience: string;
@ApiProperty({ description: 'Survey closing date' })
closingDate?: Date;
@ApiProperty({ description: 'Survey publication status' })
published: boolean;
@ApiProperty({ description: 'Survey questions' })
questions: any[];
@ApiProperty({ description: 'Creation date' })
created_at?: Date;
@ApiProperty({ description: 'Last update date' })
updated_at?: Date;
}

View File

@@ -0,0 +1,125 @@
import { Roles } from '@/common/decorators/roles.decorator';
import { PaginationDto } from '@/common/dto/pagination.dto';
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
Req,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateSurveyDto } from './dto/create-survey.dto';
import { UpdateSurveyDto } from './dto/update-survey.dto';
import { SurveysService } from './surveys.service';
import { AnswersSurveyDto } from './dto/response-survey.dto';
import { Request } from 'express';
import { FindForUserDto } from './dto/find-for-user.dto';
import { SurveyStatisticsResponseDto } from './dto/statistics-response.dto';
@ApiTags('surveys')
@Controller('surveys')
export class SurveysController {
constructor(private readonly surveysService: SurveysService) {}
@Get()
@Roles('admin')
@ApiOperation({ summary: 'Get all surveys with pagination and filters' })
@ApiResponse({ status: 200, description: 'Return paginated surveys.' })
async findAll(@Query() paginationDto: PaginationDto) {
const result = await this.surveysService.findAll(paginationDto);
return {
message: 'Surveys fetched successfully',
data: result.data,
meta: result.meta,
};
}
@Post('for-user')
@ApiOperation({ summary: 'Get all surveys with pagination and filters for user' })
@ApiResponse({ status: 200, description: 'Return paginated surveys for user.' })
async findAllForUser(@Req() req: Request, @Query() paginationDto: PaginationDto, @Body() findForUserDto: FindForUserDto) {
const userId = req['user'].id;
const result = await this.surveysService.findAllForUser(paginationDto, userId, findForUserDto);
return {
message: 'Surveys fetched successfully',
data: result.data,
meta: result.meta,
};
}
@Get('statistics')
@Roles('admin', 'superadmin', 'autoridad')
@ApiOperation({ summary: 'Get survey statistics' })
@ApiResponse({ status: 200, description: 'Return survey statistics.'})
async getStatistics() {
const data = await this.surveysService.getStatistics();
return {
message: 'Survey statistics fetched successfully',
data
};
}
@Get(':id')
@ApiOperation({ summary: 'Get a survey by ID' })
@ApiResponse({ status: 200, description: 'Return the survey.' })
@ApiResponse({ status: 404, description: 'Survey not found.' })
async findOne(@Param('id') id: string) {
const data = await this.surveysService.findOne(id);
return { message: 'Survey fetched successfully', data };
}
@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create a new survey' })
@ApiResponse({ status: 201, description: 'Survey created successfully.' })
async create(@Body() createSurveyDto: CreateSurveyDto) {
console.log(createSurveyDto);
const data = await this.surveysService.create(createSurveyDto);
return { message: 'Survey created successfully', data };
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update a survey' })
@ApiResponse({ status: 200, description: 'Survey updated successfully.' })
@ApiResponse({ status: 404, description: 'Survey not found.' })
async update(
@Param('id') id: string,
@Body() updateSurveyDto: UpdateSurveyDto,
) {
const data = await this.surveysService.update(id, updateSurveyDto);
return { message: 'Survey updated successfully', data };
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: 'Delete a survey' })
@ApiResponse({ status: 200, description: 'Survey deleted successfully.' })
@ApiResponse({ status: 404, description: 'Survey not found.' })
async remove(@Param('id') id: string) {
return await this.surveysService.remove(id);
}
@Post('answers')
@ApiOperation({ summary: 'Create a new answers' })
@ApiResponse({ status: 201, description: 'Survey answers successfully.' })
async answers(@Req() req: Request, @Body() answersSurveyDto: AnswersSurveyDto) {
const userId = (req as any).user?.id;
const data = await this.surveysService.answers(Number(userId),answersSurveyDto);
return { message: 'Survey answers created successfully', data };
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SurveysService } from './surveys.service';
import { SurveysController } from './surveys.controller';
@Module({
controllers: [SurveysController],
providers: [SurveysService],
exports: [SurveysService],
})
export class SurveysModule {}

View File

@@ -0,0 +1,535 @@
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import { surveys, answersSurveys, viewSurveys } from '@/database/index';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '@/database/index';
import { CreateSurveyDto } from './dto/create-survey.dto';
import { UpdateSurveyDto } from './dto/update-survey.dto';
import { and, count, eq, ilike, isNull, or, sql } from 'drizzle-orm';
import { SurveyDetailDto, SurveyStatisticsResponseDto } from './dto/statistics-response.dto';
import { PaginationDto } from '@/common/dto/pagination.dto';
import { AnswersSurveyDto } from './dto/response-survey.dto';
import { FindForUserDto } from './dto/find-for-user.dto';
@Injectable()
export class SurveysService {
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;
// Build search condition
const searchCondition = ilike(surveys.title, `%${search}%`);
// Build sort condition
const orderBy = sortOrder === 'asc'
? sql`${surveys[sortBy as keyof typeof surveys]} asc`
: sql`${surveys[sortBy as keyof typeof surveys]} desc`;
// Get total count for pagination
const totalCountResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(surveys)
.where(searchCondition);
const totalCount = Number(totalCountResult[0].count);
const totalPages = Math.ceil(totalCount / limit);
// Get paginated data
const data = await this.drizzle
.select()
.from(surveys)
.where(searchCondition)
.orderBy(orderBy)
.limit(limit)
.offset(offset);
const dataSurvey = data.map((survey) => {
return {
...survey,
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
};
});
// Build pagination metadata
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: dataSurvey, meta };
}
async findAllForUser(paginationDto: PaginationDto, userId: number, findForUserDto: FindForUserDto) {
const { page = 1, limit = 10, search = '', sortBy = 'created_at', sortOrder = 'asc' } = paginationDto || {};
const offset = (page - 1) * limit;
let searchCondition : any = false
// Build search condition
// if (findForUserDto.rol[0].rol === 'superadmin' || findForUserDto.rol[0].rol == 'admin') {
// searchCondition = and(
// or(eq(viewSurveys.targetAudience, 'producers'), eq(viewSurveys.targetAudience, 'organization'), eq(viewSurveys.targetAudience, 'all')),
// or(eq(viewSurveys.user_id, userId), isNull(viewSurveys.user_id))
// );
// } else {
// searchCondition = and(
// or(eq(viewSurveys.targetAudience, findForUserDto.rol[0].rol), eq(viewSurveys.targetAudience, 'all')),
// or(eq(viewSurveys.user_id, userId), isNull(viewSurveys.user_id))
// );
// }
if (findForUserDto.rol[0].rol !== 'superadmin' && findForUserDto.rol[0].rol !== 'admin') {
searchCondition = or(eq(surveys.targetAudience, findForUserDto.rol[0].rol), eq(surveys.targetAudience, 'all'))
}
// console.log(searchCondition);
// Build sort condition
const orderBy = sortOrder === 'asc'
? sql`${surveys[sortBy as keyof typeof surveys]} asc`
: sql`${surveys[sortBy as keyof typeof surveys]} desc`;
// Get total count for pagination
const totalCountResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(surveys)
.leftJoin(answersSurveys, and(eq(answersSurveys.surveyId,surveys.id),eq(answersSurveys.userId,userId)))
.where(searchCondition);
const totalCount = Number(totalCountResult[0].count);
const totalPages = Math.ceil(totalCount / limit);
// Get paginated data
const data = await this.drizzle
.select()
.from(surveys)
.leftJoin(answersSurveys, and(eq(answersSurveys.surveyId,surveys.id),eq(answersSurveys.userId,userId)))
.where(searchCondition)
.orderBy(orderBy)
.limit(limit)
.offset(offset);
// Build pagination metadata
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: string) {
const survey = await this.drizzle
.select()
.from(surveys)
.where(eq(surveys.id, parseInt(id)));
if (survey.length === 0) {
throw new HttpException('Survey not found', HttpStatus.NOT_FOUND);
}
const dataSurvey = survey.map((survey) => {
return {
...survey,
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
};
});
return dataSurvey[0];
}
async findByTitle(title: string) {
return await this.drizzle
.select()
.from(surveys)
.where(eq(surveys.title, title));
}
async create(createSurveyDto: CreateSurveyDto) {
const find = await this.findByTitle(createSurveyDto.title);
if (find.length !== 0) {
throw new HttpException('Survey already exists', HttpStatus.BAD_REQUEST);
}
const survey = await this.drizzle
.insert(surveys)
.values({
...createSurveyDto,
closingDate: createSurveyDto.closingDate?.toISOString(),
})
.returning();
const dataSurvey = survey.map((survey) => {
return {
...survey,
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
};
});
return dataSurvey[0];
}
async update(id: string, updateSurveyDto: UpdateSurveyDto) {
const find = await this.findOne(id)
if (!find) {
throw new HttpException('Survey not found', HttpStatus.BAD_REQUEST)
}
const find2 = await this.findByTitle(updateSurveyDto.title ?? '');
if (find2.length !== 0 && find2[0].id !== parseInt(id)) {
throw new HttpException('Survey already exists', HttpStatus.BAD_REQUEST);
}
const survey = await this.drizzle
.update(surveys)
.set({
...updateSurveyDto,
closingDate: updateSurveyDto.closingDate?.toISOString(),
updated_at: new Date(),
})
.where(eq(surveys.id, parseInt(id)))
.returning();
const dataSurvey = survey.map((survey) => {
return {
...survey,
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
};
});
return dataSurvey[0];
}
async remove(id: string) {
const find = await this.findOne(id);
if (!find) {
throw new HttpException('Survey not found', HttpStatus.BAD_REQUEST);
}
await this.drizzle
.delete(surveys)
.where(eq(surveys.id, parseInt(id)));
return { message: 'Survey deleted successfully' };
}
async answers(userId: number, answersSurveyDto: AnswersSurveyDto) {
const find = await this.drizzle.select()
.from(answersSurveys)
.where(and(eq(answersSurveys.surveyId, Number(answersSurveyDto.surveyId)), (eq(answersSurveys.userId, userId))));
if (find.length !== 0) {
throw new HttpException('Survey answers already exists', HttpStatus.BAD_REQUEST);
}
const survey = await this.drizzle
.insert(answersSurveys)
.values({
...answersSurveyDto,
surveyId: Number(answersSurveyDto.surveyId),
userId: userId,
})
.returning();
return survey[0];
}
async getStatistics(): Promise<SurveyStatisticsResponseDto> {
// Obtener el número total de encuestas
const totalSurveys = await this.getTotalSurveysCount();
// Obtener el número total de respuestas
const totalResponses = await this.getTotalResponsesCount();
// Calcular la tasa de finalización
const completionRate = totalSurveys > 0 ? Math.round((totalResponses / totalSurveys) * 100) : 0;
// Obtener las encuestas por mes
const surveysByMonth = await this.getSurveysByMonth();
// Obtener las respuestas por audiencia
const responsesByAudience = await this.getResponsesByAudience();
// Obtener la distribución de respuestas por encuesta
const responseDistribution = await this.getResponseDistribution();
// Obtener las estadísticas detalladas de las encuestas
const surveyDetails = await this.getSurveyDetails();
return {
totalSurveys,
totalResponses,
completionRate,
surveysByMonth,
responsesByAudience,
responseDistribution,
surveyDetails,
}
}
private async getTotalSurveysCount(): Promise<number> {
const result = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(surveys);
return Number(result[0].count);
}
private async getTotalResponsesCount(): Promise<number> {
const result = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(answersSurveys);
return Number(result[0].count);
}
private async getSurveysByMonth(): Promise<{ month: string; count: number }[]> {
const result = await this.drizzle
.select({
month: sql<string>`to_char(created_at, 'YYYY-MM')`,
count: sql<number>`count(*)`,
})
.from(surveys)
.groupBy(sql`to_char(created_at, 'YYYY-MM')`)
.orderBy(sql`to_char(created_at, 'YYYY-MM')`);
return result.map(item => ({ month: item.month, count: Number(item.count) }));
}
private async getResponsesByAudience(): Promise<{ name: any; value: number }[]> {
const result = await this.drizzle
.select({
audience: surveys.targetAudience,
count: sql<number>`count(*)`,
})
.from(answersSurveys)
.leftJoin(surveys, eq(answersSurveys.surveyId, surveys.id))
.groupBy(surveys.targetAudience);
return result.map(item =>
{
let audience = 'Sin definir'
if (item.audience == 'all') {
audience = 'General'
} else if (item.audience == 'organization') {
audience = 'Organización'
} else if (item.audience == 'producers') {
audience = 'Productores'
}
return ({ name: audience, value: Number(item.count) })
}
);
}
private async getResponseDistribution(): Promise<{ title: string; responses: number }[]> {
const result = await this.drizzle
.select({
id: surveys.id,
title: surveys.title,
responses: sql<number>`count(${answersSurveys.id})`,
})
.from(surveys)
.leftJoin(answersSurveys, eq(surveys.id, answersSurveys.surveyId))
.groupBy(surveys.id, surveys.title)
.orderBy(sql`count(${answersSurveys.id}) desc`)
.limit(10);
return result.map(item => ({ title: item.title, responses: Number(item.responses) }));
}
private async getSurveyDetails(): Promise<SurveyDetailDto[]> {
const allSurveys = await this.drizzle
.select({
id: surveys.id,
title: surveys.title,
description: surveys.description,
targetAudience: surveys.targetAudience,
createdAt: surveys.created_at,
closingDate: surveys.closingDate,
questions: surveys.questions,
})
.from(surveys);
return await Promise.all(
allSurveys.map(async (survey) => {
// Obtener el número total de respuestas para esta encuesta
const totalSurveyResponses = await this.getTotalSurveyResponses(survey.id);
// Obtener todas las respuestas para esta encuesta
const answersResult = await this.drizzle
.select({ answers: answersSurveys.answers })
.from(answersSurveys)
.where(eq(answersSurveys.surveyId, survey.id));
let audience = 'Sin definir'
if (survey.targetAudience == 'all') {
audience = 'General'
} else if (survey.targetAudience == 'organization') {
audience = 'Organización'
} else if (survey.targetAudience == 'producers') {
audience = 'Productores'
}
// Procesar las estadísticas de las preguntas
const questionStats = this.processQuestionStats(survey.questions as any[], answersResult);
return {
id: survey.id,
title: survey.title,
description: survey.description,
totalResponses: totalSurveyResponses,
// targetAudience: survey.targetAudience,
targetAudience: audience,
createdAt: survey.createdAt.toISOString(),
closingDate: survey.closingDate ? new Date(survey.closingDate).toISOString() : undefined,
questionStats,
};
})
);
}
private async getTotalSurveyResponses(surveyId: number): Promise<number> {
const result = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(answersSurveys)
.where(eq(answersSurveys.surveyId, surveyId));
return Number(result[0].count);
}
// ==================================
private processQuestionStats(questions: any[], answersResult: { answers: any }[]) {
// Initialize counters for each question option
const questionStats: Array<{ questionId: string; label: string; count: number }> = [];
// Skip title questions (type: 'title')
const surveyQuestions = questions.filter(q => q.type !== 'title');
// console.log(surveyQuestions);
// console.log('Se llamo a processQuestionStats()');
for (const question of surveyQuestions) {
// console.log('Bucle1 se ejecuto');
// For single choice, multiple choice, and select questions
// if (['single_choice', 'multiple_choice', 'select'].includes(question.type)) {
const optionCounts: Record<string, number> = {};
// // Initialize counts for each option
// for (const option of question.options) {
// optionCounts[option.text] = 0;
// }
// // Count responses for each option
// for (const answerObj of answersResult) {
// const answer = answerObj.answers.find(a => a.questionId === question.id);
// if (answer) {
// if (Array.isArray(answer.value)) {
// // For multiple choice questions
// for (const value of answer.value) {
// if (optionCounts[value] !== undefined) {
// optionCounts[value]++;
// }
// }
// } else {
// // For single choice questions
// if (optionCounts[answer.value] !== undefined) {
// optionCounts[answer.value]++;
// }
// }
// }
// }
// // Convert to the required format
// for (const option of question.options) {
// questionStats.push({
// questionId: String(question.id),
// label: option.text,
// count: optionCounts[option.value] || 0,
// });
// }
if (question.type == 'multiple_choice') {
for (const option of question.options) {
console.log(option);
let count :number = 0
// Count responses for each option
for (const obj of answersResult) {
console.log(obj.answers)
const resp = obj.answers.find(a => a.questionId == question.id)
const respArray = resp.value.split(",")
// console.log();
if (respArray[option.id] == 'true') {
count++
}
}
optionCounts[option.text] = count
// Convert to the required format
questionStats.push({
questionId: String(question.id),
label: `${question.question} ${option.text}`,
count: optionCounts[option.text] || 0,
})
}
} else if (question.type == 'single_choice' || question.type == 'select') {
for (const option of question.options) {
let count :number = 0
// Count responses for each option
for (const obj of answersResult) {
const resp = obj.answers.find(a => a.questionId == question.id)
if (resp.value == option.id) {
count++
}
}
optionCounts[option.text] = count
// Convert to the required format
questionStats.push({
questionId: String(question.id),
label: `${question.question} ${option.text}`,
count: optionCounts[option.text] || 0,
})
}
} else if (question.type === 'simple') {
// For simple text questions, just count how many responses
let responseCount = 0;
for (const answerObj of answersResult) {
const answer = answerObj.answers.find(a => a.questionId === question.id);
if (answer && answer.value) {
responseCount++;
}
}
questionStats.push({
questionId: String(question.id),
label: question.question,
count: responseCount,
});
}
}
return questionStats;
}
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class AssignRoleDto {
@ApiProperty()
@IsNumber()
userId: number;
@ApiProperty()
@IsNumber()
roleId: number;
}

View File

@@ -0,0 +1,46 @@
import { Roles } from '@/common/decorators/roles.decorator';
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AssignRoleDto } from './dto/assign-role.dto';
import { UserRolesService } from './user-roles.service';
@ApiTags('user-roles')
@Controller('user-roles')
export class UserRolesController {
constructor(private readonly userRolesService: UserRolesService) {}
@Get('user/:userId')
@Roles('admin')
@ApiOperation({ summary: 'Get roles by user ID' })
@ApiResponse({ status: 200, description: 'Return roles for the user.' })
@ApiResponse({ status: 404, description: 'User not found.' })
async getRolesByUserId(@Param('userId') userId: string) {
const data = await this.userRolesService.getRolesByUserId(+userId);
return { message: 'Roles fetched successfully', data };
}
@Post('assign')
@Roles('admin')
@ApiOperation({ summary: 'Assign a role to a user' })
@ApiResponse({ status: 200, description: 'Role assigned successfully.' })
@ApiResponse({ status: 404, description: 'User or role not found.' })
async assignRoleToUser(@Body() assignRoleDto: AssignRoleDto) {
const data = await this.userRolesService.assignRoleToUser(assignRoleDto);
return { message: 'Role assigned successfully', data };
}
@Delete('user/:userId/role/:roleId')
@Roles('admin')
@ApiOperation({ summary: 'Remove a role from a user' })
@ApiResponse({ status: 200, description: 'Role removed successfully.' })
@ApiResponse({
status: 404,
description: 'User-role relationship not found.',
})
async removeRoleFromUser(
@Param('userId') userId: string,
@Param('roleId') roleId: string,
) {
return await this.userRolesService.removeRoleFromUser(+userId, +roleId);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UserRolesController } from './user-roles.controller';
import { UserRolesService } from './user-roles.service';
import { DrizzleModule } from '@/database/drizzle.module';
@Module({
imports: [DrizzleModule],
controllers: [UserRolesController],
providers: [UserRolesService],
exports: [UserRolesService],
})
export class UserRolesModule {}

View File

@@ -0,0 +1,118 @@
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
import * as schema from 'src/database/index';
import { users, roles, usersRole } from 'src/database/index';
import { AssignRoleDto } from './dto/assign-role.dto';
@Injectable()
export class UserRolesService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) {}
async getRolesByUserId(userId: number) {
// Check if user exists
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.id, userId));
if (user.length === 0) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
// Get roles for the user
const userRoles = await this.drizzle
.select({
id: roles.id,
name: roles.name,
})
.from(usersRole)
.leftJoin(roles, eq(roles.id, usersRole.roleId))
.where(eq(usersRole.userId, userId));
return userRoles;
}
async assignRoleToUser(assignRoleDto: AssignRoleDto) {
const { userId, roleId } = assignRoleDto;
// Check if user exists
const user = await this.drizzle
.select()
.from(users)
.where(eq(users.id, userId));
if (user.length === 0) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
// Check if role exists
const role = await this.drizzle
.select()
.from(roles)
.where(eq(roles.id, roleId));
if (role.length === 0) {
throw new HttpException('Role not found', HttpStatus.NOT_FOUND);
}
// Check if the user already has this role
const existingUserRole = await this.drizzle
.select()
.from(usersRole)
.where(
and(
eq(usersRole.userId, userId),
eq(usersRole.roleId, roleId),
),
);
if (existingUserRole.length > 0) {
throw new HttpException('User already has this role', HttpStatus.BAD_REQUEST);
}
// Assign role to user
await this.drizzle.insert(usersRole).values({
userId,
roleId,
});
// Return the updated roles
return this.getRolesByUserId(userId);
}
async removeRoleFromUser(userId: number, roleId: number) {
// Check if the user-role relationship exists
const userRole = await this.drizzle
.select()
.from(usersRole)
.where(
and(
eq(usersRole.userId, userId),
eq(usersRole.roleId, roleId),
),
);
if (userRole.length === 0) {
throw new HttpException(
'User-role relationship not found',
HttpStatus.NOT_FOUND,
);
}
// Remove the role from the user
await this.drizzle
.delete(usersRole)
.where(
and(
eq(usersRole.userId, userId),
eq(usersRole.roleId, roleId),
),
);
return { message: 'Role removed from user successfully' };
}
}

View File

@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator';
export class CreateUserDto {
@ApiProperty()
@IsString()
username: string;
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsString()
fullname: string;
@ApiProperty()
@IsString({
message: 'Phone must be a string',
})
@IsOptional()
phone: string;
@ApiProperty()
@IsString({
message: 'Password must be a string',
})
password: string;
@ApiProperty()
@IsInt()
role: number;
}

View File

@@ -0,0 +1,40 @@
import { ApiProperty, PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
// import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class UpdateUserDto extends PartialType(CreateUserDto) {
// export class UpdateUserDto {
@IsOptional()
username: string;
@IsOptional()
email: string;
@IsOptional()
fullname: string;
@IsOptional()
phone: string;
@IsOptional()
password: string;
@ApiProperty()
@IsString()
@IsOptional()
isActive: string;
@IsOptional()
state: string | number | null;
@IsOptional()
municipality: string | number | null;
@IsOptional()
parish: string | number | null;
@IsOptional()
role: number;
}

View File

@@ -0,0 +1,25 @@
export class User {
id?: number;
username!: string;
email!: string;
fullname!: string;
phone?: string | null;
password?: string;
isTwoFactorEnabled?: boolean;
twoFactorSecret?: string | null;
isEmailVerified?: boolean;
isActive!: boolean;
created_at?: Date | null;
updated_at?: Date | null;
state?: string | number | null;
municipality?: string | number | null;
parish?: string | number | null;
}
export class UserRole {
id!: number;
userId!: number;
roleId!: number;
created_at?: Date | null;
updated_at?: Date | null;
}

View File

@@ -0,0 +1,81 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Roles } from '../../common/decorators/roles.decorator';
import { PaginationDto } from '../../common/dto/pagination.dto';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@Roles('admin')
@ApiOperation({ summary: 'Get all users with pagination and filters' })
@ApiResponse({ status: 200, description: 'Return paginated users.' })
async findAll(@Query() paginationDto: PaginationDto) {
const result = await this.usersService.findAll(paginationDto);
return {
message: 'Users fetched successfully',
data: result.data,
meta: result.meta
};
}
@Get(':id')
// @Roles('admin')
@ApiOperation({ summary: 'Get a user by ID' })
@ApiResponse({ status: 200, description: 'Return the user.' })
@ApiResponse({ status: 404, description: 'User not found.' })
async findOne(@Param('id') id: string) {
const data = await this.usersService.findOne(id);
return { message: 'User fetched successfully', data };
}
@Post()
@Roles('admin')
@ApiOperation({ summary: 'Create a new user' })
@ApiResponse({ status: 201, description: 'User created successfully.' })
async create(
@Body() createUserDto: CreateUserDto,
@Query('roleId') roleId?: string,
) {
const data = await this.usersService.create(
createUserDto,
roleId ? parseInt(roleId) : undefined,
);
return { message: 'User created successfully', data };
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: 'Update a user' })
@ApiResponse({ status: 200, description: 'User updated successfully.' })
@ApiResponse({ status: 404, description: 'User not found.' })
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
const data = await this.usersService.update(id, updateUserDto);
return { message: 'User updated successfully', data };
}
@Patch('profile/:id')
// @Roles('admin')
@ApiOperation({ summary: 'Update a user' })
@ApiResponse({ status: 200, description: 'User updated successfully.' })
@ApiResponse({ status: 400, description: 'email already exists.' })
@ApiResponse({ status: 404, description: 'User not found.' })
async updateProfile(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
const data = await this.usersService.updateProfile(id, updateUserDto);
return { message: 'User updated successfully', data };
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: 'Delete a user' })
@ApiResponse({ status: 200, description: 'User deleted successfully.' })
@ApiResponse({ status: 404, description: 'User not found.' })
async remove(@Param('id') id: string) {
return await this.usersService.remove(id);
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { DrizzleModule } from '@/database/drizzle.module';
@Module({
imports: [DrizzleModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

View File

@@ -0,0 +1,288 @@
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
import { Env, validateString } from '@/common/utils';
import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { users, roles, usersRole } from 'src/database/index';
import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm';
import * as bcrypt from 'bcryptjs';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
import { PaginationDto } from '../../common/dto/pagination.dto';
@Injectable()
export class UsersService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
) { }
async findAll(paginationDto?: PaginationDto): Promise<{ data: User[], meta: any }> {
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
// Calculate offset
const offset = (page - 1) * limit;
// Build search condition
let searchCondition: SQL<unknown> | undefined;
if (search) {
searchCondition = or(
like(users.username, `%${search}%`),
like(users.email, `%${search}%`),
like(users.fullname, `%${search}%`)
);
}
// Build sort condition
const orderBy = sortOrder === 'asc'
? sql`${users[sortBy as keyof typeof users]} asc`
: sql`${users[sortBy as keyof typeof users]} desc`;
// Get total count for pagination
const totalCountResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(users)
.where(searchCondition);
const totalCount = Number(totalCountResult[0].count);
const totalPages = Math.ceil(totalCount / limit);
// Get paginated data
const data = await this.drizzle
.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(searchCondition)
.orderBy(orderBy)
.limit(limit)
.offset(offset);
// Build pagination metadata
const meta = {
page,
limit,
totalCount,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
nextPage: page < totalPages ? page + 1 : null,
previousPage: page > 1 ? page - 1 : null,
};
// console.log(data);
return { data, meta };
}
async findOne(id: string): Promise<User> {
const find = await this.drizzle
.select({
id: users.id,
username: users.username,
email: users.email,
fullname: users.fullname,
phone: users.phone,
isActive: users.isActive,
role: roles.name,
state: schema.states.name,
municipality: schema.municipalities.name,
parish: schema.parishes.name
})
.from(users)
.leftJoin(usersRole, eq(usersRole.userId, users.id))
.leftJoin(roles, eq(roles.id, usersRole.roleId))
.leftJoin(schema.states, eq(schema.states.id, users.state))
.leftJoin(schema.municipalities, eq(schema.municipalities.id, users.municipality))
.leftJoin(schema.parishes, eq(schema.parishes.id, users.parish))
.where(eq(users.id, parseInt(id)));
if (find.length === 0) {
throw new HttpException('User does not exist', HttpStatus.BAD_REQUEST);
}
return find[0];
}
// Rest of the service remains the same
async create(
createUserDto: CreateUserDto,
roleId: number = 2,
): Promise<User> {
// Hash the password
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
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);
}
}
// 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,
phone: createUserDto.phone,
isEmailVerified: false,
isTwoFactorEnabled: false,
})
.returning();
// Assign role to user
await tx.insert(usersRole).values({
userId: newUser.id,
roleId: roleId,
});
// 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;
});
}
async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
const userId = parseInt(id);
// Check if user exists
await this.findOne(id);
// Prepare update data
const updateData: any = {};
if (updateUserDto.username) updateData.username = updateUserDto.username;
if (updateUserDto.email) updateData.email = updateUserDto.email;
if (updateUserDto.fullname) updateData.fullname = updateUserDto.fullname;
if (updateUserDto.password) {
updateData.password = await bcrypt.hash(updateUserDto.password, 10);
}
if (updateUserDto.phone) updateData.phone = updateUserDto.phone;
if (updateUserDto.isActive) updateData.isActive = updateUserDto.isActive;
const updateDataRole: any = {};
if (updateUserDto.role) updateDataRole.roleId = updateUserDto.role;
// Update user
await this.drizzle
.update(users)
.set(updateData)
.where(eq(users.id, userId));
await this.drizzle
.update(usersRole)
.set(updateDataRole)
.where(eq(usersRole.userId, userId));
// Return updated user
return this.findOne(id);
}
async updateProfile(id: string, updateUserDto: UpdateUserDto): Promise<User> {
// throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
const userId = parseInt(id);
// Check if user exists
await this.findOne(id);
const data = await this.drizzle
.select({
id: users.id,
email: users.email
})
.from(users)
.where(and(
not(eq(users.id, userId)),
eq(users.email, updateUserDto.email)
)
)
if (data.length > 0) {
if (data[0].email === updateUserDto.email) {
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
}
}
// Prepare update data
const updateData: any = {};
// if (updateUserDto.username) updateData.username = updateUserDto.username;
if (updateUserDto.email) updateData.email = updateUserDto.email;
if (updateUserDto.fullname) updateData.fullname = updateUserDto.fullname;
if (updateUserDto.password) {
updateData.password = await bcrypt.hash(updateUserDto.password, 10);
}
if (updateUserDto.phone) updateData.phone = updateUserDto.phone;
if (updateUserDto.isActive) updateData.isActive = updateUserDto.isActive;
if (updateUserDto.state) {
updateData.state = updateUserDto.state;
updateData.municipality = updateUserDto.municipality
updateData.parish = updateUserDto.parish
}
// Update user
await this.drizzle
.update(users)
.set(updateData)
.where(eq(users.id, userId));
// Return updated user
return this.findOne(id);
}
async remove(id: string): Promise<{ message: string, data: User }> {
const userId = parseInt(id);
// Check if user exists
const user = await this.findOne(id);
// Delete user (this will cascade delete related records due to foreign key constraints)
// await this.drizzle.delete(users).where(eq(users.id, userId));
await this.drizzle.update(users).set({ isActive: false }).where(eq(users.id, userId));
return { message: 'User deleted successfully', data: user };
}
}