Commit 65bf6dc7 by Augusto

Database All updated

parent 1941a82d
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"prisma:seed": "ts-node prisma/seed.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^2.0.2", "@nestjs-modules/mailer": "^2.0.2",
...@@ -89,5 +90,8 @@ ...@@ -89,5 +90,8 @@
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
} }
} }
/*
Warnings:
- You are about to drop the column `comments` on the `Candidate` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Candidate" DROP COLUMN "comments";
-- CreateTable
CREATE TABLE "Comment" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"candidateId" INTEGER NOT NULL,
"createdById" INTEGER,
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Comment_candidateId_idx" ON "Comment"("candidateId");
-- CreateIndex
CREATE INDEX "Comment_createdById_idx" ON "Comment"("createdById");
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_candidateId_fkey" FOREIGN KEY ("candidateId") REFERENCES "Candidate"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
/*
Warnings:
- You are about to drop the column `siteId` on the `Candidate` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "Candidate" DROP CONSTRAINT "Candidate_siteId_fkey";
-- DropIndex
DROP INDEX "Candidate_siteId_candidateCode_key";
-- DropIndex
DROP INDEX "Candidate_siteId_idx";
-- AlterTable
ALTER TABLE "Candidate" DROP COLUMN "siteId";
-- CreateTable
CREATE TABLE "CandidateSite" (
"id" SERIAL NOT NULL,
"candidateId" INTEGER NOT NULL,
"siteId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CandidateSite_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "CandidateSite_candidateId_siteId_key" ON "CandidateSite"("candidateId", "siteId");
-- CreateIndex
CREATE INDEX "CandidateSite_candidateId_idx" ON "CandidateSite"("candidateId");
-- CreateIndex
CREATE INDEX "CandidateSite_siteId_idx" ON "CandidateSite"("siteId");
-- Migrate existing data
INSERT INTO "CandidateSite" ("candidateId", "siteId", "createdAt", "updatedAt")
SELECT id, "siteId", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM "Candidate"
WHERE "siteId" IS NOT NULL;
-- AddForeignKey
ALTER TABLE "CandidateSite" ADD CONSTRAINT "CandidateSite_candidateId_fkey" FOREIGN KEY ("candidateId") REFERENCES "Candidate"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CandidateSite" ADD CONSTRAINT "CandidateSite_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES "Site"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Remove old relationship
ALTER TABLE "Candidate" DROP CONSTRAINT IF EXISTS "Candidate_siteId_fkey";
ALTER TABLE "Candidate" DROP COLUMN "siteId";
...@@ -33,6 +33,7 @@ model User { ...@@ -33,6 +33,7 @@ model User {
refreshTokens RefreshToken[] refreshTokens RefreshToken[]
resetToken String? // For password reset resetToken String? // For password reset
resetTokenExpiry DateTime? // Expiry time for reset token resetTokenExpiry DateTime? // Expiry time for reset token
Comment Comment[]
@@index([email]) @@index([email])
@@index([role]) @@index([role])
...@@ -51,44 +52,69 @@ model RefreshToken { ...@@ -51,44 +52,69 @@ model RefreshToken {
} }
model Site { model Site {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
siteCode String @unique siteCode String @unique
siteName String siteName String
latitude Float latitude Float
longitude Float longitude Float
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
candidates Candidate[] candidates CandidateSite[]
createdBy User? @relation("SiteCreator", fields: [createdById], references: [id]) createdBy User? @relation("SiteCreator", fields: [createdById], references: [id])
createdById Int? createdById Int?
updatedBy User? @relation("SiteUpdater", fields: [updatedById], references: [id]) updatedBy User? @relation("SiteUpdater", fields: [updatedById], references: [id])
updatedById Int? updatedById Int?
@@index([siteCode]) @@index([siteCode])
} }
model Candidate { model Candidate {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
candidateCode String candidateCode String
latitude Float latitude Float
longitude Float longitude Float
type String type String
address String address String
comments String?
currentStatus String currentStatus String
onGoing Boolean @default(false) onGoing Boolean @default(false)
site Site @relation(fields: [siteId], references: [id]) sites CandidateSite[]
siteId Int createdAt DateTime @default(now())
createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
updatedAt DateTime @updatedAt createdBy User? @relation("CandidateCreator", fields: [createdById], references: [id])
createdBy User? @relation("CandidateCreator", fields: [createdById], references: [id])
createdById Int? createdById Int?
updatedBy User? @relation("CandidateUpdater", fields: [updatedById], references: [id]) updatedBy User? @relation("CandidateUpdater", fields: [updatedById], references: [id])
updatedById Int? updatedById Int?
comments Comment[]
@@unique([siteId, candidateCode])
@@index([candidateCode]) @@index([candidateCode])
@@index([currentStatus]) @@index([currentStatus])
@@index([siteId])
@@index([onGoing]) @@index([onGoing])
} }
model CandidateSite {
id Int @id @default(autoincrement())
candidate Candidate @relation(fields: [candidateId], references: [id], onDelete: Cascade)
candidateId Int
site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
siteId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([candidateId, siteId])
@@index([candidateId])
@@index([siteId])
}
model Comment {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
candidate Candidate @relation(fields: [candidateId], references: [id], onDelete: Cascade)
candidateId Int
createdBy User? @relation(fields: [createdById], references: [id])
createdById Int?
@@index([candidateId])
@@index([createdById])
}
import { PrismaClient, Role } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
const hashedPassword = await bcrypt.hash('brandit123465', 10);
const superadmin = await prisma.user.upsert({
where: { email: 'augusto.fonte@brandit.pt' },
update: {
isActive: true,
role: Role.SUPERADMIN,
password: hashedPassword,
},
create: {
email: 'augusto.fonte@brandit.pt',
name: 'Augusto Fonte',
password: hashedPassword,
role: Role.SUPERADMIN,
isActive: true,
},
});
console.log('Created/Updated superadmin user:', superadmin);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
\ No newline at end of file
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Get the superadmin user
const superadmin = await prisma.user.findUnique({
where: { email: 'augusto.fonte@brandit.pt' },
});
if (!superadmin) {
console.error('Superadmin user not found. Please run the main seed.ts first.');
return;
}
// Sites data
const sitesData = [
{
siteCode: '04LO019',
siteName: 'CARRASCAL ALVIDE',
latitude: 38.7257,
longitude: -9.4229,
},
{
siteCode: '99LS120',
siteName: 'COSTA CAPARICA NORTE',
latitude: 38.6445,
longitude: -9.24,
},
{
siteCode: '06LO026',
siteName: 'MONTE ESTORIL OESTE',
latitude: 38.7042,
longitude: -9.4094,
},
{
siteCode: '047S2',
siteName: 'CASCAIS ESTORIL',
latitude: 38.7041,
longitude: -9.4078,
},
{
siteCode: '98LC129',
siteName: 'RATO',
latitude: 38.721,
longitude: -9.1531,
},
{
siteCode: '02AG024',
siteName: 'VILAMOURA FALESIA',
latitude: 37.0746,
longitude: -8.1234,
},
{
siteCode: '16RB005',
siteName: 'RENOVA2-ZIBREIRA',
latitude: 39.491,
longitude: -8.6121,
},
];
// Create sites
for (const siteData of sitesData) {
const site = await prisma.site.upsert({
where: { siteCode: siteData.siteCode },
update: {
siteName: siteData.siteName,
latitude: siteData.latitude,
longitude: siteData.longitude,
updatedById: superadmin.id,
},
create: {
siteCode: siteData.siteCode,
siteName: siteData.siteName,
latitude: siteData.latitude,
longitude: siteData.longitude,
createdById: superadmin.id,
updatedById: superadmin.id,
},
});
console.log(`Created/Updated site: ${site.siteCode} - ${site.siteName}`);
}
console.log('Sites seeding completed successfully!');
}
// Run the seeding function directly
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
\ No newline at end of file
-- Truncate all tables in the correct order due to foreign key constraints
TRUNCATE TABLE "Comment" CASCADE;
TRUNCATE TABLE "Candidate" CASCADE;
TRUNCATE TABLE "Site" CASCADE;
TRUNCATE TABLE "RefreshToken" CASCADE;
TRUNCATE TABLE "User" CASCADE;
-- Reset the sequences
ALTER SEQUENCE "User_id_seq" RESTART WITH 1;
ALTER SEQUENCE "RefreshToken_id_seq" RESTART WITH 1;
ALTER SEQUENCE "Site_id_seq" RESTART WITH 1;
ALTER SEQUENCE "Candidate_id_seq" RESTART WITH 1;
ALTER SEQUENCE "Comment_id_seq" RESTART WITH 1;
\ No newline at end of file
#!/bin/bash
# Execute the truncate SQL script
psql -d cellnex -f prisma/truncate.sql
# Seed script removed as it's only for creating a superadmin
\ No newline at end of file
...@@ -8,6 +8,7 @@ import { UsersModule } from './modules/users/users.module'; ...@@ -8,6 +8,7 @@ import { UsersModule } from './modules/users/users.module';
import { AuthModule } from './modules/auth/auth.module'; import { AuthModule } from './modules/auth/auth.module';
import { SitesModule } from './modules/sites/sites.module'; import { SitesModule } from './modules/sites/sites.module';
import { CandidatesModule } from './modules/candidates/candidates.module'; import { CandidatesModule } from './modules/candidates/candidates.module';
import { CommentsModule } from './modules/comments/comments.module';
import { MailerModule } from '@nestjs-modules/mailer'; import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path'; import { join } from 'path';
...@@ -48,6 +49,7 @@ import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'; ...@@ -48,6 +49,7 @@ import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
AuthModule, AuthModule,
SitesModule, SitesModule,
CandidatesModule, CandidatesModule,
CommentsModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [
......
...@@ -52,7 +52,7 @@ export class AuthService { ...@@ -52,7 +52,7 @@ export class AuthService {
const [accessToken, refreshToken] = await Promise.all([ const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, { this.jwtService.signAsync(payload, {
expiresIn: '15m', expiresIn: '24h',
}), }),
this.jwtService.signAsync(payload, { this.jwtService.signAsync(payload, {
expiresIn: '7d', expiresIn: '7d',
...@@ -116,7 +116,7 @@ export class AuthService { ...@@ -116,7 +116,7 @@ export class AuthService {
const [newAccessToken, newRefreshToken] = await Promise.all([ const [newAccessToken, newRefreshToken] = await Promise.all([
this.jwtService.signAsync(newPayload, { this.jwtService.signAsync(newPayload, {
expiresIn: '15m', expiresIn: '24h',
}), }),
this.jwtService.signAsync(newPayload, { this.jwtService.signAsync(newPayload, {
expiresIn: '7d', expiresIn: '7d',
......
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, Query } from '@nestjs/common'; import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { CandidatesService } from './candidates.service'; import { CandidatesService } from './candidates.service';
import { CreateCandidateDto } from './dto/create-candidate.dto'; import { CreateCandidateDto } from './dto/create-candidate.dto';
import { UpdateCandidateDto } from './dto/update-candidate.dto'; import { UpdateCandidateDto } from './dto/update-candidate.dto';
import { QueryCandidateDto } from './dto/query-candidate.dto'; import { QueryCandidateDto } from './dto/query-candidate.dto';
import { CandidateResponseDto } from './dto/candidate-response.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client';
import { User } from '../auth/decorators/user.decorator';
import { AddSitesToCandidateDto } from './dto/add-sites-to-candidate.dto';
@ApiTags('candidates') @ApiTags('candidates')
@Controller('candidates') @Controller('candidates')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth('access-token')
export class CandidatesController { export class CandidatesController {
constructor(private readonly candidatesService: CandidatesService) { } constructor(private readonly candidatesService: CandidatesService) { }
@Post() @Post()
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@ApiOperation({ summary: 'Create a new candidate' }) @ApiOperation({ summary: 'Create a new candidate' })
@ApiResponse({ status: 201, description: 'The candidate has been successfully created.' }) @ApiResponse({ status: 201, description: 'The candidate has been successfully created.', type: CandidateResponseDto })
@ApiResponse({ status: 400, description: 'Bad Request.' }) @ApiResponse({ status: 400, description: 'Bad Request.' })
create(@Body() createCandidateDto: CreateCandidateDto) { create(@Body() createCandidateDto: CreateCandidateDto, @User('id') userId: number) {
return this.candidatesService.create(createCandidateDto); return this.candidatesService.create(createCandidateDto, userId);
} }
@Get() @Get()
@ApiOperation({ summary: 'Get all candidates' }) @ApiOperation({ summary: 'Get all candidates' })
@ApiResponse({ status: 200, description: 'Return all candidates.' }) @ApiResponse({
status: 200,
description: 'Return all candidates.',
schema: {
properties: {
data: {
type: 'array',
items: { $ref: '#/components/schemas/CandidateResponseDto' }
},
meta: {
type: 'object',
properties: {
total: { type: 'number' },
page: { type: 'number' },
limit: { type: 'number' },
totalPages: { type: 'number' }
}
}
}
}
})
findAll(@Query() query: QueryCandidateDto) { findAll(@Query() query: QueryCandidateDto) {
return this.candidatesService.findAll(query); return this.candidatesService.findAll(query);
} }
@Get('site/:siteId') @Get('site/:siteId')
@ApiOperation({ summary: 'Get candidates by site id' }) @ApiOperation({ summary: 'Get candidates by site id' })
@ApiResponse({ status: 200, description: 'Return the candidates for the site.' }) @ApiResponse({
status: 200,
description: 'Return the candidates for the site.',
type: [CandidateResponseDto]
})
findBySiteId(@Param('siteId', ParseIntPipe) siteId: number) { findBySiteId(@Param('siteId', ParseIntPipe) siteId: number) {
return this.candidatesService.findBySiteId(siteId); return this.candidatesService.findBySiteId(siteId);
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get a candidate by id' }) @ApiOperation({ summary: 'Get a candidate by id' })
@ApiResponse({ status: 200, description: 'Return the candidate.' }) @ApiResponse({ status: 200, description: 'Return the candidate.', type: CandidateResponseDto })
@ApiResponse({ status: 404, description: 'Candidate not found.' }) @ApiResponse({ status: 404, description: 'Candidate not found.' })
findOne(@Param('id', ParseIntPipe) id: number) { findOne(@Param('id', ParseIntPipe) id: number) {
return this.candidatesService.findOne(id); return this.candidatesService.findOne(id);
} }
@Patch(':id') @Patch(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@ApiOperation({ summary: 'Update a candidate' }) @ApiOperation({ summary: 'Update a candidate' })
@ApiResponse({ status: 200, description: 'The candidate has been successfully updated.' }) @ApiResponse({ status: 200, description: 'The candidate has been successfully updated.', type: CandidateResponseDto })
@ApiResponse({ status: 404, description: 'Candidate not found.' }) @ApiResponse({ status: 404, description: 'Candidate not found.' })
update( update(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() updateCandidateDto: UpdateCandidateDto, @Body() updateCandidateDto: UpdateCandidateDto,
@User('id') userId: number,
) { ) {
return this.candidatesService.update(id, updateCandidateDto); return this.candidatesService.update(id, updateCandidateDto);
} }
@Delete(':id') @Delete(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN)
@ApiOperation({ summary: 'Delete a candidate' }) @ApiOperation({ summary: 'Delete a candidate' })
@ApiResponse({ status: 200, description: 'The candidate has been successfully deleted.' }) @ApiResponse({ status: 200, description: 'The candidate has been successfully deleted.', type: CandidateResponseDto })
@ApiResponse({ status: 404, description: 'Candidate not found.' }) @ApiResponse({ status: 404, description: 'Candidate not found.' })
remove(@Param('id', ParseIntPipe) id: number) { remove(@Param('id', ParseIntPipe) id: number) {
return this.candidatesService.remove(id); return this.candidatesService.remove(id);
} }
@Post(':id/sites')
@ApiOperation({ summary: 'Add multiple sites to a candidate' })
@ApiResponse({
status: 200,
description: 'The sites have been successfully added to the candidate.',
type: CandidateResponseDto
})
@ApiResponse({ status: 404, description: 'Candidate not found.' })
addSitesToCandidate(
@Param('id', ParseIntPipe) id: number,
@Body() addSitesDto: AddSitesToCandidateDto,
) {
return this.candidatesService.addSitesToCandidate(id, addSitesDto);
}
} }
\ No newline at end of file
import { IsArray, IsNumber } from 'class-validator';
export class AddSitesToCandidateDto {
@IsArray()
@IsNumber({}, { each: true })
siteIds: number[];
}
\ No newline at end of file
import { ApiProperty } from '@nestjs/swagger';
import { CandidateType, CandidateStatus } from './create-candidate.dto';
import { CommentResponseDto } from '../../comments/dto/comment-response.dto';
export class CandidateResponseDto {
@ApiProperty({ description: 'Candidate ID' })
id: number;
@ApiProperty({ description: 'Candidate code' })
candidateCode: string;
@ApiProperty({ description: 'Latitude coordinate' })
latitude: number;
@ApiProperty({ description: 'Longitude coordinate' })
longitude: number;
@ApiProperty({ enum: CandidateType, description: 'Type of candidate' })
type: CandidateType;
@ApiProperty({ description: 'Address of the candidate' })
address: string;
@ApiProperty({ enum: CandidateStatus, description: 'Current status of the candidate' })
currentStatus: CandidateStatus;
@ApiProperty({ description: 'Whether the candidate is ongoing' })
onGoing: boolean;
@ApiProperty({ description: 'ID of the site this candidate belongs to' })
siteId: number;
@ApiProperty({ description: 'Creation timestamp' })
createdAt: Date;
@ApiProperty({ description: 'Last update timestamp' })
updatedAt: Date;
@ApiProperty({ description: 'Comments associated with this candidate', type: [CommentResponseDto] })
comments: CommentResponseDto[];
}
\ No newline at end of file
import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean, IsUUID, IsEmail, IsPhoneNumber } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum CandidateType { export enum CandidateType {
...@@ -15,32 +15,6 @@ export enum CandidateStatus { ...@@ -15,32 +15,6 @@ export enum CandidateStatus {
} }
export class CreateCandidateDto { export class CreateCandidateDto {
@ApiProperty({ description: 'The first name of the candidate' })
@IsString()
firstName: string;
@ApiProperty({ description: 'The last name of the candidate' })
@IsString()
lastName: string;
@ApiProperty({ description: 'The email address of the candidate' })
@IsEmail()
email: string;
@ApiProperty({ description: 'The phone number of the candidate', required: false })
@IsOptional()
@IsPhoneNumber()
phone?: string;
@ApiProperty({ description: 'The ID of the site the candidate is associated with' })
@IsNumber()
siteId: number;
@ApiProperty({ description: 'Additional notes about the candidate', required: false })
@IsOptional()
@IsString()
notes?: string;
@ApiProperty({ description: 'Candidate code' }) @ApiProperty({ description: 'Candidate code' })
@IsString() @IsString()
candidateCode: string; candidateCode: string;
...@@ -61,11 +35,6 @@ export class CreateCandidateDto { ...@@ -61,11 +35,6 @@ export class CreateCandidateDto {
@IsString() @IsString()
address: string; address: string;
@ApiPropertyOptional({ description: 'Additional comments' })
@IsString()
@IsOptional()
comments?: string;
@ApiProperty({ description: 'Current status of the candidate' }) @ApiProperty({ description: 'Current status of the candidate' })
@IsEnum(CandidateStatus) @IsEnum(CandidateStatus)
currentStatus: CandidateStatus; currentStatus: CandidateStatus;
...@@ -73,4 +42,13 @@ export class CreateCandidateDto { ...@@ -73,4 +42,13 @@ export class CreateCandidateDto {
@ApiProperty({ description: 'Whether the candidate is ongoing' }) @ApiProperty({ description: 'Whether the candidate is ongoing' })
@IsBoolean() @IsBoolean()
onGoing: boolean; onGoing: boolean;
@ApiProperty({ description: 'ID of the site this candidate belongs to' })
@IsNumber()
siteId: number;
@ApiPropertyOptional({ description: 'Initial comment for the candidate' })
@IsString()
@IsOptional()
comment?: string;
} }
\ No newline at end of file
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsOptional, IsEmail } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validator';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { CandidateType, CandidateStatus } from './create-candidate.dto';
export class QueryCandidateDto { export class QueryCandidateDto {
@ApiProperty({ description: 'Filter by first name', required: false }) @ApiProperty({ description: 'Filter by candidate code', required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
firstName?: string; candidateCode?: string;
@ApiProperty({ description: 'Filter by last name', required: false }) @ApiProperty({ description: 'Filter by type', required: false, enum: CandidateType })
@IsOptional() @IsOptional()
@IsString() @IsEnum(CandidateType)
lastName?: string; type?: CandidateType;
@ApiProperty({ description: 'Filter by current status', required: false, enum: CandidateStatus })
@IsOptional()
@IsEnum(CandidateStatus)
currentStatus?: CandidateStatus;
@ApiProperty({ description: 'Filter by email', required: false }) @ApiProperty({ description: 'Filter by ongoing status', required: false })
@IsOptional() @IsOptional()
@IsEmail() @IsBoolean()
email?: string; @Transform(({ value }) => value === 'true')
onGoing?: boolean;
@ApiProperty({ description: 'Filter by site ID', required: false }) @ApiProperty({ description: 'Filter by site ID', required: false })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Transform(({ value }) => parseInt(value))
siteId?: number; siteId?: number;
@ApiProperty({ description: 'Page number for pagination', required: false, default: 1 }) @ApiProperty({ description: 'Page number for pagination', required: false, default: 1 })
......
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsNumber, IsOptional, IsEmail, IsPhoneNumber } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validator';
import { CandidateType, CandidateStatus } from './create-candidate.dto';
export class UpdateCandidateDto { export class UpdateCandidateDto {
@ApiProperty({ description: 'The first name of the candidate', required: false }) @ApiPropertyOptional({ description: 'Candidate code' })
@IsOptional() @IsOptional()
@IsString() @IsString()
firstName?: string; candidateCode?: string;
@ApiProperty({ description: 'The last name of the candidate', required: false }) @ApiPropertyOptional({ description: 'Latitude coordinate' })
@IsOptional()
@IsNumber()
latitude?: number;
@ApiPropertyOptional({ description: 'Longitude coordinate' })
@IsOptional()
@IsNumber()
longitude?: number;
@ApiPropertyOptional({ enum: CandidateType, description: 'Type of candidate' })
@IsOptional()
@IsEnum(CandidateType)
type?: CandidateType;
@ApiPropertyOptional({ description: 'Address of the candidate' })
@IsOptional() @IsOptional()
@IsString() @IsString()
lastName?: string; address?: string;
@ApiProperty({ description: 'The email address of the candidate', required: false }) @ApiPropertyOptional({ enum: CandidateStatus, description: 'Current status of the candidate' })
@IsOptional() @IsOptional()
@IsEmail() @IsEnum(CandidateStatus)
email?: string; currentStatus?: CandidateStatus;
@ApiProperty({ description: 'The phone number of the candidate', required: false }) @ApiPropertyOptional({ description: 'Whether the candidate is ongoing' })
@IsOptional() @IsOptional()
@IsPhoneNumber() @IsBoolean()
phone?: string; onGoing?: boolean;
@ApiProperty({ description: 'The ID of the site the candidate is associated with', required: false }) @ApiPropertyOptional({ description: 'ID of the site this candidate belongs to' })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
siteId?: number; siteId?: number;
@ApiProperty({ description: 'Additional notes about the candidate', required: false })
@IsOptional()
@IsString()
notes?: string;
} }
\ No newline at end of file
import { Controller, Get, Post, Body, Param, Delete, UseGuards, ParseIntPipe } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { CommentsService } from './comments.service';
import { CreateCommentDto } from './dto/create-comment.dto';
import { CommentResponseDto } from './dto/comment-response.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client';
@ApiTags('comments')
@Controller('comments')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth('access-token')
export class CommentsController {
constructor(private readonly commentsService: CommentsService) { }
@Post()
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR)
@ApiOperation({ summary: 'Create a new comment' })
@ApiResponse({ status: 201, description: 'The comment has been successfully created.', type: CommentResponseDto })
@ApiResponse({ status: 400, description: 'Bad Request.' })
create(@Body() createCommentDto: CreateCommentDto) {
return this.commentsService.create(createCommentDto);
}
@Get('candidate/:candidateId')
@ApiOperation({ summary: 'Get all comments for a candidate' })
@ApiResponse({
status: 200,
description: 'Return all comments for the candidate.',
type: [CommentResponseDto]
})
findAll(@Param('candidateId', ParseIntPipe) candidateId: number) {
return this.commentsService.findAll(candidateId);
}
@Get(':id')
@ApiOperation({ summary: 'Get a comment by id' })
@ApiResponse({ status: 200, description: 'Return the comment.', type: CommentResponseDto })
@ApiResponse({ status: 404, description: 'Comment not found.' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.commentsService.findOne(id);
}
@Delete(':id')
@Roles(Role.ADMIN, Role.MANAGER)
@ApiOperation({ summary: 'Delete a comment' })
@ApiResponse({ status: 200, description: 'The comment has been successfully deleted.', type: CommentResponseDto })
@ApiResponse({ status: 404, description: 'Comment not found.' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.commentsService.remove(id);
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CommentsController } from './comments.controller';
import { PrismaService } from '../../common/prisma/prisma.service';
@Module({
controllers: [CommentsController],
providers: [CommentsService, PrismaService],
exports: [CommentsService],
})
export class CommentsModule { }
\ No newline at end of file
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../common/prisma/prisma.service';
import { CreateCommentDto } from './dto/create-comment.dto';
@Injectable()
export class CommentsService {
constructor(private prisma: PrismaService) { }
async create(createCommentDto: CreateCommentDto) {
return this.prisma.comment.create({
data: {
content: createCommentDto.content,
candidateId: createCommentDto.candidateId,
createdById: createCommentDto.createdById,
},
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
}
async findAll(candidateId: number) {
return this.prisma.comment.findMany({
where: {
candidateId,
},
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
}
async findOne(id: number) {
return this.prisma.comment.findUnique({
where: { id },
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
}
async remove(id: number) {
return this.prisma.comment.delete({
where: { id },
});
}
}
\ No newline at end of file
import { ApiProperty } from '@nestjs/swagger';
class UserResponseDto {
@ApiProperty({ description: 'User ID' })
id: number;
@ApiProperty({ description: 'User name' })
name: string;
@ApiProperty({ description: 'User email' })
email: string;
}
export class CommentResponseDto {
@ApiProperty({ description: 'Comment ID' })
id: number;
@ApiProperty({ description: 'Comment content' })
content: string;
@ApiProperty({ description: 'Creation timestamp' })
createdAt: Date;
@ApiProperty({ description: 'Last update timestamp' })
updatedAt: Date;
@ApiProperty({ description: 'ID of the candidate this comment belongs to' })
candidateId: number;
@ApiProperty({ description: 'User who created the comment', type: UserResponseDto })
createdBy: UserResponseDto;
}
\ No newline at end of file
import { IsString, IsNotEmpty, IsInt, IsOptional } from 'class-validator';
export class CreateCommentDto {
@IsString()
@IsNotEmpty()
content: string;
@IsInt()
@IsNotEmpty()
candidateId: number;
@IsInt()
@IsOptional()
createdById?: number;
}
\ No newline at end of file
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, IsEnum } from 'class-validator';
export enum OrderDirection {
ASC = 'asc',
DESC = 'desc',
}
export class FindSitesDto {
@ApiPropertyOptional({ description: 'Filter by site code' })
@IsOptional()
@IsString()
siteCode?: string;
@ApiPropertyOptional({ description: 'Filter by site name' })
@IsOptional()
@IsString()
siteName?: string;
@ApiPropertyOptional({ description: 'Filter by site address' })
@IsOptional()
@IsString()
address?: string;
@ApiPropertyOptional({ description: 'Filter by site city' })
@IsOptional()
@IsString()
city?: string;
@ApiPropertyOptional({ description: 'Filter by site state' })
@IsOptional()
@IsString()
state?: string;
@ApiPropertyOptional({ description: 'Filter by site country' })
@IsOptional()
@IsString()
country?: string;
@ApiPropertyOptional({ description: 'Order by field (e.g., siteName, siteCode, address, city, state, country)' })
@IsOptional()
@IsString()
orderBy?: string;
@ApiPropertyOptional({ description: 'Order direction (asc or desc)', enum: OrderDirection })
@IsOptional()
@IsEnum(OrderDirection)
orderDirection?: OrderDirection;
}
\ No newline at end of file
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
Delete, Delete,
ParseIntPipe, ParseIntPipe,
UseGuards, UseGuards,
Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { SitesService } from './sites.service'; import { SitesService } from './sites.service';
import { CreateSiteDto } from './dto/create-site.dto'; import { CreateSiteDto } from './dto/create-site.dto';
...@@ -23,6 +24,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; ...@@ -23,6 +24,7 @@ import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
import { User } from '../auth/decorators/user.decorator'; import { User } from '../auth/decorators/user.decorator';
import { FindSitesDto } from './dto/find-sites.dto';
@ApiTags('sites') @ApiTags('sites')
@Controller('sites') @Controller('sites')
...@@ -45,13 +47,13 @@ export class SitesController { ...@@ -45,13 +47,13 @@ export class SitesController {
} }
@Get() @Get()
@ApiOperation({ summary: 'Get all sites' }) @ApiOperation({ summary: 'Get all sites with filtering and ordering options' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Return all sites.', description: 'Return all sites with applied filters and ordering.',
}) })
findAll() { findAll(@Query() findSitesDto: FindSitesDto) {
return this.sitesService.findAll(); return this.sitesService.findAll(findSitesDto);
} }
@Get('code/:siteCode') @Get('code/:siteCode')
...@@ -76,6 +78,17 @@ export class SitesController { ...@@ -76,6 +78,17 @@ export class SitesController {
return this.sitesService.findOne(id); return this.sitesService.findOne(id);
} }
@Get(':id/with-candidates')
@ApiOperation({ summary: 'Get a site with its candidates' })
@ApiResponse({
status: 200,
description: 'Return the site with its candidates.',
})
@ApiResponse({ status: 404, description: 'Site not found.' })
findOneWithCandidates(@Param('id', ParseIntPipe) id: number) {
return this.sitesService.findOneWithCandidates(id);
}
@Patch(':id') @Patch(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER) @Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@ApiOperation({ summary: 'Update a site' }) @ApiOperation({ summary: 'Update a site' })
......
...@@ -2,6 +2,8 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common ...@@ -2,6 +2,8 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common
import { PrismaService } from '../../common/prisma/prisma.service'; import { PrismaService } from '../../common/prisma/prisma.service';
import { CreateSiteDto } from './dto/create-site.dto'; import { CreateSiteDto } from './dto/create-site.dto';
import { UpdateSiteDto } from './dto/update-site.dto'; import { UpdateSiteDto } from './dto/update-site.dto';
import { FindSitesDto, OrderDirection } from './dto/find-sites.dto';
import { Prisma } from '@prisma/client';
@Injectable() @Injectable()
export class SitesService { export class SitesService {
...@@ -40,8 +42,31 @@ export class SitesService { ...@@ -40,8 +42,31 @@ export class SitesService {
} }
} }
async findAll() { async findAll(findSitesDto: FindSitesDto) {
return this.prisma.site.findMany({ const {
siteCode,
siteName,
address,
city,
state,
country,
orderBy = 'siteName',
orderDirection = OrderDirection.ASC,
} = findSitesDto;
// Build where clause for filters
const where: Prisma.SiteWhereInput = {
...(siteCode && { siteCode: { contains: siteCode, mode: Prisma.QueryMode.insensitive } }),
...(siteName && { siteName: { contains: siteName, mode: Prisma.QueryMode.insensitive } }),
...(address && { address: { contains: address, mode: Prisma.QueryMode.insensitive } }),
...(city && { city: { contains: city, mode: Prisma.QueryMode.insensitive } }),
...(state && { state: { contains: state, mode: Prisma.QueryMode.insensitive } }),
...(country && { country: { contains: country, mode: Prisma.QueryMode.insensitive } }),
};
// Get all filtered results
const sites = await this.prisma.site.findMany({
where,
include: { include: {
createdBy: { createdBy: {
select: { select: {
...@@ -57,13 +82,40 @@ export class SitesService { ...@@ -57,13 +82,40 @@ export class SitesService {
email: true, email: true,
}, },
}, },
candidates: {
include: {
candidate: {
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
},
},
},
_count: { _count: {
select: { select: {
candidates: true, candidates: true,
}, },
}, },
}, },
orderBy: {
[orderBy]: orderDirection,
},
}); });
return sites;
} }
async findOne(id: number) { async findOne(id: number) {
...@@ -86,21 +138,71 @@ export class SitesService { ...@@ -86,21 +138,71 @@ export class SitesService {
}, },
candidates: { candidates: {
include: { include: {
createdBy: { candidate: {
select: { include: {
id: true, createdBy: {
name: true, select: {
email: true, id: true,
name: true,
email: true,
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
}, },
}, },
updatedBy: { },
select: { },
id: true, _count: {
name: true, select: {
email: true, candidates: true,
},
},
},
});
if (!site) {
throw new NotFoundException(`Site with ID ${id} not found`);
}
return site;
}
async findOneWithCandidates(id: number) {
const site = await this.prisma.site.findUnique({
where: { id },
include: {
candidates: {
include: {
candidate: {
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
}, },
}, },
}, },
orderBy: {
candidate: {
candidateCode: 'asc',
},
},
}, },
_count: { _count: {
select: { select: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment