Files
backend/src/agents/agents.service.ts

663 lines
16 KiB
TypeScript
Raw Normal View History

2026-01-11 20:59:12 +05:30
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateAgentProfileDto,
UpdateAgentProfileDto,
SearchAgentsDto,
SaveFieldValuesDto,
2026-01-11 20:59:12 +05:30
} from './dto';
import { Prisma, Prisma as PrismaTypes } from '@prisma/client';
2026-01-11 20:59:12 +05:30
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,
2026-01-11 20:59:12 +05:30
userId,
slug,
},
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
return updatedProfile;
}
async searchAgents(dto: SearchAgentsDto) {
const {
search,
city,
state,
country,
agentTypeId,
2026-01-11 20:59:12 +05:30
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;
2026-01-11 20:59:12 +05:30
}
// 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,
2026-01-11 20:59:12 +05:30
},
}),
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,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
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, isVerified: 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: { isVerified },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
},
});
return updatedProfile;
}
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
2026-01-24 23:10:05 +05:30
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<string, unknown> | 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':
2026-01-24 22:31:12 +05:30
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':
2026-01-24 22:31:12 +05:30
case 'REPEATER':
return fieldValue.jsonValue;
default:
// Return first non-null value
return (
fieldValue.jsonValue ??
fieldValue.textValue ??
fieldValue.numberValue ??
fieldValue.booleanValue ??
fieldValue.dateValue?.toISOString()
);
}
}
2026-01-11 20:59:12 +05:30
}