import { Injectable, NotFoundException, ConflictException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateAgentProfileDto, UpdateAgentProfileDto, SearchAgentsDto, SaveFieldValuesDto, } from './dto'; import { Prisma, Prisma as PrismaTypes, VerificationStatus } from '@prisma/client'; function generateSlug(firstName: string, lastName: string): string { const base = `${firstName}-${lastName}` .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); const uniqueSuffix = Math.random().toString(36).substring(2, 8); return `${base}-${uniqueSuffix}`; } @Injectable() export class AgentsService { constructor(private prisma: PrismaService) {} async createProfile(userId: string, dto: CreateAgentProfileDto) { // Check if user already has an agent profile const existingProfile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (existingProfile) { throw new ConflictException('User already has an agent profile'); } const slug = generateSlug(dto.firstName, dto.lastName); const profile = await this.prisma.agentProfile.create({ data: { ...dto, userId, slug, }, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return profile; } async getProfile(userId: string) { const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } return profile; } async getProfileById(profileId: string) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } return profile; } async updateProfile(userId: string, dto: UpdateAgentProfileDto) { const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const updatedProfile = await this.prisma.agentProfile.update({ where: { userId }, data: dto, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return updatedProfile; } async searchAgents(dto: SearchAgentsDto) { const { search, city, state, country, agentTypeId, isVerified, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc', } = dto; const skip = (page - 1) * limit; // Build where clause const where: Prisma.AgentProfileWhereInput = {}; if (search) { where.OR = [ { firstName: { contains: search, mode: 'insensitive' } }, { lastName: { contains: search, mode: 'insensitive' } }, { bio: { contains: search, mode: 'insensitive' } }, { headline: { contains: search, mode: 'insensitive' } }, { companyName: { contains: search, mode: 'insensitive' } }, ]; } if (city) { where.city = { contains: city, mode: 'insensitive' }; } if (state) { where.state = { contains: state, mode: 'insensitive' }; } if (country) { where.country = { contains: country, mode: 'insensitive' }; } if (isVerified !== undefined) { where.isVerified = isVerified; } // Filter by agent type if (agentTypeId) { where.agentTypeId = agentTypeId; } // Build orderBy const orderBy: Prisma.AgentProfileOrderByWithRelationInput = {}; if (sortBy === 'averageRating') { orderBy.averageRating = sortOrder; } else if (sortBy === 'totalReviews') { orderBy.totalReviews = sortOrder; } else if (sortBy === 'firstName') { orderBy.firstName = sortOrder; } else { orderBy.createdAt = sortOrder; } const [agents, total] = await Promise.all([ this.prisma.agentProfile.findMany({ where, skip, take: limit, orderBy, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }), this.prisma.agentProfile.count({ where }), ]); return { data: agents, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } async getFeaturedAgents(limit = 10) { const agents = await this.prisma.agentProfile.findMany({ where: { isVerified: true, isFeatured: true, }, take: limit, orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }], include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return agents; } async getTopRatedAgents(limit = 10) { const agents = await this.prisma.agentProfile.findMany({ where: { isVerified: true, totalReviews: { gte: 5 }, }, take: limit, orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }], include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return agents; } // Admin methods async adminUpdateProfile(profileId: string, dto: UpdateAgentProfileDto) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const updatedProfile = await this.prisma.agentProfile.update({ where: { id: profileId }, data: dto, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return updatedProfile; } async adminDeleteProfile(profileId: string) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } await this.prisma.agentProfile.delete({ where: { id: profileId }, }); return { message: 'Agent profile deleted successfully' }; } async adminVerifyAgent( profileId: string, data: { status: VerificationStatus; note?: string; adminId: string; }, ) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const updateData: Prisma.AgentProfileUpdateInput = { verificationStatus: data.status, verificationNote: data.note || null, }; if (data.status === VerificationStatus.APPROVED) { updateData.isVerified = true; updateData.verifiedAt = new Date(); updateData.verifiedBy = data.adminId; } else if (data.status === VerificationStatus.REJECTED) { updateData.isVerified = false; updateData.verifiedAt = null; updateData.verifiedBy = null; } const updatedProfile = await this.prisma.agentProfile.update({ where: { id: profileId }, data: updateData, include: { user: { select: { id: true, email: true, avatar: true, }, }, }, }); return updatedProfile; } async getVerificationDocuments(agentProfileId: string) { // Find the upload-documents section field const field = await this.prisma.profileField.findFirst({ where: { section: { slug: 'upload-documents' }, slug: 'documents', }, }); if (!field) return []; const fieldValue = await this.prisma.agentProfileFieldValue.findUnique({ where: { agentProfileId_fieldId: { agentProfileId, fieldId: field.id }, }, }); return fieldValue?.jsonValue || []; } async adminSetFeatured(profileId: string, isFeatured: boolean) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const updatedProfile = await this.prisma.agentProfile.update({ where: { id: profileId }, data: { isFeatured }, include: { user: { select: { id: true, email: true, avatar: true, }, }, }, }); return updatedProfile; } async getAllAgentsAdmin(dto: SearchAgentsDto) { // Same as searchAgents but returns all data for admin return this.searchAgents(dto); } // Field Values methods async saveFieldValues(userId: string, dto: SaveFieldValuesDto) { // Get agent profile const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Process each field value const results: Array<{ id: string; agentProfileId: string; fieldId: string; textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: Prisma.JsonValue; dateValue: Date | null; createdAt: Date; updatedAt: Date; field: { id: string; slug: string; name: string; fieldType: string; }; }> = []; for (const fieldValue of dto.fieldValues) { // Find the field by slug const field = await this.prisma.profileField.findFirst({ where: { slug: fieldValue.fieldSlug }, include: { section: true }, }); if (!field) { // Skip fields that don't exist continue; } // Determine which column to use based on value type and field type const valueData = this.getValueData(field.fieldType, fieldValue.value); // Upsert the field value const result = await this.prisma.agentProfileFieldValue.upsert({ where: { agentProfileId_fieldId: { agentProfileId: profile.id, fieldId: field.id, }, }, create: { agentProfileId: profile.id, fieldId: field.id, ...valueData, }, update: valueData, include: { field: { select: { id: true, slug: true, name: true, fieldType: true, }, }, }, }); results.push(result); } return { message: 'Field values saved successfully', savedCount: results.length, data: results, }; } async getFieldValues(userId: string) { // Get agent profile const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Get all field values for this profile const fieldValues = await this.prisma.agentProfileFieldValue.findMany({ where: { agentProfileId: profile.id }, include: { field: { select: { id: true, slug: true, name: true, fieldType: true, section: { select: { id: true, slug: true, name: true, }, }, }, }, }, }); // Transform to a more usable format const transformedValues = fieldValues.map((fv) => ({ fieldSlug: fv.field.slug, fieldName: fv.field.name, fieldType: fv.field.fieldType, sectionSlug: fv.field.section.slug, sectionName: fv.field.section.name, value: this.extractValue(fv), })); return { agentProfileId: profile.id, fieldValues: transformedValues, }; } async getFieldValuesByAgentId(agentId: string) { // Get agent profile by ID const profile = await this.prisma.agentProfile.findUnique({ where: { id: agentId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Get all field values for this profile const fieldValues = await this.prisma.agentProfileFieldValue.findMany({ where: { agentProfileId: profile.id }, include: { field: { select: { id: true, slug: true, name: true, fieldType: true, section: { select: { id: true, slug: true, name: true, }, }, }, }, }, }); // Transform to a more usable format const transformedValues = fieldValues.map((fv) => ({ fieldSlug: fv.field.slug, fieldName: fv.field.name, fieldType: fv.field.fieldType, sectionSlug: fv.field.section.slug, sectionName: fv.field.section.name, value: this.extractValue(fv), })); return { agentProfileId: profile.id, fieldValues: transformedValues, }; } private getValueData( fieldType: string, value: string | number | boolean | string[] | Record | null, ): { textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: PrismaTypes.InputJsonValue | typeof PrismaTypes.DbNull; dateValue: Date | null; } { // Reset all value columns const valueData: { textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: PrismaTypes.InputJsonValue | typeof PrismaTypes.DbNull; dateValue: Date | null; } = { textValue: null, numberValue: null, booleanValue: null, jsonValue: PrismaTypes.DbNull, dateValue: null, }; if (value === null || value === undefined) { return valueData; } switch (fieldType) { case 'TEXT': case 'TEXTAREA': valueData.textValue = String(value); break; case 'NUMBER': case 'RANGE': valueData.numberValue = typeof value === 'number' ? value : parseFloat(String(value)); break; case 'CHECKBOX': valueData.booleanValue = typeof value === 'boolean' ? value : value === 'true'; break; case 'DATE': valueData.dateValue = new Date(String(value)); break; case 'SELECT': case 'RADIO': // Single selection - store as text valueData.textValue = String(value); break; case 'MULTI_SELECT': case 'CHECKBOX_GROUP': case 'TAG_INPUT': case 'FILE': case 'FILE_UPLOAD': case 'REPEATER': // Arrays and complex objects - store as JSON valueData.jsonValue = value as PrismaTypes.InputJsonValue; break; default: // Default to JSON for unknown types if (typeof value === 'object') { valueData.jsonValue = value as PrismaTypes.InputJsonValue; } else { valueData.textValue = String(value); } } return valueData; } private extractValue(fieldValue: { textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: unknown; dateValue: Date | null; field: { fieldType: string }; }): unknown { const { fieldType } = fieldValue.field; switch (fieldType) { case 'TEXT': case 'TEXTAREA': case 'SELECT': case 'RADIO': return fieldValue.textValue; case 'NUMBER': case 'RANGE': return fieldValue.numberValue; case 'CHECKBOX': return fieldValue.booleanValue; case 'DATE': return fieldValue.dateValue?.toISOString(); case 'MULTI_SELECT': case 'CHECKBOX_GROUP': case 'TAG_INPUT': case 'FILE': case 'FILE_UPLOAD': case 'REPEATER': return fieldValue.jsonValue; default: // Return first non-null value return ( fieldValue.jsonValue ?? fieldValue.textValue ?? fieldValue.numberValue ?? fieldValue.booleanValue ?? fieldValue.dateValue?.toISOString() ); } } }