From 5844d178370b026c6e72a1464fdb89bf74f6d600 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sat, 24 Jan 2026 21:06:08 +0530 Subject: [PATCH] feat: Add dynamic field value management for agent profiles, including DTOs, controller endpoints, and service logic for saving and retrieving. --- src/agents/agents.controller.ts | 26 +++ src/agents/agents.service.ts | 244 +++++++++++++++++++++++- src/agents/dto/index.ts | 1 + src/agents/dto/save-field-values.dto.ts | 31 +++ 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/agents/dto/save-field-values.dto.ts diff --git a/src/agents/agents.controller.ts b/src/agents/agents.controller.ts index ad96858..c8c1594 100644 --- a/src/agents/agents.controller.ts +++ b/src/agents/agents.controller.ts @@ -21,6 +21,7 @@ import { CreateAgentProfileDto, UpdateAgentProfileDto, SearchAgentsDto, + SaveFieldValuesDto, } from './dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; @@ -117,6 +118,31 @@ export class AgentsController { return this.agentsService.updateProfile(userId, dto); } + @Put('profile/field-values') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.AGENT) + @ApiBearerAuth() + @ApiOperation({ summary: 'Save dynamic field values for agent profile' }) + @ApiResponse({ status: 200, description: 'Field values saved successfully' }) + @ApiResponse({ status: 404, description: 'Profile not found' }) + async saveFieldValues( + @CurrentUser('id') userId: string, + @Body() dto: SaveFieldValuesDto, + ) { + return this.agentsService.saveFieldValues(userId, dto); + } + + @Get('profile/field-values') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.AGENT) + @ApiBearerAuth() + @ApiOperation({ summary: 'Get dynamic field values for agent profile' }) + @ApiResponse({ status: 200, description: 'Field values retrieved successfully' }) + @ApiResponse({ status: 404, description: 'Profile not found' }) + async getFieldValues(@CurrentUser('id') userId: string) { + return this.agentsService.getFieldValues(userId); + } + // Admin endpoints @Get('admin/list') diff --git a/src/agents/agents.service.ts b/src/agents/agents.service.ts index 6ed02fa..0fda9d9 100644 --- a/src/agents/agents.service.ts +++ b/src/agents/agents.service.ts @@ -8,8 +8,9 @@ import { CreateAgentProfileDto, UpdateAgentProfileDto, SearchAgentsDto, + SaveFieldValuesDto, } from './dto'; -import { Prisma } from '@prisma/client'; +import { Prisma, Prisma as PrismaTypes } from '@prisma/client'; function generateSlug(firstName: string, lastName: string): string { const base = `${firstName}-${lastName}` @@ -367,4 +368,245 @@ export class AgentsService { // 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, + }; + } + + 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': + // 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': + return fieldValue.jsonValue; + default: + // Return first non-null value + return ( + fieldValue.jsonValue ?? + fieldValue.textValue ?? + fieldValue.numberValue ?? + fieldValue.booleanValue ?? + fieldValue.dateValue?.toISOString() + ); + } + } } diff --git a/src/agents/dto/index.ts b/src/agents/dto/index.ts index babc822..3aa127b 100644 --- a/src/agents/dto/index.ts +++ b/src/agents/dto/index.ts @@ -1,3 +1,4 @@ export * from './create-agent-profile.dto'; export * from './update-agent-profile.dto'; export * from './search-agents.dto'; +export * from './save-field-values.dto'; diff --git a/src/agents/dto/save-field-values.dto.ts b/src/agents/dto/save-field-values.dto.ts new file mode 100644 index 0000000..db8951d --- /dev/null +++ b/src/agents/dto/save-field-values.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class FieldValueDto { + @ApiProperty({ + example: 'first-name', + description: 'Field slug', + }) + @IsString() + @IsNotEmpty() + fieldSlug: string; + + @ApiProperty({ + example: 'John', + description: 'Field value - can be string, number, boolean, array, or object', + }) + @IsOptional() + value: string | number | boolean | string[] | Record | null; +} + +export class SaveFieldValuesDto { + @ApiProperty({ + type: [FieldValueDto], + description: 'Array of field values to save', + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => FieldValueDto) + fieldValues: FieldValueDto[]; +}