510 lines
12 KiB
TypeScript
510 lines
12 KiB
TypeScript
|
|
import {
|
||
|
|
Injectable,
|
||
|
|
NotFoundException,
|
||
|
|
ForbiddenException,
|
||
|
|
ConflictException,
|
||
|
|
} from '@nestjs/common';
|
||
|
|
import { PrismaService } from '../prisma/prisma.service';
|
||
|
|
import {
|
||
|
|
CreateAgentProfileDto,
|
||
|
|
UpdateAgentProfileDto,
|
||
|
|
SearchAgentsDto,
|
||
|
|
} from './dto';
|
||
|
|
import { Prisma } 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 { specializationIds, ...profileData } = dto;
|
||
|
|
const slug = generateSlug(dto.firstName, dto.lastName);
|
||
|
|
|
||
|
|
const profile = await this.prisma.agentProfile.create({
|
||
|
|
data: {
|
||
|
|
...profileData,
|
||
|
|
userId,
|
||
|
|
slug,
|
||
|
|
specializations: specializationIds?.length
|
||
|
|
? {
|
||
|
|
create: specializationIds.map((specId) => ({
|
||
|
|
specializationId: specId,
|
||
|
|
})),
|
||
|
|
}
|
||
|
|
: undefined,
|
||
|
|
},
|
||
|
|
include: {
|
||
|
|
user: {
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
email: true,
|
||
|
|
avatar: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: 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,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
verificationRequests: {
|
||
|
|
orderBy: { createdAt: 'desc' },
|
||
|
|
take: 5,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
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,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: 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 { specializationIds, ...profileData } = dto;
|
||
|
|
|
||
|
|
// If specializationIds is provided, update the specializations
|
||
|
|
if (specializationIds !== undefined) {
|
||
|
|
// Delete existing specializations
|
||
|
|
await this.prisma.agentSpecialization.deleteMany({
|
||
|
|
where: { agentProfileId: profile.id },
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create new specializations
|
||
|
|
if (specializationIds.length > 0) {
|
||
|
|
await this.prisma.agentSpecialization.createMany({
|
||
|
|
data: specializationIds.map((specId) => ({
|
||
|
|
agentProfileId: profile.id,
|
||
|
|
specializationId: specId,
|
||
|
|
})),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const updatedProfile = await this.prisma.agentProfile.update({
|
||
|
|
where: { userId },
|
||
|
|
data: profileData,
|
||
|
|
include: {
|
||
|
|
user: {
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
email: true,
|
||
|
|
avatar: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
return updatedProfile;
|
||
|
|
}
|
||
|
|
|
||
|
|
async searchAgents(dto: SearchAgentsDto) {
|
||
|
|
const {
|
||
|
|
search,
|
||
|
|
city,
|
||
|
|
state,
|
||
|
|
country,
|
||
|
|
categoryId,
|
||
|
|
specializationIds,
|
||
|
|
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 category
|
||
|
|
if (categoryId) {
|
||
|
|
where.specializations = {
|
||
|
|
some: {
|
||
|
|
specialization: {
|
||
|
|
categoryId,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Filter by specializations
|
||
|
|
if (specializationIds?.length) {
|
||
|
|
where.specializations = {
|
||
|
|
some: {
|
||
|
|
specializationId: { in: specializationIds },
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: 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,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: 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,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: 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 { specializationIds, ...profileData } = dto;
|
||
|
|
|
||
|
|
if (specializationIds !== undefined) {
|
||
|
|
await this.prisma.agentSpecialization.deleteMany({
|
||
|
|
where: { agentProfileId: profileId },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (specializationIds.length > 0) {
|
||
|
|
await this.prisma.agentSpecialization.createMany({
|
||
|
|
data: specializationIds.map((specId) => ({
|
||
|
|
agentProfileId: profileId,
|
||
|
|
specializationId: specId,
|
||
|
|
})),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const updatedProfile = await this.prisma.agentProfile.update({
|
||
|
|
where: { id: profileId },
|
||
|
|
data: profileData,
|
||
|
|
include: {
|
||
|
|
user: {
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
email: true,
|
||
|
|
avatar: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
specializations: {
|
||
|
|
include: {
|
||
|
|
specialization: {
|
||
|
|
include: {
|
||
|
|
category: 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');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete related records first
|
||
|
|
await this.prisma.agentSpecialization.deleteMany({
|
||
|
|
where: { agentProfileId: profileId },
|
||
|
|
});
|
||
|
|
|
||
|
|
await this.prisma.verificationRequest.deleteMany({
|
||
|
|
where: { agentProfileId: profileId },
|
||
|
|
});
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|