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:
@@ -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')
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
31
src/agents/dto/save-field-values.dto.ts
Normal file
31
src/agents/dto/save-field-values.dto.ts
Normal 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[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user