Files
frontend/src/utils/profileDataMapper.ts

397 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FieldValueResponse } from '@/services/agents.service';
// Experience data structure expected by ExperienceSection component
export interface ExperienceData {
years: string;
contracts: string;
licensingAreas: string[];
expertiseYears: { area: string; years: string }[];
certifications: { name: string; org: string }[];
}
// Certification entry from repeater field
interface CertificationEntry {
certification_name?: string;
issuing_organization?: string;
issue_date?: string;
expiration_date?: string;
}
// Helper to get field value by slug
function getFieldValue(fieldValues: FieldValueResponse[], slug: string): unknown {
const field = fieldValues.find(f => f.fieldSlug === slug);
return field?.value;
}
// Helper to get label from value for select/radio fields
function mapYearsValueToLabel(value: string | undefined): string {
if (!value) return '-';
const mapping: Record<string, string> = {
'<2': 'Less than 2 years',
'2+': '2+ years',
'5+': '5+ years',
'10+': '10+ years',
};
return mapping[value] || value;
}
// Helper to format contracts count
function formatContractsCount(value: unknown): string {
if (!value) return '-';
const num = typeof value === 'number' ? value : parseInt(String(value), 10);
if (isNaN(num)) return '-';
if (num >= 100) return '100+ Contracts';
if (num >= 50) return '50+ Contracts';
if (num >= 20) return '20+ Contracts';
if (num >= 10) return '10+ Contracts';
if (num >= 5) return '5+ Contracts';
return `${num} Contract${num !== 1 ? 's' : ''}`;
}
// Parse expertise areas with years (format: "Area Name - X yrs" or just "Area Name")
function parseExpertiseAreas(value: unknown): { area: string; years: string }[] {
if (!value || !Array.isArray(value)) return [];
return value.map((item: string) => {
// Check if it contains a year indicator
const yearMatch = item.match(/^(.+?)\s*[-]\s*(\d+)\s*(yrs?|years?)$/i);
if (yearMatch) {
return { area: yearMatch[1].trim(), years: `${yearMatch[2]} yrs` };
}
// Just area name without years
return { area: item.trim(), years: '' };
});
}
// Parse certification entries from repeater field
function parseCertifications(value: unknown): { name: string; org: string }[] {
if (!value) return [];
// If it's an array of certification entries
if (Array.isArray(value)) {
return value
.filter((entry: CertificationEntry) => entry?.certification_name)
.map((entry: CertificationEntry) => ({
name: entry.certification_name || '',
org: entry.issuing_organization || '',
}));
}
return [];
}
/**
* Maps field values from API response to ExperienceData structure
*/
export function mapFieldValuesToExperience(fieldValues: FieldValueResponse[]): ExperienceData {
// Get years in business (could be years_in_business from Professional or Lender experience)
const yearsInBusiness = getFieldValue(fieldValues, 'years_in_business') as string | undefined;
// Get contracts completed
const contractsCompleted = getFieldValue(fieldValues, 'contracts_completed');
// Get licensing areas (could be licensed_areas from Professional or Lender)
const licensedAreas = getFieldValue(fieldValues, 'licensed_areas') as string[] | undefined;
// Get expertise areas with years
const expertiseAreas = getFieldValue(fieldValues, 'expertise_areas') as string[] | undefined;
// Get certification entries from repeater
const certificationEntries = getFieldValue(fieldValues, 'certification_entries');
return {
years: mapYearsValueToLabel(yearsInBusiness),
contracts: formatContractsCount(contractsCompleted),
licensingAreas: licensedAreas || [],
expertiseYears: parseExpertiseAreas(expertiseAreas),
certifications: parseCertifications(certificationEntries),
};
}
/**
* Check if experience data has any meaningful content
*/
export function hasExperienceData(experience: ExperienceData): boolean {
return (
experience.years !== '-' ||
experience.contracts !== '-' ||
experience.licensingAreas.length > 0 ||
experience.expertiseYears.length > 0 ||
experience.certifications.length > 0
);
}
// Profile card data structure
export interface ProfileCardData {
bio: string;
expertise: string[];
serviceAreas: string[];
city: string | null;
state: string | null;
}
// Contact info data structure
export interface ContactInfoData {
email: string | null;
phone: string | null;
}
/**
* Maps field values to contact info data (email, phone)
*/
export function mapFieldValuesToContactInfo(fieldValues: FieldValueResponse[]): ContactInfoData {
// Get phone from various possible field slugs
const phoneNumber = getFieldValue(fieldValues, 'phone_number') as string | undefined;
const phone = getFieldValue(fieldValues, 'phone') as string | undefined;
const cellNumber = getFieldValue(fieldValues, 'cell_number') as string | undefined;
const officeNumber = getFieldValue(fieldValues, 'office_number') as string | undefined;
// Get email from various possible field slugs
const emailField = getFieldValue(fieldValues, 'email') as string | undefined;
const emailAddress = getFieldValue(fieldValues, 'email_address') as string | undefined;
return {
email: emailField || emailAddress || null,
phone: phoneNumber || phone || cellNumber || officeNumber || null,
};
}
// Dynamic field data structure for SpecializationSection component
// Each field within the "Specialization" section becomes a card
export interface SpecializationFieldItem {
fieldSlug: string;
fieldName: string;
icon?: string;
values: string[];
}
export interface SpecializationFieldsData {
fields: SpecializationFieldItem[];
}
// Availability data structure
export interface AvailabilityData {
type: string;
schedule: string[];
}
// Map availability option values to human-readable labels
const availabilityLabels: Record<string, string> = {
'mf_9_5': 'Monday - Friday, 9 AM - 5 PM',
'mf_8_6': 'Monday - Friday, 8 AM - 6 PM',
'weekends': 'Weekends',
'evenings': 'Evenings',
'flexible': 'Flexible Schedule',
'full_time': 'Full-time',
'part_time': 'Part-time',
'24_7': '24/7 Available',
'by_appointment': 'By Appointment Only',
'monday': 'Monday',
'tuesday': 'Tuesday',
'wednesday': 'Wednesday',
'thursday': 'Thursday',
'friday': 'Friday',
'saturday': 'Saturday',
'sunday': 'Sunday',
};
/**
* Maps field values to availability data
*/
export function mapFieldValuesToAvailability(fieldValues: FieldValueResponse[]): AvailabilityData {
const availabilityValues = getFieldValue(fieldValues, 'availability') as string[] | undefined;
if (!availabilityValues || availabilityValues.length === 0) {
return {
type: '',
schedule: [],
};
}
// Map values to human-readable labels
const schedule = availabilityValues.map(val =>
availabilityLabels[val] || val.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
);
// Determine type based on values
let type = 'Available';
if (availabilityValues.includes('full_time') || availabilityValues.includes('mf_9_5') || availabilityValues.includes('mf_8_6')) {
type = 'Full-time';
} else if (availabilityValues.includes('part_time')) {
type = 'Part-time';
} else if (availabilityValues.includes('flexible')) {
type = 'Flexible';
}
return {
type,
schedule,
};
}
/**
* Helper to format snake_case values to Title Case
*/
function formatSnakeCaseToTitleCase(value: string): string {
return value
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
/**
* Extract all values from a field that may be an array (multiselect) or string
* Returns formatted values joined by comma, or null if empty
*/
function extractAllValues(value: unknown): string | null {
if (Array.isArray(value) && value.length > 0) {
return value.map(v => formatSnakeCaseToTitleCase(String(v))).join(', ');
}
if (typeof value === 'string' && value.trim()) {
return formatSnakeCaseToTitleCase(value);
}
return null;
}
/**
* Maps field values to profile card data (bio, expertise, service areas, location)
*/
export function mapFieldValuesToProfileCard(fieldValues: FieldValueResponse[]): ProfileCardData {
// Get bio/description from various possible fields
const description = getFieldValue(fieldValues, 'description') as string | undefined;
const descrption = getFieldValue(fieldValues, 'descrption') as string | undefined; // typo in field slug
const biography = getFieldValue(fieldValues, 'biography') as string | undefined;
const bio = getFieldValue(fieldValues, 'bio') as string | undefined;
// Get expertise from various possible fields
const aboutMeExpertise = getFieldValue(fieldValues, 'about_me_expertise') as string[] | undefined;
const expertiseAreas = getFieldValue(fieldValues, 'expertise_areas') as string[] | undefined;
const specializations = getFieldValue(fieldValues, 'specializations') as string[] | undefined;
const areasOfExpertise = getFieldValue(fieldValues, 'areas_of_expertise') as string[] | undefined;
// Get service areas from various possible fields
const serviceAreas = getFieldValue(fieldValues, 'service_areas') as string[] | undefined;
const licensedAreas = getFieldValue(fieldValues, 'licensed_areas') as string[] | undefined;
const coverageAreas = getFieldValue(fieldValues, 'coverage_areas') as string[] | undefined;
// Get city and state from dynamic fields (multiselect fields)
const cityValue = getFieldValue(fieldValues, 'city');
const stateValue = getFieldValue(fieldValues, 'state');
return {
bio: description || descrption || biography || bio || '',
expertise: aboutMeExpertise || expertiseAreas || specializations || areasOfExpertise || [],
serviceAreas: serviceAreas || licensedAreas || coverageAreas || [],
city: extractAllValues(cityValue),
state: extractAllValues(stateValue),
};
}
// Field slug to icon mapping for specialization fields
const fieldIconMapping: Record<string, string> = {
'client_specialization': '/assets/icons/home-icon.svg',
'property_type': '/assets/icons/chart-icon.svg',
'loan_type': '/assets/icons/wallet-icon.svg',
'areas_expertise_years': '/assets/icons/certification-icon.svg',
'licensing_areas': '/assets/icons/location-icon.svg',
'hobbies': '/assets/icons/heart-icon.svg',
'hobbies_interests': '/assets/icons/heart-icon.svg',
'price_points': '/assets/icons/wallet-icon.svg',
'price_point': '/assets/icons/wallet-icon.svg',
'default': '/assets/icons/home-icon.svg',
};
/**
* Get icon for a field based on its slug
*/
function getFieldIcon(fieldSlug: string): string {
const normalizedSlug = fieldSlug.toLowerCase();
// Check for exact match first
if (fieldIconMapping[normalizedSlug]) {
return fieldIconMapping[normalizedSlug];
}
// Check for partial matches
if (normalizedSlug.includes('loan')) return fieldIconMapping['loan_type'];
if (normalizedSlug.includes('property')) return fieldIconMapping['property_type'];
if (normalizedSlug.includes('hobby') || normalizedSlug.includes('interest')) return fieldIconMapping['hobbies'];
if (normalizedSlug.includes('price')) return fieldIconMapping['price_points'];
if (normalizedSlug.includes('client') || normalizedSlug.includes('special')) return fieldIconMapping['client_specialization'];
if (normalizedSlug.includes('license') || normalizedSlug.includes('area')) return fieldIconMapping['licensing_areas'];
if (normalizedSlug.includes('expertise') || normalizedSlug.includes('certification')) return fieldIconMapping['areas_expertise_years'];
return fieldIconMapping['default'];
}
/**
* Convert field value to string array for display
*/
function valueToStringArray(value: unknown): string[] {
if (!value) return [];
if (Array.isArray(value)) {
return value.map(v => String(v)).filter(v => v.trim() !== '');
}
if (typeof value === 'string' && value.trim()) {
return [value];
}
if (typeof value === 'number' || typeof value === 'boolean') {
return [String(value)];
}
return [];
}
/**
* Maps field values from the "Specialization" section to individual field cards
* Each field within the Specialization section becomes its own card
*/
export function mapFieldValuesToSpecializationFields(fieldValues: FieldValueResponse[]): SpecializationFieldsData {
// Filter only fields from the "Specialization" section
const specializationFields = fieldValues.filter(field => {
const sectionSlug = field.sectionSlug?.toLowerCase() || '';
return sectionSlug === 'specialization' || sectionSlug.includes('specialization');
});
// Create a card for each field, deduplicating by fieldSlug
const fields: SpecializationFieldItem[] = [];
const seenSlugs = new Set<string>();
for (const field of specializationFields) {
const { fieldSlug, fieldName, value } = field;
// Skip if no field info or no value
if (!fieldSlug || !fieldName) continue;
// Skip duplicate fieldSlugs (API may return duplicates)
if (seenSlugs.has(fieldSlug)) continue;
seenSlugs.add(fieldSlug);
const values = valueToStringArray(value);
if (values.length === 0) continue;
fields.push({
fieldSlug,
fieldName,
icon: getFieldIcon(fieldSlug),
values,
});
}
return { fields };
}
/**
* Check if specialization fields data has any meaningful content
*/
export function hasSpecializationFieldsData(data: SpecializationFieldsData): boolean {
return data.fields.length > 0 && data.fields.some(f => f.values.length > 0);
}