feat: Add dynamic field value management for agent profiles, including DTOs, controller endpoints, and service logic for saving and retrieving.

This commit is contained in:
pradeepkumar
2026-01-24 21:06:08 +05:30
parent 8dc8a961ab
commit 5844d17837
4 changed files with 301 additions and 1 deletions

View File

@@ -21,6 +21,7 @@ import {
CreateAgentProfileDto, CreateAgentProfileDto,
UpdateAgentProfileDto, UpdateAgentProfileDto,
SearchAgentsDto, SearchAgentsDto,
SaveFieldValuesDto,
} from './dto'; } from './dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
@@ -117,6 +118,31 @@ export class AgentsController {
return this.agentsService.updateProfile(userId, dto); 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 // Admin endpoints
@Get('admin/list') @Get('admin/list')

View File

@@ -8,8 +8,9 @@ import {
CreateAgentProfileDto, CreateAgentProfileDto,
UpdateAgentProfileDto, UpdateAgentProfileDto,
SearchAgentsDto, SearchAgentsDto,
SaveFieldValuesDto,
} from './dto'; } from './dto';
import { Prisma } from '@prisma/client'; import { Prisma, Prisma as PrismaTypes } from '@prisma/client';
function generateSlug(firstName: string, lastName: string): string { function generateSlug(firstName: string, lastName: string): string {
const base = `${firstName}-${lastName}` const base = `${firstName}-${lastName}`
@@ -367,4 +368,245 @@ export class AgentsService {
// Same as searchAgents but returns all data for admin // Same as searchAgents but returns all data for admin
return this.searchAgents(dto); 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<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':
// 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()
);
}
}
} }

View File

@@ -1,3 +1,4 @@
export * from './create-agent-profile.dto'; export * from './create-agent-profile.dto';
export * from './update-agent-profile.dto'; export * from './update-agent-profile.dto';
export * from './search-agents.dto'; export * from './search-agents.dto';
export * from './save-field-values.dto';

View File

@@ -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<string, unknown> | null;
}
export class SaveFieldValuesDto {
@ApiProperty({
type: [FieldValueDto],
description: 'Array of field values to save',
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => FieldValueDto)
fieldValues: FieldValueDto[];
}