2026-01-11 20:59:12 +05:30
|
|
|
import {
|
|
|
|
|
Injectable,
|
|
|
|
|
NotFoundException,
|
|
|
|
|
ConflictException,
|
2026-02-09 00:33:54 +05:30
|
|
|
BadRequestException,
|
2026-01-11 20:59:12 +05:30
|
|
|
} from '@nestjs/common';
|
|
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
|
import {
|
|
|
|
|
CreateAgentProfileDto,
|
|
|
|
|
UpdateAgentProfileDto,
|
|
|
|
|
SearchAgentsDto,
|
2026-01-24 21:06:08 +05:30
|
|
|
SaveFieldValuesDto,
|
2026-01-11 20:59:12 +05:30
|
|
|
} from './dto';
|
2026-01-28 13:48:14 +05:30
|
|
|
import { Prisma, Prisma as PrismaTypes, VerificationStatus } 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: {
|
2026-01-20 12:16:01 +05:30
|
|
|
...dto,
|
2026-01-11 20:59:12 +05:30
|
|
|
userId,
|
|
|
|
|
slug,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
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 },
|
2026-01-20 12:16:01 +05:30
|
|
|
data: dto,
|
2026-01-11 20:59:12 +05:30
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
agentType: true,
|
2026-01-11 20:59:12 +05:30
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedProfile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async searchAgents(dto: SearchAgentsDto) {
|
|
|
|
|
const {
|
|
|
|
|
search,
|
|
|
|
|
city,
|
|
|
|
|
state,
|
|
|
|
|
country,
|
2026-01-20 12:16:01 +05:30
|
|
|
agentTypeId,
|
2026-01-11 20:59:12 +05:30
|
|
|
isVerified,
|
|
|
|
|
page = 1,
|
|
|
|
|
limit = 10,
|
|
|
|
|
sortBy = 'createdAt',
|
|
|
|
|
sortOrder = 'desc',
|
2026-02-01 18:30:14 +05:30
|
|
|
filters,
|
2026-01-11 20:59:12 +05:30
|
|
|
} = 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 12:16:01 +05:30
|
|
|
// Filter by agent type
|
|
|
|
|
if (agentTypeId) {
|
|
|
|
|
where.agentTypeId = agentTypeId;
|
2026-01-11 20:59:12 +05:30
|
|
|
}
|
|
|
|
|
|
2026-02-01 18:30:14 +05:30
|
|
|
// Process dynamic profile field filters
|
|
|
|
|
if (filters) {
|
|
|
|
|
try {
|
|
|
|
|
const parsedFilters: Record<string, string[]> = JSON.parse(filters);
|
|
|
|
|
const filterSlugs = Object.keys(parsedFilters);
|
|
|
|
|
|
|
|
|
|
if (filterSlugs.length > 0) {
|
|
|
|
|
// Get field IDs for the filter slugs
|
|
|
|
|
const fields = await this.prisma.profileField.findMany({
|
|
|
|
|
where: { slug: { in: filterSlugs } },
|
|
|
|
|
select: { id: true, slug: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const slugToFieldId = new Map(fields.map((f) => [f.slug, f.id]));
|
|
|
|
|
|
|
|
|
|
// For each filter, find agents that have ANY of the selected values
|
|
|
|
|
// Use AND logic between different fields
|
|
|
|
|
const filterConditions: string[][] = [];
|
|
|
|
|
|
|
|
|
|
for (const [slug, selectedValues] of Object.entries(parsedFilters)) {
|
|
|
|
|
const fieldId = slugToFieldId.get(slug);
|
|
|
|
|
if (!fieldId || !selectedValues || selectedValues.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
// Find agentProfileIds where jsonValue contains ANY of the selected values
|
|
|
|
|
const matchingValues = await this.prisma.agentProfileFieldValue.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
fieldId,
|
|
|
|
|
// jsonValue contains any of the selected values (for MULTI_SELECT, CHECKBOX_GROUP)
|
|
|
|
|
// We need to check if the jsonValue array contains any of the selected values
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
agentProfileId: true,
|
|
|
|
|
jsonValue: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Filter in memory - check if jsonValue array contains any selected value
|
|
|
|
|
const matchingAgentIds = matchingValues
|
|
|
|
|
.filter((fv) => {
|
|
|
|
|
const jsonVal = fv.jsonValue as string[] | null;
|
|
|
|
|
if (!jsonVal || !Array.isArray(jsonVal)) return false;
|
|
|
|
|
return selectedValues.some((sv) => jsonVal.includes(sv));
|
|
|
|
|
})
|
|
|
|
|
.map((fv) => fv.agentProfileId);
|
|
|
|
|
|
|
|
|
|
filterConditions.push(matchingAgentIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AND all filter conditions - find intersection of all matching agent IDs
|
|
|
|
|
if (filterConditions.length > 0) {
|
|
|
|
|
const intersection = filterConditions.reduce((acc, curr) => {
|
|
|
|
|
const currSet = new Set(curr);
|
|
|
|
|
return acc.filter((id) => currSet.has(id));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If no agents match all filters, return empty result
|
|
|
|
|
if (intersection.length === 0) {
|
|
|
|
|
return {
|
|
|
|
|
data: [],
|
|
|
|
|
meta: {
|
|
|
|
|
total: 0,
|
|
|
|
|
page,
|
|
|
|
|
limit,
|
|
|
|
|
totalPages: 0,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
where.id = { in: intersection };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Invalid JSON, ignore filters
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
agentType: true,
|
2026-02-01 22:46:08 +05:30
|
|
|
fieldValues: {
|
|
|
|
|
include: {
|
|
|
|
|
field: {
|
|
|
|
|
select: {
|
|
|
|
|
slug: true,
|
|
|
|
|
name: true,
|
|
|
|
|
fieldType: 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,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
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 },
|
2026-01-20 12:16:01 +05:30
|
|
|
data: dto,
|
2026-01-11 20:59:12 +05:30
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-20 12:16:01 +05:30
|
|
|
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' };
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 13:48:14 +05:30
|
|
|
async adminVerifyAgent(
|
|
|
|
|
profileId: string,
|
|
|
|
|
data: {
|
|
|
|
|
status: VerificationStatus;
|
|
|
|
|
note?: string;
|
|
|
|
|
adminId: string;
|
|
|
|
|
},
|
|
|
|
|
) {
|
2026-01-11 20:59:12 +05:30
|
|
|
const profile = await this.prisma.agentProfile.findUnique({
|
|
|
|
|
where: { id: profileId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!profile) {
|
|
|
|
|
throw new NotFoundException('Agent profile not found');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 13:48:14 +05:30
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:59:12 +05:30
|
|
|
const updatedProfile = await this.prisma.agentProfile.update({
|
|
|
|
|
where: { id: profileId },
|
2026-01-28 13:48:14 +05:30
|
|
|
data: updateData,
|
2026-01-11 20:59:12 +05:30
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedProfile;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 13:48:14 +05:30
|
|
|
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 || [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:59:12 +05:30
|
|
|
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);
|
|
|
|
|
}
|
2026-01-24 21:06:08 +05:30
|
|
|
|
|
|
|
|
// 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
|
2026-01-24 21:06:08 +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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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':
|
2026-01-24 21:06:08 +05:30
|
|
|
// 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':
|
2026-01-24 21:06:08 +05:30
|
|
|
return fieldValue.jsonValue;
|
|
|
|
|
default:
|
|
|
|
|
// Return first non-null value
|
|
|
|
|
return (
|
|
|
|
|
fieldValue.jsonValue ??
|
|
|
|
|
fieldValue.textValue ??
|
|
|
|
|
fieldValue.numberValue ??
|
|
|
|
|
fieldValue.booleanValue ??
|
|
|
|
|
fieldValue.dateValue?.toISOString()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 00:33:54 +05:30
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// NOTIFICATION PREFERENCES
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async getNotificationPreferences(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { notificationPreferences: true, role: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== 'AGENT') {
|
|
|
|
|
throw new BadRequestException('This endpoint is for agents only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return preferences or default empty object
|
|
|
|
|
return user.notificationPreferences || { email: {}, push: {} };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateNotificationPreferences(
|
|
|
|
|
userId: string,
|
|
|
|
|
preferences: { email?: Record<string, boolean>; push?: Record<string, boolean> },
|
|
|
|
|
) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { role: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== 'AGENT') {
|
|
|
|
|
throw new BadRequestException('This endpoint is for agents only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedUser = await this.prisma.user.update({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
data: {
|
|
|
|
|
notificationPreferences: preferences,
|
|
|
|
|
},
|
|
|
|
|
select: { notificationPreferences: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedUser.notificationPreferences;
|
|
|
|
|
}
|
2026-02-09 00:55:55 +05:30
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// PRIVACY PREFERENCES
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async getPrivacyPreferences(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { privacyPreferences: true, role: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== 'AGENT') {
|
|
|
|
|
throw new BadRequestException('This endpoint is for agents only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return preferences or default empty object
|
|
|
|
|
return user.privacyPreferences || { privacySettings: {}, dataSettings: {} };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updatePrivacyPreferences(
|
|
|
|
|
userId: string,
|
|
|
|
|
preferences: {
|
|
|
|
|
privacySettings?: Record<string, string>;
|
|
|
|
|
dataSettings?: Record<string, boolean>;
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { role: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== 'AGENT') {
|
|
|
|
|
throw new BadRequestException('This endpoint is for agents only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedUser = await this.prisma.user.update({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
data: {
|
|
|
|
|
privacyPreferences: preferences,
|
|
|
|
|
},
|
|
|
|
|
select: { privacyPreferences: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedUser.privacyPreferences;
|
|
|
|
|
}
|
2026-01-11 20:59:12 +05:30
|
|
|
}
|