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');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 02:04:12 +05:30
|
|
|
// Filter out fields that don't exist in the database schema
|
|
|
|
|
// serviceAreas is accepted by DTO but stored in field values, not direct column
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
const { serviceAreas, ...profileData } = dto as UpdateAgentProfileDto & {
|
|
|
|
|
serviceAreas?: string[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-11 20:59:12 +05:30
|
|
|
const updatedProfile = await this.prisma.agentProfile.update({
|
|
|
|
|
where: { userId },
|
2026-02-09 02:04:12 +05:30
|
|
|
data: profileData,
|
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
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-09 02:04:12 +05:30
|
|
|
// Sync phone to field values if phone was updated
|
|
|
|
|
// This keeps AgentProfile.phone and AgentProfileFieldValue in sync
|
|
|
|
|
if (dto.phone !== undefined) {
|
|
|
|
|
await this.syncPhoneToFieldValues(profile.id, dto.phone);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:59:12 +05:30
|
|
|
return updatedProfile;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 02:04:12 +05:30
|
|
|
/**
|
|
|
|
|
* Sync phone from AgentProfile to AgentProfileFieldValue
|
|
|
|
|
* This ensures both storage locations have the same phone value
|
|
|
|
|
*/
|
|
|
|
|
private async syncPhoneToFieldValues(
|
|
|
|
|
agentProfileId: string,
|
|
|
|
|
phone: string | null,
|
|
|
|
|
) {
|
|
|
|
|
// Find any phone-related field definitions
|
|
|
|
|
const phoneField = await this.prisma.profileField.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
slug: { in: ['phone', 'phone_number', 'cell_number', 'office_number'] },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (phoneField) {
|
|
|
|
|
await this.prisma.agentProfileFieldValue.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
agentProfileId_fieldId: {
|
|
|
|
|
agentProfileId,
|
|
|
|
|
fieldId: phoneField.id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
agentProfileId,
|
|
|
|
|
fieldId: phoneField.id,
|
|
|
|
|
textValue: phone,
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
textValue: phone,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:59:12 +05:30
|
|
|
async searchAgents(dto: SearchAgentsDto) {
|
|
|
|
|
const {
|
|
|
|
|
search,
|
2026-03-04 20:49:37 +05:30
|
|
|
location,
|
2026-01-11 20:59:12 +05:30
|
|
|
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' } },
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 20:49:37 +05:30
|
|
|
// General location search - searches across legacy columns AND dynamic field values
|
|
|
|
|
if (location) {
|
|
|
|
|
// Find agent IDs that have matching city/state in dynamic field values
|
|
|
|
|
// City and State are CHECKBOX_GROUP fields stored as JSON arrays in jsonValue
|
|
|
|
|
const locationFields = await this.prisma.profileField.findMany({
|
|
|
|
|
where: { slug: { in: ['city', 'state', 'service_areas'] } },
|
|
|
|
|
select: { id: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const fieldIds = locationFields.map((f) => f.id);
|
|
|
|
|
|
|
|
|
|
let matchingAgentIds: string[] = [];
|
|
|
|
|
if (fieldIds.length > 0) {
|
|
|
|
|
// Search jsonValue (JSON arrays) and textValue using raw SQL for case-insensitive JSON content matching
|
|
|
|
|
const locationLower = location.toLowerCase();
|
|
|
|
|
// Also match snake_case version (e.g., "New York" -> "new_york")
|
|
|
|
|
const locationSnakeCase = locationLower.replace(/\s+/g, '_');
|
|
|
|
|
|
|
|
|
|
const matchingFieldValues =
|
|
|
|
|
await this.prisma.agentProfileFieldValue.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
fieldId: { in: fieldIds },
|
|
|
|
|
OR: [
|
|
|
|
|
{
|
|
|
|
|
textValue: { contains: location, mode: 'insensitive' },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
select: { agentProfileId: true, jsonValue: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Also fetch all field values with jsonValue for these fields to search within JSON arrays
|
|
|
|
|
const jsonFieldValues =
|
|
|
|
|
await this.prisma.agentProfileFieldValue.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
fieldId: { in: fieldIds },
|
|
|
|
|
jsonValue: { not: Prisma.JsonNull },
|
|
|
|
|
},
|
|
|
|
|
select: { agentProfileId: true, jsonValue: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Filter JSON array values that contain the location string
|
|
|
|
|
const jsonMatchIds = jsonFieldValues
|
|
|
|
|
.filter((fv) => {
|
|
|
|
|
const val = fv.jsonValue;
|
|
|
|
|
if (Array.isArray(val)) {
|
|
|
|
|
return val.some((v: unknown) => {
|
|
|
|
|
if (typeof v !== 'string') return false;
|
|
|
|
|
const vLower = v.toLowerCase();
|
|
|
|
|
return (
|
|
|
|
|
vLower.includes(locationLower) ||
|
|
|
|
|
vLower.includes(locationSnakeCase) ||
|
|
|
|
|
locationLower.includes(vLower) ||
|
|
|
|
|
locationSnakeCase.includes(vLower)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (typeof val === 'string') {
|
|
|
|
|
return val.toLowerCase().includes(locationLower);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
})
|
|
|
|
|
.map((fv) => fv.agentProfileId);
|
|
|
|
|
|
|
|
|
|
const textMatchIds = matchingFieldValues.map(
|
|
|
|
|
(fv) => fv.agentProfileId,
|
|
|
|
|
);
|
|
|
|
|
matchingAgentIds = [
|
|
|
|
|
...new Set([...textMatchIds, ...jsonMatchIds]),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search legacy columns OR dynamic field values
|
|
|
|
|
where.OR = [
|
|
|
|
|
...(where.OR || []),
|
|
|
|
|
{ city: { contains: location, mode: 'insensitive' } },
|
|
|
|
|
{ state: { contains: location, mode: 'insensitive' } },
|
|
|
|
|
{ country: { contains: location, mode: 'insensitive' } },
|
|
|
|
|
...(matchingAgentIds.length > 0
|
|
|
|
|
? [{ id: { in: matchingAgentIds } }]
|
|
|
|
|
: []),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:59:12 +05:30
|
|
|
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) {
|
2026-03-04 22:11:48 +05:30
|
|
|
// Get field IDs for the filter slugs
|
2026-02-01 18:30:14 +05:30
|
|
|
const fields = await this.prisma.profileField.findMany({
|
|
|
|
|
where: { slug: { in: filterSlugs } },
|
2026-02-24 21:45:39 +05:30
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
slug: true,
|
|
|
|
|
},
|
2026-02-01 18:30:14 +05:30
|
|
|
});
|
|
|
|
|
|
2026-03-04 21:22:33 +05:30
|
|
|
// Group fields by slug (same slug can exist in multiple type-specific sections)
|
|
|
|
|
const slugToFields = new Map<string, typeof fields>();
|
|
|
|
|
for (const f of fields) {
|
|
|
|
|
const existing = slugToFields.get(f.slug) || [];
|
|
|
|
|
existing.push(f);
|
|
|
|
|
slugToFields.set(f.slug, existing);
|
|
|
|
|
}
|
2026-02-01 18:30:14 +05:30
|
|
|
|
|
|
|
|
// For each filter, find agents that have ANY of the selected values
|
2026-03-04 21:22:33 +05:30
|
|
|
// Use AND logic between different filter slugs, OR logic within same slug
|
2026-02-01 18:30:14 +05:30
|
|
|
const filterConditions: string[][] = [];
|
|
|
|
|
|
|
|
|
|
for (const [slug, selectedValues] of Object.entries(parsedFilters)) {
|
2026-03-04 21:22:33 +05:30
|
|
|
const slugFields = slugToFields.get(slug);
|
|
|
|
|
if (!slugFields || slugFields.length === 0 || !selectedValues || selectedValues.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
// Search across ALL fields with this slug (e.g. loan_type in both Professional and Lender sections)
|
|
|
|
|
const allMatchingAgentIds: string[] = [];
|
2026-03-04 22:11:48 +05:30
|
|
|
const fieldIds = slugFields.map((f) => f.id);
|
2026-03-04 21:22:33 +05:30
|
|
|
|
2026-03-04 22:11:48 +05:30
|
|
|
const matchingValues = await this.prisma.agentProfileFieldValue.findMany({
|
|
|
|
|
where: { fieldId: { in: fieldIds } },
|
|
|
|
|
select: {
|
|
|
|
|
agentProfileId: true,
|
|
|
|
|
jsonValue: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-03-04 21:22:33 +05:30
|
|
|
|
2026-03-04 22:11:48 +05:30
|
|
|
const matchingIds = 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);
|
2026-03-04 21:22:33 +05:30
|
|
|
|
2026-03-04 22:11:48 +05:30
|
|
|
allMatchingAgentIds.push(...matchingIds);
|
2026-03-04 21:22:33 +05:30
|
|
|
|
|
|
|
|
filterConditions.push([...new Set(allMatchingAgentIds)]);
|
2026-02-01 18:30:14 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2026-03-04 22:11:48 +05:30
|
|
|
// Find the field by slug, preferring the one from a section assigned to this agent's type
|
|
|
|
|
const candidateFields = await this.prisma.profileField.findMany({
|
2026-01-24 21:06:08 +05:30
|
|
|
where: { slug: fieldValue.fieldSlug },
|
2026-03-04 22:11:48 +05:30
|
|
|
include: {
|
|
|
|
|
section: {
|
|
|
|
|
include: {
|
|
|
|
|
agentTypeSections: { select: { agentTypeId: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-24 21:06:08 +05:30
|
|
|
});
|
|
|
|
|
|
2026-03-04 22:11:48 +05:30
|
|
|
if (candidateFields.length === 0) {
|
2026-01-24 21:06:08 +05:30
|
|
|
// Skip fields that don't exist
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 22:11:48 +05:30
|
|
|
// Pick the field from a section assigned to this agent's type, or global, or fallback to first
|
|
|
|
|
const field =
|
|
|
|
|
candidateFields.find(
|
|
|
|
|
(f) =>
|
|
|
|
|
f.section.agentTypeSections.some(
|
|
|
|
|
(ats) => ats.agentTypeId === profile.agentTypeId,
|
|
|
|
|
),
|
|
|
|
|
) ||
|
|
|
|
|
candidateFields.find((f) => f.section.isGlobal) ||
|
|
|
|
|
candidateFields[0];
|
|
|
|
|
|
2026-01-24 21:06:08 +05:30
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 02:04:12 +05:30
|
|
|
// Sync phone from field values to AgentProfile.phone
|
|
|
|
|
// This keeps both storage locations in sync
|
|
|
|
|
const phoneSlugs = ['phone', 'phone_number', 'cell_number', 'office_number'];
|
|
|
|
|
const phoneFieldValue = dto.fieldValues.find((fv) =>
|
|
|
|
|
phoneSlugs.includes(fv.fieldSlug),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (phoneFieldValue !== undefined) {
|
|
|
|
|
const phoneValue = phoneFieldValue.value;
|
|
|
|
|
await this.prisma.agentProfile.update({
|
|
|
|
|
where: { id: profile.id },
|
|
|
|
|
data: {
|
|
|
|
|
phone:
|
|
|
|
|
phoneValue === '' || phoneValue === null
|
|
|
|
|
? null
|
|
|
|
|
: String(phoneValue),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 21:06:08 +05:30
|
|
|
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-02-09 01:15:45 +05:30
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// DELETE ACCOUNT
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async deleteAccount(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { id: true, role: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== 'AGENT') {
|
|
|
|
|
throw new BadRequestException('This endpoint is for agents only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete in a transaction to ensure data consistency
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
// First, delete the agent profile and related data
|
|
|
|
|
const agentProfile = await tx.agentProfile.findUnique({
|
|
|
|
|
where: { userId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (agentProfile) {
|
|
|
|
|
// Delete agent profile field values
|
|
|
|
|
await tx.agentProfileFieldValue.deleteMany({
|
|
|
|
|
where: { agentProfileId: agentProfile.id },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Delete the agent profile
|
|
|
|
|
await tx.agentProfile.delete({
|
|
|
|
|
where: { userId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finally, delete the user account
|
|
|
|
|
await tx.user.delete({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { message: 'Account deleted successfully' };
|
|
|
|
|
}
|
2026-01-11 20:59:12 +05:30
|
|
|
}
|