import { Injectable, NotFoundException, ConflictException, ForbiddenException } 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 }; // Resolve effective slug: explicit slug from DTO wins; otherwise regenerate from name if name changed let effectiveSlug: string | undefined; if (updateSectionDto.slug) { effectiveSlug = updateSectionDto.slug; } else if (updateSectionDto.name) { effectiveSlug = this.generateSlug(updateSectionDto.name); } if (effectiveSlug) { // Check if slug conflicts with another section const existing = await this.prisma.profileSection.findFirst({ where: { AND: [{ id: { not: id } }, { slug: effectiveSlug }], }, }); if (existing) { throw new ConflictException('A section with this slug already exists'); } data.slug = effectiveSlug; } 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 this is a system section if (section.isSystem) { throw new ForbiddenException( 'Cannot delete system sections. You can only deactivate them.', ); } // 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 with custom ordering support */ 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 ALL AgentTypeSection entries for this agent type (including global sections) const assignments = await this.prisma.agentTypeSection.findMany({ where: { agentTypeId }, include: { section: { include: includeFields ? { fields: { where: { isActive: true }, orderBy: { sortOrder: 'asc' }, }, } : undefined, }, }, }); // Create a map of section IDs that have custom assignments const assignmentMap = new Map(assignments.map((a) => [a.sectionId, a])); // Get all global sections const globalSections = await this.prisma.profileSection.findMany({ where: { isGlobal: true, isActive: true, }, include: includeFields ? { fields: { where: { isActive: true }, orderBy: { sortOrder: 'asc' }, }, } : undefined, }); // Combine all sections with their effective sort order const allSections: any[] = []; // Add global sections (with custom order if assigned, otherwise use default) for (const section of globalSections) { const assignment = assignmentMap.get(section.id); allSections.push({ ...section, isRequired: assignment?.isRequired ?? false, isGlobal: true, effectiveSortOrder: assignment?.sortOrder ?? section.sortOrder, hasCustomOrder: !!assignment, }); } // Add non-global assigned sections for (const assignment of assignments) { if (!assignment.section.isGlobal) { allSections.push({ ...assignment.section, isRequired: assignment.isRequired, isGlobal: false, effectiveSortOrder: assignment.sortOrder, hasCustomOrder: true, }); } } // Sort by effective sort order allSections.sort((a, b) => a.effectiveSortOrder - b.effectiveSortOrder); return { agentType, sections: allSections, }; } /** * Update section ordering for a specific agent type * Creates or updates AgentTypeSection entries with custom sortOrder */ async updateSectionOrderForAgentType( agentTypeId: string, sectionOrders: { sectionId: string; sortOrder: number; isRequired?: boolean }[], ) { // Verify agent type exists const agentType = await this.prisma.agentType.findUnique({ where: { id: agentTypeId }, }); if (!agentType) { throw new NotFoundException(`Agent type with ID ${agentTypeId} not found`); } // Process each section order update const results = await Promise.all( sectionOrders.map(async ({ sectionId, sortOrder, isRequired }) => { // Check if assignment exists const existing = await this.prisma.agentTypeSection.findUnique({ where: { agentTypeId_sectionId: { agentTypeId, sectionId, }, }, }); if (existing) { // Update existing assignment return this.prisma.agentTypeSection.update({ where: { id: existing.id }, data: { sortOrder, ...(isRequired !== undefined && { isRequired }), }, include: { section: true }, }); } else { // Create new assignment (for global sections that need custom ordering) return this.prisma.agentTypeSection.create({ data: { agentTypeId, sectionId, sortOrder, isRequired: isRequired ?? false, }, include: { section: true }, }); } }), ); return { success: true, updated: results.length, assignments: results, }; } }