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 {
longitude Float
type String?
isDigi Boolean @default(false)
isReported Boolean @default(false)
companies CompanyName[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
......
......@@ -51,7 +51,7 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
const port = process.env.PORT ?? 3000;
const port = process.env.PORT ?? 3001;
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
console.log(`Swagger documentation is available at: http://localhost:${port}/docs`);
......
......@@ -104,7 +104,7 @@ export class CandidatesController {
}
@Delete(':id')
@Roles(Role.SUPERADMIN, Role.ADMIN)
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.PARTNER, Role.MANAGER)
@ApiOperation({ summary: 'Delete a candidate' })
@ApiResponse({ status: 200, description: 'The candidate has been successfully deleted.', type: CandidateResponseDto })
@ApiResponse({ status: 404, description: 'Candidate not found.' })
......
......@@ -2,10 +2,11 @@ import { IsString, IsNumber, IsOptional, IsEnum, IsBoolean } from 'class-validat
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum CandidateType {
TOWER = 'TOWER',
ROOFTOP = 'ROOFTOP',
GROUND = 'GROUND',
OTHER = 'OTHER',
Greenfield = 'Greenfield',
Indoor = 'Indoor',
Micro = 'Micro',
Rooftop = 'Rooftop',
Tunel = 'Tunel',
}
export enum CandidateStatus {
......@@ -16,7 +17,7 @@ export enum CandidateStatus {
MNO_VALIDATION = 'MNO_VALIDATION',
CLOSING = 'CLOSING',
SEARCH_AREA = 'SEARCH_AREA',
PAM = 'PAM'
}
export class CreateCandidateDto {
......
......@@ -18,7 +18,7 @@ export class CommentsController {
constructor(private readonly commentsService: CommentsService) { }
@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' })
@ApiBody({ type: CreateCommentDto })
@ApiResponse({ status: 201, description: 'The comment has been successfully created.', type: CommentResponseDto })
......@@ -49,7 +49,7 @@ export class CommentsController {
}
@Delete(':id')
@Roles(Role.ADMIN, Role.MANAGER, Role.SUPERADMIN)
@Roles(Role.ADMIN, Role.MANAGER, Role.SUPERADMIN, Role.PARTNER)
@ApiOperation({ summary: 'Delete a comment' })
@ApiResponse({ status: 200, description: 'The comment has been successfully deleted.', type: CommentResponseDto })
@ApiResponse({ status: 404, description: 'Comment not found.' })
......@@ -58,7 +58,7 @@ export class CommentsController {
}
@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' })
@ApiBody({ type: UpdateCommentDto })
@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 { Transform, Type } from 'class-transformer';
......@@ -8,56 +8,56 @@ export enum OrderDirection {
}
export class FindSitesDto {
@ApiPropertyOptional({ description: 'Page number (1-based)', default: 1 })
@ApiProperty({ required: false })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({ description: 'Number of items per page', default: 10 })
@ApiProperty({ required: false })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 10;
@ApiPropertyOptional({ description: 'Filter by site code' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
siteCode?: string;
@ApiPropertyOptional({ description: 'Filter by site name' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
siteName?: string;
@ApiPropertyOptional({ description: 'Filter by site type' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
type?: string;
@ApiPropertyOptional({ description: 'Filter by site address' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
address?: string;
@ApiPropertyOptional({ description: 'Filter by site city' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
city?: string;
@ApiPropertyOptional({ description: 'Filter by site state' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
state?: string;
@ApiPropertyOptional({ description: 'Filter by site country' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
country?: string;
@ApiPropertyOptional({ description: 'Filter by isDigi status', default: undefined })
@ApiProperty({ required: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => {
......@@ -67,7 +67,7 @@ export class FindSitesDto {
})
isDigi?: boolean;
@ApiPropertyOptional({ description: 'Filter sites that have candidates', default: false })
@ApiProperty({ required: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => {
......@@ -77,13 +77,18 @@ export class FindSitesDto {
})
withCandidates?: boolean;
@ApiPropertyOptional({ description: 'Order by field (e.g., siteName, siteCode, address, city, state, country)' })
@ApiProperty({ required: false })
@IsOptional()
@IsString()
orderBy?: string;
@ApiPropertyOptional({ description: 'Order direction (asc or desc)', enum: OrderDirection })
@ApiProperty({ required: false, enum: OrderDirection })
@IsOptional()
@IsEnum(OrderDirection)
orderDirection?: OrderDirection;
@ApiProperty({ required: false })
@IsBoolean()
@IsOptional()
isReported?: boolean;
}
\ No newline at end of file
......@@ -46,6 +46,9 @@ export class SiteResponseDto {
@ApiProperty({ description: 'Whether the site is a Digi site', default: false })
isDigi: boolean;
@ApiProperty({ description: 'Whether the site is reported', default: false })
isReported: boolean;
@ApiProperty({ description: 'Creation timestamp' })
createdAt: Date;
......
import { PartialType } from '@nestjs/swagger';
import { CreateSiteDto } from './create-site.dto';
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsOptional, IsBoolean, IsArray } from 'class-validator';
import { CompanyName } from '@prisma/client';
export class UpdateSiteDto extends PartialType(CreateSiteDto) { }
\ No newline at end of file
export class UpdateSiteDto {
@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 {
ParseIntPipe,
UseGuards,
Query,
Request,
} from '@nestjs/common';
import { SitesService } from './sites.service';
import { CreateSiteDto } from './dto/create-site.dto';
......@@ -18,6 +19,8 @@ import {
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
......@@ -26,6 +29,7 @@ import { Role } from '@prisma/client';
import { User } from '../auth/decorators/user.decorator';
import { FindSitesDto } from './dto/find-sites.dto';
import { SiteResponseDto } from './dto/site-response.dto';
import { Partner } from '../auth/decorators/partner.decorator';
@ApiTags('sites')
@Controller('sites')
......@@ -63,6 +67,8 @@ export class SitesController {
}
@Get()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.SUPERADMIN, Role.PARTNER)
@ApiOperation({
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).'
......@@ -88,8 +94,12 @@ export class SitesController {
}
}
})
findAll(@Query() findSitesDto: FindSitesDto) {
return this.sitesService.findAll(findSitesDto);
async findAll(
@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')
......@@ -112,22 +122,47 @@ export class SitesController {
type: SiteResponseDto
})
@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);
}
@Get(':id/with-candidates')
@ApiOperation({ summary: 'Get a site with its candidates' })
@ApiQuery({
name: 'partnerId',
required: false,
type: Number,
description: 'Filter candidates by specific partner ID'
})
@ApiResponse({
status: 200,
description: 'Return the site with its candidates.',
type: SiteResponseDto
})
@ApiResponse({ status: 404, description: 'Site not found.' })
findOneWithCandidates(@Param('id', ParseIntPipe) id: number) {
return this.sitesService.findOneWithCandidates(id);
findOneWithCandidates(
@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')
@Roles(Role.SUPERADMIN, Role.ADMIN, Role.MANAGER)
@ApiOperation({ summary: 'Update a site' })
......
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional, IsString, IsInt, Min, IsEnum } from 'class-validator';
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 {
@ApiProperty({
......@@ -30,4 +41,48 @@ export class FindUsersDto {
return value;
})
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 {
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-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 {
ApiTags,
ApiOperation,
......@@ -50,7 +50,7 @@ export class UsersController {
@ApiOperation({ summary: 'Get all users' })
@ApiResponse({
status: 200,
description: 'Return all users.',
description: 'Return all users with pagination.',
})
@ApiQuery({
name: 'active',
......@@ -64,6 +64,30 @@ export class UsersController {
type: Boolean,
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) {
return this.usersService.findAll(query);
}
......
......@@ -32,7 +32,11 @@ export class UsersService {
}
async findAll(query?: FindUsersDto) {
const where = {};
let where: any = {
NOT: {
role: Role.SUPERADMIN
}
};
if (query?.active !== undefined) {
where['isActive'] = query.active;
......@@ -40,13 +44,57 @@ export class UsersService {
// If unassignedPartners is true, filter for active users with PARTNER role and no partner
if (query?.unassignedPartners === true) {
where['isActive'] = true;
where['role'] = Role.PARTNER;
where['partnerId'] = null;
// Create a new where object for partner-specific query
where = {
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,
skip,
take: limit,
orderBy,
select: {
id: true,
email: true,
......@@ -58,6 +106,17 @@ export class UsersService {
partnerId: true,
},
});
// Return users with pagination metadata
return {
data: users,
meta: {
totalCount,
page,
limit,
totalPages: Math.ceil(totalCount / limit),
},
};
}
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