fix
This commit is contained in:
30
src/profile-fields/dto/assign-section.dto.ts
Normal file
30
src/profile-fields/dto/assign-section.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IsUUID, IsOptional, IsBoolean, IsInt } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class AssignSectionDto {
|
||||
@ApiProperty({ description: 'Agent Type ID to assign this section to' })
|
||||
@IsUUID()
|
||||
agentTypeId: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sort order for this agent type', default: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether this section is required for this agent type', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateAssignmentDto {
|
||||
@ApiPropertyOptional({ description: 'Sort order for this agent type' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether this section is required for this agent type' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isRequired?: boolean;
|
||||
}
|
||||
113
src/profile-fields/dto/create-field.dto.ts
Normal file
113
src/profile-fields/dto/create-field.dto.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsUUID,
|
||||
IsEnum,
|
||||
IsObject,
|
||||
IsArray,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
export class FieldOptionDto {
|
||||
@ApiProperty({ description: 'Option value' })
|
||||
@IsString()
|
||||
value: string;
|
||||
|
||||
@ApiProperty({ description: 'Option display label' })
|
||||
@IsString()
|
||||
label: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sort order for this option' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class RangeConfigDto {
|
||||
@ApiProperty({ description: 'Minimum value' })
|
||||
min: number;
|
||||
|
||||
@ApiProperty({ description: 'Maximum value' })
|
||||
max: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Step increment', default: 1 })
|
||||
@IsOptional()
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export class CreateFieldDto {
|
||||
@ApiProperty({ description: 'Section ID this field belongs to' })
|
||||
@IsUUID()
|
||||
sectionId: string;
|
||||
|
||||
@ApiProperty({ description: 'Field name', example: 'Years in Business' })
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Field type', enum: FieldType })
|
||||
@IsEnum(FieldType)
|
||||
fieldType: FieldType;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Help text for the field' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Placeholder text' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
placeholder?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Default value (JSON string for complex types)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
defaultValue?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sort order within the section', default: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether this field is active', default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether this field is required', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isRequired?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Validation rules (JSON object)' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
validation?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Options for SELECT, RADIO, MULTI_SELECT, CHECKBOX_GROUP', type: [FieldOptionDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => FieldOptionDto)
|
||||
options?: FieldOptionDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Configuration for RANGE fields', type: RangeConfigDto })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
rangeConfig?: RangeConfigDto;
|
||||
|
||||
@ApiPropertyOptional({ description: 'UI configuration (JSON object)' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
uiConfig?: Record<string, any>;
|
||||
}
|
||||
36
src/profile-fields/dto/create-section.dto.ts
Normal file
36
src/profile-fields/dto/create-section.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsInt, MinLength, MaxLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateSectionDto {
|
||||
@ApiProperty({ description: 'Section name', example: 'Location' })
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Section description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Icon URL or icon name' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
icon?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sort order for display', default: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether this section is active', default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Whether this section applies to all agent types', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isGlobal?: boolean;
|
||||
}
|
||||
5
src/profile-fields/dto/index.ts
Normal file
5
src/profile-fields/dto/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './create-section.dto';
|
||||
export * from './update-section.dto';
|
||||
export * from './create-field.dto';
|
||||
export * from './update-field.dto';
|
||||
export * from './assign-section.dto';
|
||||
4
src/profile-fields/dto/update-field.dto.ts
Normal file
4
src/profile-fields/dto/update-field.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType, OmitType } from '@nestjs/swagger';
|
||||
import { CreateFieldDto } from './create-field.dto';
|
||||
|
||||
export class UpdateFieldDto extends PartialType(OmitType(CreateFieldDto, ['sectionId'] as const)) {}
|
||||
4
src/profile-fields/dto/update-section.dto.ts
Normal file
4
src/profile-fields/dto/update-section.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateSectionDto } from './create-section.dto';
|
||||
|
||||
export class UpdateSectionDto extends PartialType(CreateSectionDto) {}
|
||||
4
src/profile-fields/index.ts
Normal file
4
src/profile-fields/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './profile-fields.module';
|
||||
export * from './profile-sections.service';
|
||||
export * from './profile-fields.service';
|
||||
export * from './dto';
|
||||
99
src/profile-fields/profile-fields.controller.ts
Normal file
99
src/profile-fields/profile-fields.controller.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { ProfileFieldsService } from './profile-fields.service';
|
||||
import { CreateFieldDto, UpdateFieldDto } from './dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { UserRole } from '@prisma/client';
|
||||
|
||||
@ApiTags('Profile Fields')
|
||||
@Controller('profile-fields')
|
||||
export class ProfileFieldsController {
|
||||
constructor(private readonly fieldsService: ProfileFieldsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create a new profile field (Admin only)' })
|
||||
create(@Body() createFieldDto: CreateFieldDto) {
|
||||
return this.fieldsService.create(createFieldDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get all profile fields (Admin only)' })
|
||||
@ApiQuery({ name: 'sectionId', required: false, type: String })
|
||||
@ApiQuery({ name: 'includeInactive', required: false, type: Boolean })
|
||||
findAll(
|
||||
@Query('sectionId') sectionId?: string,
|
||||
@Query('includeInactive') includeInactive?: string,
|
||||
) {
|
||||
return this.fieldsService.findAll(sectionId, includeInactive === 'true');
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get a profile field by ID (Admin only)' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.fieldsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update a profile field (Admin only)' })
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateFieldDto: UpdateFieldDto,
|
||||
) {
|
||||
return this.fieldsService.update(id, updateFieldDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Delete a profile field (Admin only)' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.fieldsService.remove(id);
|
||||
}
|
||||
|
||||
@Patch('section/:sectionId/reorder')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Reorder fields within a section (Admin only)' })
|
||||
reorderFields(
|
||||
@Param('sectionId', ParseUUIDPipe) sectionId: string,
|
||||
@Body() body: { fieldIds: string[] },
|
||||
) {
|
||||
return this.fieldsService.reorderFields(sectionId, body.fieldIds);
|
||||
}
|
||||
|
||||
@Patch('bulk/order')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Bulk update field order (Admin only)' })
|
||||
bulkUpdateOrder(@Body() body: { updates: { id: string; sortOrder: number }[] }) {
|
||||
return this.fieldsService.bulkUpdateOrder(body.updates);
|
||||
}
|
||||
}
|
||||
14
src/profile-fields/profile-fields.module.ts
Normal file
14
src/profile-fields/profile-fields.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProfileSectionsController } from './profile-sections.controller';
|
||||
import { ProfileSectionsService } from './profile-sections.service';
|
||||
import { ProfileFieldsController } from './profile-fields.controller';
|
||||
import { ProfileFieldsService } from './profile-fields.service';
|
||||
import { PrismaModule } from '../prisma';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [ProfileSectionsController, ProfileFieldsController],
|
||||
providers: [ProfileSectionsService, ProfileFieldsService],
|
||||
exports: [ProfileSectionsService, ProfileFieldsService],
|
||||
})
|
||||
export class ProfileFieldsModule {}
|
||||
233
src/profile-fields/profile-fields.service.ts
Normal file
233
src/profile-fields/profile-fields.service.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma';
|
||||
import { CreateFieldDto, UpdateFieldDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class ProfileFieldsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Generate a URL-friendly slug from the name
|
||||
*/
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/(^_|_$)/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new profile field
|
||||
*/
|
||||
async create(createFieldDto: CreateFieldDto) {
|
||||
const { sectionId, options, rangeConfig, validation, uiConfig, ...rest } = createFieldDto;
|
||||
|
||||
// Verify section exists
|
||||
const section = await this.prisma.profileSection.findUnique({
|
||||
where: { id: sectionId },
|
||||
});
|
||||
|
||||
if (!section) {
|
||||
throw new NotFoundException(`Section with ID ${sectionId} not found`);
|
||||
}
|
||||
|
||||
// Generate slug
|
||||
const slug = this.generateSlug(createFieldDto.name);
|
||||
|
||||
// Check if slug is unique within section
|
||||
const existing = await this.prisma.profileField.findUnique({
|
||||
where: {
|
||||
sectionId_slug: {
|
||||
sectionId,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('A field with this name already exists in this section');
|
||||
}
|
||||
|
||||
// Validate options for certain field types
|
||||
const requiresOptions = ['SELECT', 'RADIO', 'MULTI_SELECT', 'CHECKBOX'];
|
||||
if (requiresOptions.includes(createFieldDto.fieldType) && (!options || options.length === 0)) {
|
||||
// CHECKBOX can be a single toggle without options
|
||||
if (createFieldDto.fieldType !== 'CHECKBOX') {
|
||||
throw new BadRequestException(`Field type ${createFieldDto.fieldType} requires options`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate range config for RANGE type
|
||||
if (createFieldDto.fieldType === 'RANGE' && !rangeConfig) {
|
||||
throw new BadRequestException('RANGE field type requires rangeConfig');
|
||||
}
|
||||
|
||||
return this.prisma.profileField.create({
|
||||
data: {
|
||||
...rest,
|
||||
sectionId,
|
||||
slug,
|
||||
options: options ? (options as unknown as Prisma.InputJsonValue) : undefined,
|
||||
rangeConfig: rangeConfig ? (rangeConfig as unknown as Prisma.InputJsonValue) : undefined,
|
||||
validation: validation ? (validation as unknown as Prisma.InputJsonValue) : undefined,
|
||||
uiConfig: uiConfig ? (uiConfig as unknown as Prisma.InputJsonValue) : undefined,
|
||||
},
|
||||
include: {
|
||||
section: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fields, optionally filtered by section
|
||||
*/
|
||||
async findAll(sectionId?: string, includeInactive = false) {
|
||||
const where: any = {};
|
||||
|
||||
if (sectionId) {
|
||||
where.sectionId = sectionId;
|
||||
}
|
||||
|
||||
if (!includeInactive) {
|
||||
where.isActive = true;
|
||||
}
|
||||
|
||||
return this.prisma.profileField.findMany({
|
||||
where,
|
||||
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
|
||||
include: {
|
||||
section: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single field by ID
|
||||
*/
|
||||
async findOne(id: string) {
|
||||
const field = await this.prisma.profileField.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
section: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!field) {
|
||||
throw new NotFoundException(`Field with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a field
|
||||
*/
|
||||
async update(id: string, updateFieldDto: UpdateFieldDto) {
|
||||
const field = await this.findOne(id);
|
||||
|
||||
const data: any = { ...updateFieldDto };
|
||||
|
||||
// If name is being updated, update the slug too
|
||||
if (updateFieldDto.name) {
|
||||
const newSlug = this.generateSlug(updateFieldDto.name);
|
||||
|
||||
// Check if new slug conflicts within the section
|
||||
const existing = await this.prisma.profileField.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ id: { not: id } },
|
||||
{ sectionId: field.sectionId },
|
||||
{ slug: newSlug },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('A field with this name already exists in this section');
|
||||
}
|
||||
|
||||
data.slug = newSlug;
|
||||
}
|
||||
|
||||
return this.prisma.profileField.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
section: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a field
|
||||
*/
|
||||
async remove(id: string) {
|
||||
await this.findOne(id);
|
||||
|
||||
// Check if any agents have values for this field
|
||||
const valuesCount = await this.prisma.agentProfileFieldValue.count({
|
||||
where: { fieldId: id },
|
||||
});
|
||||
|
||||
if (valuesCount > 0) {
|
||||
throw new ConflictException(
|
||||
`Cannot delete field. ${valuesCount} agent(s) have values for this field. Deactivate the field instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.profileField.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder fields within a section
|
||||
*/
|
||||
async reorderFields(sectionId: string, fieldIds: string[]) {
|
||||
// Verify section exists
|
||||
const section = await this.prisma.profileSection.findUnique({
|
||||
where: { id: sectionId },
|
||||
});
|
||||
|
||||
if (!section) {
|
||||
throw new NotFoundException(`Section with ID ${sectionId} not found`);
|
||||
}
|
||||
|
||||
// Update sort order for each field
|
||||
const updates = fieldIds.map((fieldId, index) =>
|
||||
this.prisma.profileField.update({
|
||||
where: { id: fieldId },
|
||||
data: { sortOrder: index },
|
||||
}),
|
||||
);
|
||||
|
||||
await this.prisma.$transaction(updates);
|
||||
|
||||
return this.findAll(sectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update fields (for drag-drop reordering)
|
||||
*/
|
||||
async bulkUpdateOrder(updates: { id: string; sortOrder: number }[]) {
|
||||
const operations = updates.map(({ id, sortOrder }) =>
|
||||
this.prisma.profileField.update({
|
||||
where: { id },
|
||||
data: { sortOrder },
|
||||
}),
|
||||
);
|
||||
|
||||
await this.prisma.$transaction(operations);
|
||||
|
||||
return { success: true, updated: updates.length };
|
||||
}
|
||||
}
|
||||
111
src/profile-fields/profile-sections.controller.ts
Normal file
111
src/profile-fields/profile-sections.controller.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { ProfileSectionsService } from './profile-sections.service';
|
||||
import { CreateSectionDto, UpdateSectionDto, AssignSectionDto, UpdateAssignmentDto } from './dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { UserRole } from '@prisma/client';
|
||||
|
||||
@ApiTags('Profile Sections')
|
||||
@Controller('profile-sections')
|
||||
export class ProfileSectionsController {
|
||||
constructor(private readonly sectionsService: ProfileSectionsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create a new profile section (Admin only)' })
|
||||
create(@Body() createSectionDto: CreateSectionDto) {
|
||||
return this.sectionsService.create(createSectionDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get all profile sections (Admin only)' })
|
||||
@ApiQuery({ name: 'includeInactive', required: false, type: Boolean })
|
||||
findAll(@Query('includeInactive') includeInactive?: string) {
|
||||
return this.sectionsService.findAll(includeInactive === 'true');
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get a profile section by ID with fields (Admin only)' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.sectionsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update a profile section (Admin only)' })
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateSectionDto: UpdateSectionDto,
|
||||
) {
|
||||
return this.sectionsService.update(id, updateSectionDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Delete a profile section (Admin only)' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.sectionsService.remove(id);
|
||||
}
|
||||
|
||||
@Post(':id/assign')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Assign section to an agent type (Admin only)' })
|
||||
assignToAgentType(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() assignDto: AssignSectionDto,
|
||||
) {
|
||||
return this.sectionsService.assignToAgentType(id, assignDto);
|
||||
}
|
||||
|
||||
@Patch(':id/assignment/:agentTypeId')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update section assignment (Admin only)' })
|
||||
updateAssignment(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('agentTypeId', ParseUUIDPipe) agentTypeId: string,
|
||||
@Body() updateDto: UpdateAssignmentDto,
|
||||
) {
|
||||
return this.sectionsService.updateAssignment(id, agentTypeId, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id/assignment/:agentTypeId')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Remove section from an agent type (Admin only)' })
|
||||
removeFromAgentType(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('agentTypeId', ParseUUIDPipe) agentTypeId: string,
|
||||
) {
|
||||
return this.sectionsService.removeFromAgentType(id, agentTypeId);
|
||||
}
|
||||
}
|
||||
310
src/profile-fields/profile-sections.service.ts
Normal file
310
src/profile-fields/profile-sections.service.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma';
|
||||
import { CreateSectionDto, UpdateSectionDto, AssignSectionDto, UpdateAssignmentDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class ProfileSectionsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Generate a URL-friendly slug from the name
|
||||
*/
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new profile section
|
||||
*/
|
||||
async create(createSectionDto: CreateSectionDto) {
|
||||
const slug = this.generateSlug(createSectionDto.name);
|
||||
|
||||
// Check if slug already exists
|
||||
const existing = await this.prisma.profileSection.findUnique({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('A section with this name already exists');
|
||||
}
|
||||
|
||||
return this.prisma.profileSection.create({
|
||||
data: {
|
||||
...createSectionDto,
|
||||
slug,
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: { fields: true, agentTypeSections: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all profile sections
|
||||
*/
|
||||
async findAll(includeInactive = false) {
|
||||
const where = includeInactive ? {} : { isActive: true };
|
||||
|
||||
return this.prisma.profileSection.findMany({
|
||||
where,
|
||||
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
|
||||
include: {
|
||||
_count: {
|
||||
select: { fields: true, agentTypeSections: true },
|
||||
},
|
||||
agentTypeSections: {
|
||||
include: {
|
||||
agentType: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single section by ID with all fields
|
||||
*/
|
||||
async findOne(id: string) {
|
||||
const section = await this.prisma.profileSection.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
fields: {
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
agentTypeSections: {
|
||||
include: {
|
||||
agentType: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
_count: {
|
||||
select: { fields: true, agentTypeSections: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!section) {
|
||||
throw new NotFoundException(`Section with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a section
|
||||
*/
|
||||
async update(id: string, updateSectionDto: UpdateSectionDto) {
|
||||
// Check if section exists
|
||||
await this.findOne(id);
|
||||
|
||||
const data: any = { ...updateSectionDto };
|
||||
|
||||
// If name is being updated, update the slug too
|
||||
if (updateSectionDto.name) {
|
||||
const newSlug = this.generateSlug(updateSectionDto.name);
|
||||
|
||||
// Check if new slug conflicts with another section
|
||||
const existing = await this.prisma.profileSection.findFirst({
|
||||
where: {
|
||||
AND: [{ id: { not: id } }, { slug: newSlug }],
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('A section with this name already exists');
|
||||
}
|
||||
|
||||
data.slug = newSlug;
|
||||
}
|
||||
|
||||
return this.prisma.profileSection.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
_count: {
|
||||
select: { fields: true, agentTypeSections: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a section
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const section = await this.findOne(id);
|
||||
|
||||
// Check if section has any fields
|
||||
if (section._count.fields > 0) {
|
||||
throw new ConflictException(
|
||||
`Cannot delete section. It has ${section._count.fields} field(s). Delete the fields first.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.profileSection.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a section to an agent type
|
||||
*/
|
||||
async assignToAgentType(sectionId: string, assignDto: AssignSectionDto) {
|
||||
// Verify section exists
|
||||
await this.findOne(sectionId);
|
||||
|
||||
// Verify agent type exists
|
||||
const agentType = await this.prisma.agentType.findUnique({
|
||||
where: { id: assignDto.agentTypeId },
|
||||
});
|
||||
|
||||
if (!agentType) {
|
||||
throw new NotFoundException(`Agent type with ID ${assignDto.agentTypeId} not found`);
|
||||
}
|
||||
|
||||
// Check if already assigned
|
||||
const existing = await this.prisma.agentTypeSection.findUnique({
|
||||
where: {
|
||||
agentTypeId_sectionId: {
|
||||
agentTypeId: assignDto.agentTypeId,
|
||||
sectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('This section is already assigned to this agent type');
|
||||
}
|
||||
|
||||
return this.prisma.agentTypeSection.create({
|
||||
data: {
|
||||
sectionId,
|
||||
agentTypeId: assignDto.agentTypeId,
|
||||
sortOrder: assignDto.sortOrder ?? 0,
|
||||
isRequired: assignDto.isRequired ?? false,
|
||||
},
|
||||
include: {
|
||||
section: true,
|
||||
agentType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a section assignment
|
||||
*/
|
||||
async updateAssignment(sectionId: string, agentTypeId: string, updateDto: UpdateAssignmentDto) {
|
||||
const assignment = await this.prisma.agentTypeSection.findUnique({
|
||||
where: {
|
||||
agentTypeId_sectionId: {
|
||||
agentTypeId,
|
||||
sectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!assignment) {
|
||||
throw new NotFoundException('Section assignment not found');
|
||||
}
|
||||
|
||||
return this.prisma.agentTypeSection.update({
|
||||
where: { id: assignment.id },
|
||||
data: updateDto,
|
||||
include: {
|
||||
section: true,
|
||||
agentType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a section from an agent type
|
||||
*/
|
||||
async removeFromAgentType(sectionId: string, agentTypeId: string) {
|
||||
const assignment = await this.prisma.agentTypeSection.findUnique({
|
||||
where: {
|
||||
agentTypeId_sectionId: {
|
||||
agentTypeId,
|
||||
sectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!assignment) {
|
||||
throw new NotFoundException('Section assignment not found');
|
||||
}
|
||||
|
||||
return this.prisma.agentTypeSection.delete({
|
||||
where: { id: assignment.id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sections for a specific agent type
|
||||
*/
|
||||
async getSectionsForAgentType(agentTypeId: string, includeFields = true) {
|
||||
const agentType = await this.prisma.agentType.findUnique({
|
||||
where: { id: agentTypeId },
|
||||
});
|
||||
|
||||
if (!agentType) {
|
||||
throw new NotFoundException(`Agent type with ID ${agentTypeId} not found`);
|
||||
}
|
||||
|
||||
// Get assigned sections
|
||||
const assignments = await this.prisma.agentTypeSection.findMany({
|
||||
where: { agentTypeId },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
include: {
|
||||
section: {
|
||||
include: includeFields
|
||||
? {
|
||||
fields: {
|
||||
where: { isActive: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Also get global sections
|
||||
const globalSections = await this.prisma.profileSection.findMany({
|
||||
where: {
|
||||
isGlobal: true,
|
||||
isActive: true,
|
||||
},
|
||||
include: includeFields
|
||||
? {
|
||||
fields: {
|
||||
where: { isActive: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
|
||||
return {
|
||||
agentType,
|
||||
sections: [
|
||||
...globalSections.map((s) => ({ ...s, isRequired: false, isGlobal: true })),
|
||||
...assignments.map((a) => ({
|
||||
...a.section,
|
||||
isRequired: a.isRequired,
|
||||
isGlobal: false,
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user