feat: implement dynamic work environment section and improve availability data mapping with backend-resolved labels

This commit is contained in:
pradeepkumar
2026-04-14 09:07:37 +05:30
parent 1eccde73e3
commit dc2b279947
3 changed files with 114 additions and 73 deletions

View File

@@ -21,11 +21,12 @@ import { connectionRequestsService } from '@/services/connection-requests.servic
import { uploadService } from '@/services/upload.service'; import { uploadService } from '@/services/upload.service';
import { usersService } from '@/services/users.service'; import { usersService } from '@/services/users.service';
import { testimonialsService, Testimonial } from '@/services/testimonials.service'; import { testimonialsService, Testimonial } from '@/services/testimonials.service';
import { mapFieldValuesToExperience, mapFieldValuesToProfileCard, mapFieldValuesToAvailability, mapFieldValuesToSpecializationFields, mapFieldValuesToContactInfo, ExperienceData, ProfileCardData, AvailabilityData, SpecializationFieldsData, ContactInfoData } from '@/utils/profileDataMapper'; import { mapFieldValuesToExperience, mapFieldValuesToProfileCard, mapFieldValuesToAvailability, mapFieldValuesToSpecializationFields, mapFieldValuesToContactInfo, mapFieldValuesToWorkEnvironment, ExperienceData, ProfileCardData, AvailabilityData, SpecializationFieldsData, ContactInfoData, WorkEnvironmentData } from '@/utils/profileDataMapper';
// Mock data for sections not yet available from API // Default work environment data
const mockData = { const defaultWorkEnvironmentData: WorkEnvironmentData = {
preferredWorkEnvironment: 'Preferred work environment includes on-site property visits, in-depth market research, client consultations, property tours, and active involvement in negotiations to ensure the best outcomes', label: 'Preferred Work Environment',
content: '',
}; };
// Default experience data when no field values are available // Default experience data when no field values are available
@@ -59,6 +60,7 @@ const defaultProfileCardData: ProfileCardData = {
const defaultAvailabilityData: AvailabilityData = { const defaultAvailabilityData: AvailabilityData = {
type: '', type: '',
schedule: [], schedule: [],
label: 'Availability',
}; };
// Default specialization fields data when no field values are available // Default specialization fields data when no field values are available
@@ -79,6 +81,7 @@ export default function AgentDashboard() {
const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience); const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience);
const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData); const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData);
const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(defaultAvailabilityData); const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(defaultAvailabilityData);
const [workEnvironmentData, setWorkEnvironmentData] = useState<WorkEnvironmentData>(defaultWorkEnvironmentData);
const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData); const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(defaultContactInfoData); const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(defaultContactInfoData);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -150,6 +153,10 @@ export default function AgentDashboard() {
const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues); const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues);
setAvailabilityData(mappedAvailability); setAvailabilityData(mappedAvailability);
// Map field values to work environment data
const mappedWorkEnv = mapFieldValuesToWorkEnvironment(fieldValuesResponse.fieldValues);
setWorkEnvironmentData(mappedWorkEnv);
// Map field values to specialization fields data (only fields from "Specialization" section) // Map field values to specialization fields data (only fields from "Specialization" section)
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues); const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues);
setSpecializationFieldsData(mappedSpecializationFields); setSpecializationFieldsData(mappedSpecializationFields);
@@ -452,11 +459,11 @@ export default function AgentDashboard() {
{/* Info Cards Section */} {/* Info Cards Section */}
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6"> <div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
<InfoCard <InfoCard
title="Availability" title={availabilityData.label}
icon={ icon={
<Image <Image
src="/assets/icons/availability-clock-icon.svg" src="/assets/icons/availability-clock-icon.svg"
alt="Availability" alt={availabilityData.label}
width={28} width={28}
height={31} height={31}
/> />
@@ -478,18 +485,20 @@ export default function AgentDashboard() {
</div> </div>
} }
/> />
<InfoCard {workEnvironmentData.content && (
title="" <InfoCard
icon={ title={workEnvironmentData.label}
<Image icon={
src="/assets/icons/work-environment-icon.svg" <Image
alt="Work environment" src="/assets/icons/work-environment-icon.svg"
width={28} alt={workEnvironmentData.label}
height={28} width={28}
/> height={28}
} />
content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{mockData.preferredWorkEnvironment}</p>} }
/> content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{workEnvironmentData.content}</p>}
/>
)}
{testimonials.length > 0 && ( {testimonials.length > 0 && (
<InfoCard <InfoCard
title="" title=""

View File

@@ -21,11 +21,12 @@ import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agen
import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service'; import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service';
import { uploadService } from '@/services/upload.service'; import { uploadService } from '@/services/upload.service';
import { testimonialsService, Testimonial } from '@/services/testimonials.service'; import { testimonialsService, Testimonial } from '@/services/testimonials.service';
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, mapFieldValuesToProfileCard, mapFieldValuesToContactInfo, mapFieldValuesToAvailability, ExperienceData, SpecializationFieldsData, ProfileCardData, ContactInfoData, AvailabilityData } from '@/utils/profileDataMapper'; import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, mapFieldValuesToProfileCard, mapFieldValuesToContactInfo, mapFieldValuesToAvailability, mapFieldValuesToWorkEnvironment, ExperienceData, SpecializationFieldsData, ProfileCardData, ContactInfoData, AvailabilityData, WorkEnvironmentData } from '@/utils/profileDataMapper';
// Mock data for sections not yet available from API // Default work environment data
const mockData = { const defaultWorkEnvironmentData: WorkEnvironmentData = {
preferredWorkEnvironment: 'Preferred work environment includes on-site property visits, in-depth market research, client consultations, property tours, and active involvement in negotiations to ensure the best outcomes', label: 'Preferred Work Environment',
content: '',
}; };
// Default experience data when no field values are available // Default experience data when no field values are available
@@ -70,6 +71,7 @@ const defaultContactInfoData: ContactInfoData = {
const defaultAvailabilityData: AvailabilityData = { const defaultAvailabilityData: AvailabilityData = {
type: '', type: '',
schedule: [], schedule: [],
label: 'Availability',
}; };
export default function AgentProfileView() { export default function AgentProfileView() {
@@ -84,6 +86,7 @@ export default function AgentProfileView() {
const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData); const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData);
const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(defaultContactInfoData); const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(defaultContactInfoData);
const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(defaultAvailabilityData); const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(defaultAvailabilityData);
const [workEnvironmentData, setWorkEnvironmentData] = useState<WorkEnvironmentData>(defaultWorkEnvironmentData);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
@@ -143,6 +146,10 @@ export default function AgentProfileView() {
const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues); const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues);
setAvailabilityData(mappedAvailability); setAvailabilityData(mappedAvailability);
// Map field values to work environment data
const mappedWorkEnv = mapFieldValuesToWorkEnvironment(fieldValuesResponse.fieldValues);
setWorkEnvironmentData(mappedWorkEnv);
// Map testimonials for TestimonialsSection // Map testimonials for TestimonialsSection
setTestimonials( setTestimonials(
testimonialsData.map((t) => ({ testimonialsData.map((t) => ({
@@ -380,11 +387,11 @@ export default function AgentProfileView() {
{/* Info Cards Section */} {/* Info Cards Section */}
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6"> <div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
<InfoCard <InfoCard
title="Availability" title={availabilityData.label}
icon={ icon={
<Image <Image
src="/assets/icons/availability-clock-icon.svg" src="/assets/icons/availability-clock-icon.svg"
alt="Availability" alt={availabilityData.label}
width={28} width={28}
height={31} height={31}
/> />
@@ -406,18 +413,20 @@ export default function AgentProfileView() {
</div> </div>
} }
/> />
<InfoCard {workEnvironmentData.content && (
title="" <InfoCard
icon={ title={workEnvironmentData.label}
<Image icon={
src="/assets/icons/work-environment-icon.svg" <Image
alt="Work environment" src="/assets/icons/work-environment-icon.svg"
width={28} alt={workEnvironmentData.label}
height={28} width={28}
/> height={28}
} />
content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{mockData.preferredWorkEnvironment}</p>} }
/> content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{workEnvironmentData.content}</p>}
/>
)}
{testimonials.length > 0 && ( {testimonials.length > 0 && (
<InfoCard <InfoCard
title="" title=""

View File

@@ -219,60 +219,83 @@ export interface SpecializationFieldsData {
export interface AvailabilityData { export interface AvailabilityData {
type: string; type: string;
schedule: string[]; schedule: string[];
label: string;
} }
// Map availability option values to human-readable labels // Work environment data structure
const availabilityLabels: Record<string, string> = { export interface WorkEnvironmentData {
'mf_9_5': 'Monday - Friday, 9 AM - 5 PM', label: string;
'mf_8_6': 'Monday - Friday, 8 AM - 6 PM', content: string;
'weekends': 'Weekends', }
'evenings': 'Evenings',
'flexible': 'Flexible Schedule', /**
'full_time': 'Full-time', * Maps field values to work environment data (label + content)
'part_time': 'Part-time', */
'24_7': '24/7 Available', export function mapFieldValuesToWorkEnvironment(fieldValues: FieldValueResponse[]): WorkEnvironmentData {
'by_appointment': 'By Appointment Only', const field = fieldValues.find(f => f.fieldSlug === 'preferred_work_environment');
'monday': 'Monday', return {
'tuesday': 'Tuesday', label: field?.fieldName || 'Preferred Work Environment',
'wednesday': 'Wednesday', content: (field?.value as string | undefined)?.trim() || '',
'thursday': 'Thursday', };
'friday': 'Friday', }
'saturday': 'Saturday',
'sunday': 'Sunday', // Type-determining values (business hours → Full-time, etc.)
}; const fullTimeKeywords = ['full_time', 'mf_9_5', 'mf_8_6', 'full time', '9-5', '9 am', '9am'];
const partTimeKeywords = ['part_time', 'part time'];
const flexibleKeywords = ['flexible'];
function matchesKeyword(value: string, keywords: string[]): boolean {
const normalized = value.toLowerCase();
return keywords.some(kw => normalized.includes(kw));
}
/** /**
* Maps field values to availability data * Maps field values to availability data
* Uses backend-resolved `valueLabel` and deduplicates by normalized label.
*/ */
export function mapFieldValuesToAvailability(fieldValues: FieldValueResponse[]): AvailabilityData { export function mapFieldValuesToAvailability(fieldValues: FieldValueResponse[]): AvailabilityData {
const availabilityValues = getFieldValue(fieldValues, 'availability') as string[] | undefined; const field = fieldValues.find(f => f.fieldSlug === 'availability');
const label = field?.fieldName || 'Availability';
if (!availabilityValues || availabilityValues.length === 0) { const rawValues = (field?.value as string[] | undefined) || [];
return { const labelValues = (field?.valueLabel as string[] | undefined) || [];
type: '',
schedule: [], if (rawValues.length === 0) {
}; return { type: '', schedule: [], label };
} }
// Map values to human-readable labels // Prefer backend-resolved labels, fall back to raw value with snake_case formatting
const schedule = availabilityValues.map(val => const resolvedLabels = rawValues.map((val, i) => {
availabilityLabels[val] || val.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) const fromBackend = labelValues[i];
); if (fromBackend && typeof fromBackend === 'string' && fromBackend.trim()) {
return fromBackend.trim();
}
return val.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
});
// Determine type based on values // Deduplicate by normalized comparison (case/whitespace insensitive)
const seen = new Set<string>();
const schedule: string[] = [];
for (const item of resolvedLabels) {
const key = item.toLowerCase().replace(/[\s,.-]+/g, '');
if (!seen.has(key)) {
seen.add(key);
schedule.push(item);
}
}
// Determine availability type based on values OR resolved labels
const allValues = [...rawValues.map(v => String(v)), ...resolvedLabels];
let type = 'Available'; let type = 'Available';
if (availabilityValues.includes('full_time') || availabilityValues.includes('mf_9_5') || availabilityValues.includes('mf_8_6')) { if (allValues.some(v => matchesKeyword(v, fullTimeKeywords))) {
type = 'Full-time'; type = 'Full-time';
} else if (availabilityValues.includes('part_time')) { } else if (allValues.some(v => matchesKeyword(v, partTimeKeywords))) {
type = 'Part-time'; type = 'Part-time';
} else if (availabilityValues.includes('flexible')) { } else if (allValues.some(v => matchesKeyword(v, flexibleKeywords))) {
type = 'Flexible'; type = 'Flexible';
} }
return { return { type, schedule, label };
type,
schedule,
};
} }
/** /**