Files
backend/src/profile-fields/profile-sections.service.ts

410 lines
10 KiB
TypeScript
Raw Normal View History

import { Injectable, NotFoundException, ConflictException, ForbiddenException } from '@nestjs/common';
2026-01-24 03:33:54 +05:30
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);
}
2026-01-24 03:33:54 +05:30
if (effectiveSlug) {
// Check if slug conflicts with another section
2026-01-24 03:33:54 +05:30
const existing = await this.prisma.profileSection.findFirst({
where: {
AND: [{ id: { not: id } }, { slug: effectiveSlug }],
2026-01-24 03:33:54 +05:30
},
});
if (existing) {
throw new ConflictException('A section with this slug already exists');
2026-01-24 03:33:54 +05:30
}
data.slug = effectiveSlug;
2026-01-24 03:33:54 +05:30
}
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.',
);
}
2026-01-24 03:33:54 +05:30
// 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
2026-01-24 03:33:54 +05:30
*/
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)
2026-01-24 03:33:54 +05:30
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
2026-01-24 03:33:54 +05:30
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);
2026-01-24 03:33:54 +05:30
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,
2026-01-24 03:33:54 +05:30
};
}
}