2026-01-24 12:49:25 +05:30
|
|
|
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 };
|
|
|
|
|
|
2026-04-11 23:05:56 +05:30
|
|
|
// 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
|
|
|
|
2026-04-11 23:05:56 +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: {
|
2026-04-11 23:05:56 +05:30
|
|
|
AND: [{ id: { not: id } }, { slug: effectiveSlug }],
|
2026-01-24 03:33:54 +05:30
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
2026-04-11 23:05:56 +05:30
|
|
|
throw new ConflictException('A section with this slug already exists');
|
2026-01-24 03:33:54 +05:30
|
|
|
}
|
|
|
|
|
|
2026-04-11 23:05:56 +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);
|
|
|
|
|
|
2026-01-24 12:49:25 +05:30
|
|
|
// 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 },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-24 15:32:54 +05:30
|
|
|
* 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`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:32:54 +05:30
|
|
|
// 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,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-24 15:32:54 +05:30
|
|
|
// 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,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-24 15:32:54 +05:30
|
|
|
// 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,
|
2026-01-24 15:32:54 +05:30
|
|
|
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
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|