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,
|
||||
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')
|
||||
|
||||
@@ -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<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 './update-agent-profile.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