Commit 65bf6dc7 by Augusto

Database All updated

parent 1941a82d
......@@ -17,7 +17,8 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"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": {
"@nestjs-modules/mailer": "^2.0.2",
......@@ -89,5 +90,8 @@
],
"coverageDirectory": "../coverage",
"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 {
refreshTokens RefreshToken[]
resetToken String? // For password reset
resetTokenExpiry DateTime? // Expiry time for reset token
Comment Comment[]
@@index([email])
@@index([role])
......@@ -51,44 +52,69 @@ model RefreshToken {
}
model Site {
id Int @id @default(autoincrement())
siteCode String @unique
id Int @id @default(autoincrement())
siteCode String @unique
siteName String
latitude Float
longitude Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
candidates Candidate[]
createdBy User? @relation("SiteCreator", fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
candidates CandidateSite[]
createdBy User? @relation("SiteCreator", fields: [createdById], references: [id])
createdById Int?
updatedBy User? @relation("SiteUpdater", fields: [updatedById], references: [id])
updatedBy User? @relation("SiteUpdater", fields: [updatedById], references: [id])
updatedById Int?
@@index([siteCode])
}
model Candidate {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
candidateCode String
latitude Float
longitude Float
type String
address String
comments String?
currentStatus String
onGoing Boolean @default(false)
site Site @relation(fields: [siteId], references: [id])
siteId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation("CandidateCreator", fields: [createdById], references: [id])
onGoing Boolean @default(false)
sites CandidateSite[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation("CandidateCreator", fields: [createdById], references: [id])
createdById Int?
updatedBy User? @relation("CandidateUpdater", fields: [updatedById], references: [id])
updatedBy User? @relation("CandidateUpdater", fields: [updatedById], references: [id])
updatedById Int?
comments Comment[]
@@unique([siteId, candidateCode])
@@index([candidateCode])
@@index([currentStatus])
@@index([siteId])
@@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';
import { AuthModule } from './modules/auth/auth.module';
import { SitesModule } from './modules/sites/sites.module';
import { CandidatesModule } from './modules/candidates/candidates.module';
import { CommentsModule } from './modules/comments/comments.module';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path';
......@@ -48,6 +49,7 @@ import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
AuthModule,
SitesModule,
CandidatesModule,
CommentsModule,
],
controllers: [AppController],
providers: [
......
......@@ -52,7 +52,7 @@ export class AuthService {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
expiresIn: '15m',
expiresIn: '24h',
}),
this.jwtService.signAsync(payload, {
expiresIn: '7d',
......@@ -116,7 +116,7 @@ export class AuthService {
const [newAccessToken, newRefreshToken] = await Promise.all([
this.jwtService.signAsync(newPayload, {
expiresIn: '15m',
expiresIn: '24h',
}),
this.jwtService.signAsync(newPayload, {
expiresIn: '7d',
......
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { CandidatesService } from './candidates.service';
import { CreateCandidateDto } from './dto/create-candidate.dto';
import { UpdateCandidateDto } from './dto/update-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')
@Controller('candidates')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth('access-token')
export class CandidatesController {
constructor(private readonly candidatesService: CandidatesService) { }
@Post()
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@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.' })
create(@Body() createCandidateDto: CreateCandidateDto) {
return this.candidatesService.create(createCandidateDto);
create(@Body() createCandidateDto: CreateCandidateDto, @User('id') userId: number) {
return this.candidatesService.create(createCandidateDto, userId);
}
@Get()
@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) {
return this.candidatesService.findAll(query);
}
@Get('site/:siteId')
@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) {
return this.candidatesService.findBySiteId(siteId);
}
@Get(':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.' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.candidatesService.findOne(id);
}
@Patch(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@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.' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateCandidateDto: UpdateCandidateDto,
@User('id') userId: number,
) {
return this.candidatesService.update(id, updateCandidateDto);
}
@Delete(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN)
@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.' })
remove(@Param('id', ParseIntPipe) id: number) {
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';
export enum CandidateType {
......@@ -15,32 +15,6 @@ export enum CandidateStatus {
}
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' })
@IsString()
candidateCode: string;
......@@ -61,11 +35,6 @@ export class CreateCandidateDto {
@IsString()
address: string;
@ApiPropertyOptional({ description: 'Additional comments' })
@IsString()
@IsOptional()
comments?: string;
@ApiProperty({ description: 'Current status of the candidate' })
@IsEnum(CandidateStatus)
currentStatus: CandidateStatus;
......@@ -73,4 +42,13 @@ export class CreateCandidateDto {
@ApiProperty({ description: 'Whether the candidate is ongoing' })
@IsBoolean()
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 { IsString, IsNumber, IsOptional, IsEmail } from 'class-validator';
import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validator';
import { Transform } from 'class-transformer';
import { CandidateType, CandidateStatus } from './create-candidate.dto';
export class QueryCandidateDto {
@ApiProperty({ description: 'Filter by first name', required: false })
@ApiProperty({ description: 'Filter by candidate code', required: false })
@IsOptional()
@IsString()
firstName?: string;
candidateCode?: string;
@ApiProperty({ description: 'Filter by last name', required: false })
@ApiProperty({ description: 'Filter by type', required: false, enum: CandidateType })
@IsOptional()
@IsString()
lastName?: string;
@IsEnum(CandidateType)
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()
@IsEmail()
email?: string;
@IsBoolean()
@Transform(({ value }) => value === 'true')
onGoing?: boolean;
@ApiProperty({ description: 'Filter by site ID', required: false })
@IsOptional()
@IsNumber()
@Transform(({ value }) => parseInt(value))
siteId?: number;
@ApiProperty({ description: 'Page number for pagination', required: false, default: 1 })
......
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsOptional, IsEmail, IsPhoneNumber } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validator';
import { CandidateType, CandidateStatus } from './create-candidate.dto';
export class UpdateCandidateDto {
@ApiProperty({ description: 'The first name of the candidate', required: false })
@ApiPropertyOptional({ description: 'Candidate code' })
@IsOptional()
@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()
@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()
@IsEmail()
email?: string;
@IsEnum(CandidateStatus)
currentStatus?: CandidateStatus;
@ApiProperty({ description: 'The phone number of the candidate', required: false })
@ApiPropertyOptional({ description: 'Whether the candidate is ongoing' })
@IsOptional()
@IsPhoneNumber()
phone?: string;
@IsBoolean()
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()
@IsNumber()
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 {
Delete,
ParseIntPipe,
UseGuards,
Query,
} from '@nestjs/common';
import { SitesService } from './sites.service';
import { CreateSiteDto } from './dto/create-site.dto';
......@@ -23,6 +24,7 @@ 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 { FindSitesDto } from './dto/find-sites.dto';
@ApiTags('sites')
@Controller('sites')
......@@ -45,13 +47,13 @@ export class SitesController {
}
@Get()
@ApiOperation({ summary: 'Get all sites' })
@ApiOperation({ summary: 'Get all sites with filtering and ordering options' })
@ApiResponse({
status: 200,
description: 'Return all sites.',
description: 'Return all sites with applied filters and ordering.',
})
findAll() {
return this.sitesService.findAll();
findAll(@Query() findSitesDto: FindSitesDto) {
return this.sitesService.findAll(findSitesDto);
}
@Get('code/:siteCode')
......@@ -76,6 +78,17 @@ export class SitesController {
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')
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@ApiOperation({ summary: 'Update a site' })
......
......@@ -2,6 +2,8 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common
import { PrismaService } from '../../common/prisma/prisma.service';
import { CreateSiteDto } from './dto/create-site.dto';
import { UpdateSiteDto } from './dto/update-site.dto';
import { FindSitesDto, OrderDirection } from './dto/find-sites.dto';
import { Prisma } from '@prisma/client';
@Injectable()
export class SitesService {
......@@ -40,8 +42,31 @@ export class SitesService {
}
}
async findAll() {
return this.prisma.site.findMany({
async findAll(findSitesDto: FindSitesDto) {
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: {
createdBy: {
select: {
......@@ -57,13 +82,40 @@ export class SitesService {
email: true,
},
},
candidates: {
include: {
candidate: {
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
},
},
},
_count: {
select: {
candidates: true,
},
},
},
orderBy: {
[orderBy]: orderDirection,
},
});
return sites;
}
async findOne(id: number) {
......@@ -86,21 +138,71 @@ export class SitesService {
},
candidates: {
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
candidate: {
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
_count: {
select: {
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: {
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