Commit a20e2148 by Augusto

0.0.6

parent 023e6ff8
-- CreateTable
CREATE TABLE "Photo" (
"id" SERIAL NOT NULL,
"url" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"candidateId" INTEGER NOT NULL,
CONSTRAINT "Photo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Photo_candidateId_idx" ON "Photo"("candidateId");
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_candidateId_fkey" FOREIGN KEY ("candidateId") REFERENCES "Candidate"("id") ON DELETE CASCADE ON UPDATE CASCADE;
\ No newline at end of file
...@@ -55,6 +55,7 @@ model Site { ...@@ -55,6 +55,7 @@ model Site {
longitude Float longitude Float
type String? type String?
isDigi Boolean @default(false) isDigi Boolean @default(false)
isReported Boolean @default(false)
companies CompanyName[] @default([]) companies CompanyName[] @default([])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
......
...@@ -51,7 +51,7 @@ async function bootstrap() { ...@@ -51,7 +51,7 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document); SwaggerModule.setup('docs', app, document);
const port = process.env.PORT ?? 3000; const port = process.env.PORT ?? 3001;
await app.listen(port); await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`); console.log(`Application is running on: http://localhost:${port}`);
console.log(`Swagger documentation is available at: http://localhost:${port}/docs`); console.log(`Swagger documentation is available at: http://localhost:${port}/docs`);
......
...@@ -104,7 +104,7 @@ export class CandidatesController { ...@@ -104,7 +104,7 @@ export class CandidatesController {
} }
@Delete(':id') @Delete(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN) @Roles(Role.SUPERADMIN, Role.ADMIN, Role.PARTNER, Role.MANAGER)
@ApiOperation({ summary: 'Delete a candidate' }) @ApiOperation({ summary: 'Delete a candidate' })
@ApiResponse({ status: 200, description: 'The candidate has been successfully deleted.', type: CandidateResponseDto }) @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.' })
......
...@@ -2,10 +2,11 @@ import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validat ...@@ -2,10 +2,11 @@ import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validat
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum CandidateType { export enum CandidateType {
TOWER = 'TOWER', Greenfield = 'Greenfield',
ROOFTOP = 'ROOFTOP', Indoor = 'Indoor',
GROUND = 'GROUND', Micro = 'Micro',
OTHER = 'OTHER', Rooftop = 'Rooftop',
Tunel = 'Tunel',
} }
export enum CandidateStatus { export enum CandidateStatus {
...@@ -16,7 +17,7 @@ export enum CandidateStatus { ...@@ -16,7 +17,7 @@ export enum CandidateStatus {
MNO_VALIDATION = 'MNO_VALIDATION', MNO_VALIDATION = 'MNO_VALIDATION',
CLOSING = 'CLOSING', CLOSING = 'CLOSING',
SEARCH_AREA = 'SEARCH_AREA', SEARCH_AREA = 'SEARCH_AREA',
PAM = 'PAM'
} }
export class CreateCandidateDto { export class CreateCandidateDto {
......
...@@ -18,7 +18,7 @@ export class CommentsController { ...@@ -18,7 +18,7 @@ export class CommentsController {
constructor(private readonly commentsService: CommentsService) { } constructor(private readonly commentsService: CommentsService) { }
@Post() @Post()
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.SUPERADMIN) @Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.SUPERADMIN, Role.PARTNER)
@ApiOperation({ summary: 'Create a new comment' }) @ApiOperation({ summary: 'Create a new comment' })
@ApiBody({ type: CreateCommentDto }) @ApiBody({ type: CreateCommentDto })
@ApiResponse({ status: 201, description: 'The comment has been successfully created.', type: CommentResponseDto }) @ApiResponse({ status: 201, description: 'The comment has been successfully created.', type: CommentResponseDto })
...@@ -49,7 +49,7 @@ export class CommentsController { ...@@ -49,7 +49,7 @@ export class CommentsController {
} }
@Delete(':id') @Delete(':id')
@Roles(Role.ADMIN, Role.MANAGER, Role.SUPERADMIN) @Roles(Role.ADMIN, Role.MANAGER, Role.SUPERADMIN, Role.PARTNER)
@ApiOperation({ summary: 'Delete a comment' }) @ApiOperation({ summary: 'Delete a comment' })
@ApiResponse({ status: 200, description: 'The comment has been successfully deleted.', type: CommentResponseDto }) @ApiResponse({ status: 200, description: 'The comment has been successfully deleted.', type: CommentResponseDto })
@ApiResponse({ status: 404, description: 'Comment not found.' }) @ApiResponse({ status: 404, description: 'Comment not found.' })
...@@ -58,7 +58,7 @@ export class CommentsController { ...@@ -58,7 +58,7 @@ export class CommentsController {
} }
@Put(':id') @Put(':id')
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.SUPERADMIN) @Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.SUPERADMIN, Role.PARTNER)
@ApiOperation({ summary: 'Update a comment' }) @ApiOperation({ summary: 'Update a comment' })
@ApiBody({ type: UpdateCommentDto }) @ApiBody({ type: UpdateCommentDto })
@ApiResponse({ status: 200, description: 'The comment has been successfully updated.', type: CommentResponseDto }) @ApiResponse({ status: 200, description: 'The comment has been successfully updated.', type: CommentResponseDto })
......
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, IsEnum, IsInt, Min, IsBoolean } from 'class-validator'; import { IsOptional, IsString, IsEnum, IsInt, Min, IsBoolean } from 'class-validator';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
...@@ -8,56 +8,56 @@ export enum OrderDirection { ...@@ -8,56 +8,56 @@ export enum OrderDirection {
} }
export class FindSitesDto { export class FindSitesDto {
@ApiPropertyOptional({ description: 'Page number (1-based)', default: 1 }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()
@Min(1) @Min(1)
page?: number = 1; page?: number = 1;
@ApiPropertyOptional({ description: 'Number of items per page', default: 10 }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()
@Min(1) @Min(1)
limit?: number = 10; limit?: number = 10;
@ApiPropertyOptional({ description: 'Filter by site code' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
siteCode?: string; siteCode?: string;
@ApiPropertyOptional({ description: 'Filter by site name' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
siteName?: string; siteName?: string;
@ApiPropertyOptional({ description: 'Filter by site type' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
type?: string; type?: string;
@ApiPropertyOptional({ description: 'Filter by site address' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
address?: string; address?: string;
@ApiPropertyOptional({ description: 'Filter by site city' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
city?: string; city?: string;
@ApiPropertyOptional({ description: 'Filter by site state' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
state?: string; state?: string;
@ApiPropertyOptional({ description: 'Filter by site country' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
country?: string; country?: string;
@ApiPropertyOptional({ description: 'Filter by isDigi status', default: undefined }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@Transform(({ value }) => { @Transform(({ value }) => {
...@@ -67,7 +67,7 @@ export class FindSitesDto { ...@@ -67,7 +67,7 @@ export class FindSitesDto {
}) })
isDigi?: boolean; isDigi?: boolean;
@ApiPropertyOptional({ description: 'Filter sites that have candidates', default: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@Transform(({ value }) => { @Transform(({ value }) => {
...@@ -77,13 +77,18 @@ export class FindSitesDto { ...@@ -77,13 +77,18 @@ export class FindSitesDto {
}) })
withCandidates?: boolean; withCandidates?: boolean;
@ApiPropertyOptional({ description: 'Order by field (e.g., siteName, siteCode, address, city, state, country)' }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
orderBy?: string; orderBy?: string;
@ApiPropertyOptional({ description: 'Order direction (asc or desc)', enum: OrderDirection }) @ApiProperty({ required: false, enum: OrderDirection })
@IsOptional() @IsOptional()
@IsEnum(OrderDirection) @IsEnum(OrderDirection)
orderDirection?: OrderDirection; orderDirection?: OrderDirection;
@ApiProperty({ required: false })
@IsBoolean()
@IsOptional()
isReported?: boolean;
} }
\ No newline at end of file
...@@ -46,6 +46,9 @@ export class SiteResponseDto { ...@@ -46,6 +46,9 @@ export class SiteResponseDto {
@ApiProperty({ description: 'Whether the site is a Digi site', default: false }) @ApiProperty({ description: 'Whether the site is a Digi site', default: false })
isDigi: boolean; isDigi: boolean;
@ApiProperty({ description: 'Whether the site is reported', default: false })
isReported: boolean;
@ApiProperty({ description: 'Creation timestamp' }) @ApiProperty({ description: 'Creation timestamp' })
createdAt: Date; createdAt: Date;
......
import { PartialType } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { CreateSiteDto } from './create-site.dto'; import { IsString, IsNumber, IsOptional, IsBoolean, IsArray } from 'class-validator';
import { CompanyName } from '@prisma/client';
export class UpdateSiteDto extends PartialType(CreateSiteDto) { } export class UpdateSiteDto {
\ No newline at end of file @ApiProperty({ required: false })
@IsString()
@IsOptional()
siteCode?: string;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
siteName?: string;
@ApiProperty({ required: false })
@IsNumber()
@IsOptional()
latitude?: number;
@ApiProperty({ required: false })
@IsNumber()
@IsOptional()
longitude?: number;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
type?: string;
@ApiProperty({ required: false })
@IsBoolean()
@IsOptional()
isDigi?: boolean;
@ApiProperty({ required: false })
@IsBoolean()
@IsOptional()
isReported?: boolean;
@ApiProperty({ required: false, enum: CompanyName, isArray: true })
@IsArray()
@IsOptional()
companies?: CompanyName[];
}
\ No newline at end of file
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
ParseIntPipe, ParseIntPipe,
UseGuards, UseGuards,
Query, Query,
Request,
} 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';
...@@ -18,6 +19,8 @@ import { ...@@ -18,6 +19,8 @@ import {
ApiOperation, ApiOperation,
ApiResponse, ApiResponse,
ApiBearerAuth, ApiBearerAuth,
ApiParam,
ApiQuery,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
...@@ -26,6 +29,7 @@ import { Role } from '@prisma/client'; ...@@ -26,6 +29,7 @@ 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'; import { FindSitesDto } from './dto/find-sites.dto';
import { SiteResponseDto } from './dto/site-response.dto'; import { SiteResponseDto } from './dto/site-response.dto';
import { Partner } from '../auth/decorators/partner.decorator';
@ApiTags('sites') @ApiTags('sites')
@Controller('sites') @Controller('sites')
...@@ -63,6 +67,8 @@ export class SitesController { ...@@ -63,6 +67,8 @@ export class SitesController {
} }
@Get() @Get()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.SUPERADMIN, Role.PARTNER)
@ApiOperation({ @ApiOperation({
summary: 'Get all sites for list view (with pagination)', summary: 'Get all sites for list view (with pagination)',
description: 'Returns paginated sites with applied filters and ordering. Use withCandidates=true to filter sites that have candidates. You can filter by type, siteCode, siteName, address, city, state, country, and isDigi status (true/false).' description: 'Returns paginated sites with applied filters and ordering. Use withCandidates=true to filter sites that have candidates. You can filter by type, siteCode, siteName, address, city, state, country, and isDigi status (true/false).'
...@@ -88,8 +94,12 @@ export class SitesController { ...@@ -88,8 +94,12 @@ export class SitesController {
} }
} }
}) })
findAll(@Query() findSitesDto: FindSitesDto) { async findAll(
return this.sitesService.findAll(findSitesDto); @Query() findSitesDto: FindSitesDto,
@Request() req,
) {
const partnerId = req.user.role === Role.PARTNER ? req.user.partnerId : null;
return this.sitesService.findAll(findSitesDto, partnerId);
} }
@Get('code/:siteCode') @Get('code/:siteCode')
...@@ -112,22 +122,47 @@ export class SitesController { ...@@ -112,22 +122,47 @@ export class SitesController {
type: SiteResponseDto type: SiteResponseDto
}) })
@ApiResponse({ status: 404, description: 'Site not found.' }) @ApiResponse({ status: 404, description: 'Site not found.' })
findOne(@Param('id', ParseIntPipe) id: number) { findOne(
@Param('id', ParseIntPipe) id: number,
@Partner() partnerId: number | null,
@User('role') role: Role
) {
// For PARTNER role, we restrict access to only see sites associated with their partnerId
if (role === Role.PARTNER) {
return this.sitesService.findOneFilteredByPartner(id, partnerId);
}
return this.sitesService.findOne(id); return this.sitesService.findOne(id);
} }
@Get(':id/with-candidates') @Get(':id/with-candidates')
@ApiOperation({ summary: 'Get a site with its candidates' }) @ApiOperation({ summary: 'Get a site with its candidates' })
@ApiQuery({
name: 'partnerId',
required: false,
type: Number,
description: 'Filter candidates by specific partner ID'
})
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Return the site with its candidates.', description: 'Return the site with its candidates.',
type: SiteResponseDto type: SiteResponseDto
}) })
@ApiResponse({ status: 404, description: 'Site not found.' }) @ApiResponse({ status: 404, description: 'Site not found.' })
findOneWithCandidates(@Param('id', ParseIntPipe) id: number) { findOneWithCandidates(
return this.sitesService.findOneWithCandidates(id); @Param('id', ParseIntPipe) id: number,
@Partner() partnerId: number | null,
@User('role') role: Role,
@Query('partnerId') filterPartnerId?: number
) {
// For PARTNER role, we restrict access to only see candidates created with their partnerId
if (role === Role.PARTNER) {
return this.sitesService.findOneWithCandidates(id, partnerId);
}
// If a specific partnerId is provided in the query, use that for filtering
return this.sitesService.findOneWithCandidates(id, filterPartnerId);
} }
@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' })
......
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { Injectable, NotFoundException, ConflictException, ForbiddenException } from '@nestjs/common';
import { PrismaClient, Prisma, CompanyName } from '@prisma/client'; import { PrismaClient, Prisma, CompanyName } from '@prisma/client';
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';
...@@ -10,7 +10,8 @@ export enum CandidateStatusPriority { ...@@ -10,7 +10,8 @@ export enum CandidateStatusPriority {
NEGOTIATION_ONGOING = 2, NEGOTIATION_ONGOING = 2,
MNO_VALIDATION = 3, MNO_VALIDATION = 3,
CLOSING = 4, CLOSING = 4,
APPROVED = 5, PAM = 5,
APPROVED = 6,
} }
@Injectable() @Injectable()
...@@ -76,7 +77,7 @@ export class SitesService { ...@@ -76,7 +77,7 @@ export class SitesService {
} }
} }
async findAll(findSitesDto: FindSitesDto) { async findAll(findSitesDto: FindSitesDto, partnerId?: number | null) {
const { const {
page = 1, page = 1,
limit = 10, limit = 10,
...@@ -91,6 +92,7 @@ export class SitesService { ...@@ -91,6 +92,7 @@ export class SitesService {
withCandidates, withCandidates,
type, type,
isDigi, isDigi,
isReported,
} = findSitesDto; } = findSitesDto;
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
...@@ -104,8 +106,10 @@ export class SitesService { ...@@ -104,8 +106,10 @@ export class SitesService {
...(country && { country: { contains: country, mode: Prisma.QueryMode.insensitive } }), ...(country && { country: { contains: country, mode: Prisma.QueryMode.insensitive } }),
...(type && { type: { contains: type, mode: Prisma.QueryMode.insensitive } }), ...(type && { type: { contains: type, mode: Prisma.QueryMode.insensitive } }),
...(isDigi !== undefined && { isDigi }), ...(isDigi !== undefined && { isDigi }),
...(isReported !== undefined && { isReported }),
}; };
// Only filter by candidates if withCandidates is true
if (withCandidates === true) { if (withCandidates === true) {
where.candidates = { where.candidates = {
some: {}, some: {},
...@@ -124,26 +128,33 @@ export class SitesService { ...@@ -124,26 +128,33 @@ export class SitesService {
candidates: true, candidates: true,
}, },
}, },
candidates: withCandidates ? { candidates: {
where: partnerId ? {
candidate: {
partnerId: partnerId
}
} : undefined,
include: { include: {
candidate: true, candidate: {
}, select: {
} : undefined, currentStatus: true
}
}
}
},
}, },
}), }),
this.prisma.site.count({ where }), this.prisma.site.count({ where }),
]); ]);
// Add highest priority status to sites with candidates // Add highest priority status to all sites
const sitesWithHighestStatus = sites.map(site => { const sitesWithHighestStatus = sites.map(site => {
if (site.candidates && site.candidates.length > 0) { const highestCandidateStatus = this.getHighestPriorityStatus(site.candidates);
return { return {
...site, ...site,
highestCandidateStatus: this.getHighestPriorityStatus(site.candidates), highestCandidateStatus,
candidates: undefined, // Remove candidates to avoid sending large data candidates: withCandidates ? site.candidates : undefined, // Only include full candidates data if withCandidates is true
}; };
}
return site;
}); });
return { return {
...@@ -221,11 +232,89 @@ export class SitesService { ...@@ -221,11 +232,89 @@ export class SitesService {
return site; return site;
} }
async findOneWithCandidates(id: number) { async findOneFilteredByPartner(id: number, partnerId: number | null) {
if (!partnerId) {
throw new ForbiddenException('User does not have access to any partner resources');
}
const site = await this.prisma.site.findUnique({
where: { id },
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
updatedBy: {
select: {
id: true,
name: true,
email: true,
},
},
candidates: {
where: {
candidate: {
partnerId: partnerId
}
},
include: {
candidate: {
include: {
createdBy: {
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`);
}
// Add highest priority status if the site has candidates
if (site.candidates && site.candidates.length > 0) {
const highestCandidateStatus = this.getHighestPriorityStatus(site.candidates);
return {
...site,
highestCandidateStatus,
};
}
return site;
}
async findOneWithCandidates(id: number, partnerId?: number | null) {
const site = await this.prisma.site.findUnique({ const site = await this.prisma.site.findUnique({
where: { id }, where: { id },
include: { include: {
candidates: { candidates: {
where: partnerId ? {
candidate: {
partnerId: partnerId
}
} : undefined,
include: { include: {
candidate: { candidate: {
include: { include: {
...@@ -243,6 +332,12 @@ export class SitesService { ...@@ -243,6 +332,12 @@ export class SitesService {
email: true, email: true,
}, },
}, },
partner: {
select: {
id: true,
name: true,
},
},
comments: { comments: {
take: 1, take: 1,
include: { include: {
...@@ -281,6 +376,79 @@ export class SitesService { ...@@ -281,6 +376,79 @@ export class SitesService {
return site; return site;
} }
async findOneWithCandidatesFilteredByPartner(id: number, partnerId: number | null) {
if (!partnerId) {
throw new ForbiddenException('User does not have access to any partner resources');
}
// First, fetch the site with all candidates
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,
},
},
comments: {
take: 1,
include: {
createdBy: {
select: {
id: true,
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
},
},
},
},
},
},
});
if (!site) {
throw new NotFoundException(`Site with ID ${id} not found`);
}
// Filter the candidates to only include those with the partner's ID
const filteredSite = {
...site,
candidates: site.candidates.filter(candidateSite =>
candidateSite.candidate.partnerId === partnerId
)
};
// Add highest priority status if the site has filtered candidates
if (filteredSite.candidates && filteredSite.candidates.length > 0) {
const highestCandidateStatus = this.getHighestPriorityStatus(filteredSite.candidates);
return {
...filteredSite,
highestCandidateStatus,
};
}
return filteredSite;
}
async update(id: number, updateSiteDto: UpdateSiteDto, userId: number) { async update(id: number, updateSiteDto: UpdateSiteDto, userId: number) {
try { try {
return await this.prisma.site.update({ return await this.prisma.site.update({
...@@ -292,6 +460,7 @@ export class SitesService { ...@@ -292,6 +460,7 @@ export class SitesService {
longitude: updateSiteDto.longitude, longitude: updateSiteDto.longitude,
type: updateSiteDto.type, type: updateSiteDto.type,
isDigi: updateSiteDto.isDigi, isDigi: updateSiteDto.isDigi,
isReported: updateSiteDto.isReported,
companies: updateSiteDto.companies !== undefined ? updateSiteDto.companies : undefined, companies: updateSiteDto.companies !== undefined ? updateSiteDto.companies : undefined,
updatedById: userId, updatedById: userId,
}, },
...@@ -414,6 +583,7 @@ export class SitesService { ...@@ -414,6 +583,7 @@ export class SitesService {
withCandidates, withCandidates,
type, type,
isDigi, isDigi,
isReported,
} = findSitesDto; } = findSitesDto;
const where: Prisma.SiteWhereInput = { const where: Prisma.SiteWhereInput = {
...@@ -425,13 +595,11 @@ export class SitesService { ...@@ -425,13 +595,11 @@ export class SitesService {
...(country && { country: { contains: country, mode: Prisma.QueryMode.insensitive } }), ...(country && { country: { contains: country, mode: Prisma.QueryMode.insensitive } }),
...(type && { type: { contains: type, mode: Prisma.QueryMode.insensitive } }), ...(type && { type: { contains: type, mode: Prisma.QueryMode.insensitive } }),
...(isDigi !== undefined && { isDigi }), ...(isDigi !== undefined && { isDigi }),
}; ...(isReported !== undefined && { isReported }),
candidates: {
if (withCandidates === true) {
where.candidates = {
some: {}, some: {},
}; }
} };
const sites = await this.prisma.site.findMany({ const sites = await this.prisma.site.findMany({
where, where,
...@@ -442,24 +610,26 @@ export class SitesService { ...@@ -442,24 +610,26 @@ export class SitesService {
candidates: true, candidates: true,
}, },
}, },
candidates: withCandidates ? { candidates: {
include: { include: {
candidate: true, candidate: {
}, select: {
} : undefined, currentStatus: true
}
}
}
},
}, },
}); });
// Add highest priority status to sites with candidates // Add highest priority status to all sites
const sitesWithHighestStatus = sites.map(site => { const sitesWithHighestStatus = sites.map(site => {
if (site.candidates && site.candidates.length > 0) { const highestCandidateStatus = this.getHighestPriorityStatus(site.candidates);
return { return {
...site, ...site,
highestCandidateStatus: this.getHighestPriorityStatus(site.candidates), highestCandidateStatus,
candidates: undefined, // Remove candidates to avoid sending large data candidates: withCandidates ? site.candidates : undefined, // Only include full candidates data if withCandidates is true
}; };
}
return site;
}); });
return sitesWithHighestStatus; return sitesWithHighestStatus;
......
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean, IsOptional, IsString, IsInt, Min, IsEnum } from 'class-validator';
import { Transform } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
export enum UserOrderBy {
NAME_ASC = 'name_asc',
NAME_DESC = 'name_desc',
EMAIL_ASC = 'email_asc',
EMAIL_DESC = 'email_desc',
CREATED_AT_ASC = 'createdAt_asc',
CREATED_AT_DESC = 'createdAt_desc',
ROLE_ASC = 'role_asc',
ROLE_DESC = 'role_desc',
}
export class FindUsersDto { export class FindUsersDto {
@ApiProperty({ @ApiProperty({
...@@ -30,4 +41,48 @@ export class FindUsersDto { ...@@ -30,4 +41,48 @@ export class FindUsersDto {
return value; return value;
}) })
unassignedPartners?: boolean; unassignedPartners?: boolean;
@ApiProperty({
description: 'Page number for pagination (starts at 1)',
example: 1,
required: false,
default: 1,
})
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiProperty({
description: 'Number of items per page',
example: 10,
required: false,
default: 10,
})
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
limit?: number = 10;
@ApiProperty({
description: 'Search term to filter users by name or email',
example: 'john',
required: false,
})
@IsOptional()
@IsString()
search?: string;
@ApiProperty({
description: 'Order by field and direction',
enum: UserOrderBy,
example: UserOrderBy.NAME_ASC,
required: false,
default: UserOrderBy.NAME_ASC,
})
@IsOptional()
@IsEnum(UserOrderBy)
orderBy?: UserOrderBy = UserOrderBy.NAME_ASC;
} }
\ No newline at end of file
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
import { UsersService } from './users.service'; import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto'; import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { FindUsersDto } from './dto/find-users.dto'; import { FindUsersDto, UserOrderBy } from './dto/find-users.dto';
import { import {
ApiTags, ApiTags,
ApiOperation, ApiOperation,
...@@ -50,7 +50,7 @@ export class UsersController { ...@@ -50,7 +50,7 @@ export class UsersController {
@ApiOperation({ summary: 'Get all users' }) @ApiOperation({ summary: 'Get all users' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Return all users.', description: 'Return all users with pagination.',
}) })
@ApiQuery({ @ApiQuery({
name: 'active', name: 'active',
...@@ -64,6 +64,30 @@ export class UsersController { ...@@ -64,6 +64,30 @@ export class UsersController {
type: Boolean, type: Boolean,
description: 'Get only active users with PARTNER role who don\'t have a partner assigned (true/false)', description: 'Get only active users with PARTNER role who don\'t have a partner assigned (true/false)',
}) })
@ApiQuery({
name: 'page',
required: false,
type: Number,
description: 'Page number for pagination (starts at 1)',
})
@ApiQuery({
name: 'limit',
required: false,
type: Number,
description: 'Number of items per page',
})
@ApiQuery({
name: 'search',
required: false,
type: String,
description: 'Search term to filter users by name or email',
})
@ApiQuery({
name: 'orderBy',
required: false,
enum: UserOrderBy,
description: 'Order by field and direction',
})
findAll(@Query() query: FindUsersDto) { findAll(@Query() query: FindUsersDto) {
return this.usersService.findAll(query); return this.usersService.findAll(query);
} }
......
...@@ -32,7 +32,11 @@ export class UsersService { ...@@ -32,7 +32,11 @@ export class UsersService {
} }
async findAll(query?: FindUsersDto) { async findAll(query?: FindUsersDto) {
const where = {}; let where: any = {
NOT: {
role: Role.SUPERADMIN
}
};
if (query?.active !== undefined) { if (query?.active !== undefined) {
where['isActive'] = query.active; where['isActive'] = query.active;
...@@ -40,13 +44,57 @@ export class UsersService { ...@@ -40,13 +44,57 @@ export class UsersService {
// If unassignedPartners is true, filter for active users with PARTNER role and no partner // If unassignedPartners is true, filter for active users with PARTNER role and no partner
if (query?.unassignedPartners === true) { if (query?.unassignedPartners === true) {
where['isActive'] = true; // Create a new where object for partner-specific query
where['role'] = Role.PARTNER; where = {
where['partnerId'] = null; isActive: true,
role: Role.PARTNER,
partnerId: null
};
} }
return this.prisma.user.findMany({ // Add search functionality for name or email
if (query?.search) {
where.OR = [
{
name: {
contains: query.search,
mode: 'insensitive', // Case insensitive search
},
},
{
email: {
contains: query.search,
mode: 'insensitive', // Case insensitive search
},
},
];
}
// Set up pagination
const page = query?.page || 1;
const limit = query?.limit || 10;
const skip = (page - 1) * limit;
// Set up ordering
let orderBy: any = { name: 'asc' }; // Default ordering
if (query?.orderBy) {
const [field, direction] = query.orderBy.split('_');
// Special handling for role field if needed
orderBy = { [field]: direction };
}
// Get total count for pagination metadata
const totalCount = await this.prisma.user.count({ where });
// Fetch users with pagination and ordering
const users = await this.prisma.user.findMany({
where, where,
skip,
take: limit,
orderBy,
select: { select: {
id: true, id: true,
email: true, email: true,
...@@ -58,6 +106,17 @@ export class UsersService { ...@@ -58,6 +106,17 @@ export class UsersService {
partnerId: true, partnerId: true,
}, },
}); });
// Return users with pagination metadata
return {
data: users,
meta: {
totalCount,
page,
limit,
totalPages: Math.ceil(totalCount / limit),
},
};
} }
async findOne(id: number) { async findOne(id: number) {
......
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