2026-01-18 21:21:17 +05:30
|
|
|
'use client';
|
|
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
import { useState, useRef, useEffect } from 'react';
|
2026-01-18 21:21:17 +05:30
|
|
|
import { useRouter } from 'next/navigation';
|
2026-01-24 20:36:22 +05:30
|
|
|
import { QuickLinks } from './components';
|
|
|
|
|
import DynamicSection from './components/DynamicSection';
|
|
|
|
|
import { profileSectionsService, ProfileSection, AgentTypeSectionsResponse } from '@/services/profile-sections.service';
|
|
|
|
|
import { agentsService, AgentProfile } from '@/services/agents.service';
|
2026-01-18 21:21:17 +05:30
|
|
|
|
|
|
|
|
export default function EditProfilePage() {
|
|
|
|
|
const router = useRouter();
|
2026-01-18 21:29:52 +05:30
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
2026-01-18 21:21:17 +05:30
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
// State for agent profile and dynamic sections
|
|
|
|
|
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
|
|
|
|
|
const [sections, setSections] = useState<ProfileSection[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// Form data keyed by field slug
|
|
|
|
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
|
|
|
|
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Fetch agent profile and sections on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
// First, fetch the agent's profile to get their agentTypeId
|
|
|
|
|
const profile = await agentsService.getMyProfile();
|
|
|
|
|
setAgentProfile(profile);
|
|
|
|
|
|
|
|
|
|
if (!profile.agentTypeId) {
|
|
|
|
|
setError('Your profile does not have an agent type assigned. Please contact support.');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now fetch the sections for this agent type
|
|
|
|
|
const response: AgentTypeSectionsResponse = await profileSectionsService.getSectionsForAgentType(profile.agentTypeId);
|
|
|
|
|
|
|
|
|
|
// Sort sections by effectiveSortOrder
|
|
|
|
|
const sortedSections = [...response.sections].sort(
|
|
|
|
|
(a, b) => (a.effectiveSortOrder || a.sortOrder) - (b.effectiveSortOrder || b.sortOrder)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setSections(sortedSections);
|
|
|
|
|
|
|
|
|
|
// Initialize form data with existing profile data + default values from fields
|
|
|
|
|
const initialData: Record<string, unknown> = {};
|
|
|
|
|
|
|
|
|
|
// Pre-fill with existing profile data
|
|
|
|
|
if (profile.firstName) initialData['first-name'] = profile.firstName;
|
|
|
|
|
if (profile.lastName) initialData['last-name'] = profile.lastName;
|
|
|
|
|
if (profile.email) initialData['email'] = profile.email;
|
|
|
|
|
if (profile.phone) initialData['phone'] = profile.phone;
|
|
|
|
|
if (profile.bio) initialData['bio'] = profile.bio;
|
|
|
|
|
if (profile.licenseNumber) initialData['license-number'] = profile.licenseNumber;
|
|
|
|
|
if (profile.experience) initialData['experience'] = profile.experience;
|
|
|
|
|
if (profile.specializations?.length) initialData['specializations'] = profile.specializations;
|
|
|
|
|
if (profile.languages?.length) initialData['languages'] = profile.languages;
|
|
|
|
|
if (profile.serviceAreas?.length) initialData['service-areas'] = profile.serviceAreas;
|
|
|
|
|
|
|
|
|
|
// Then apply field default values where no existing data
|
|
|
|
|
sortedSections.forEach(section => {
|
|
|
|
|
section.fields?.forEach(field => {
|
|
|
|
|
if (field.defaultValue && initialData[field.slug] === undefined) {
|
|
|
|
|
// Parse default value based on field type
|
|
|
|
|
switch (field.fieldType) {
|
|
|
|
|
case 'CHECKBOX':
|
|
|
|
|
initialData[field.slug] = field.defaultValue === 'true';
|
|
|
|
|
break;
|
|
|
|
|
case 'NUMBER':
|
|
|
|
|
case 'RANGE':
|
|
|
|
|
initialData[field.slug] = parseFloat(field.defaultValue) || 0;
|
|
|
|
|
break;
|
|
|
|
|
case 'CHECKBOX_GROUP':
|
|
|
|
|
case 'MULTI_SELECT':
|
|
|
|
|
case 'TAG_INPUT':
|
|
|
|
|
try {
|
|
|
|
|
initialData[field.slug] = JSON.parse(field.defaultValue);
|
|
|
|
|
} catch {
|
|
|
|
|
initialData[field.slug] = [];
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
initialData[field.slug] = field.defaultValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
setFormData(initialData);
|
|
|
|
|
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
console.error('Failed to fetch data:', err);
|
|
|
|
|
|
|
|
|
|
// Check for specific error types
|
|
|
|
|
const error = err as { response?: { status?: number } };
|
|
|
|
|
if (error.response?.status === 401) {
|
|
|
|
|
setError('You are not logged in or your session has expired. Please log in again.');
|
|
|
|
|
} else if (error.response?.status === 404) {
|
|
|
|
|
setError('Agent profile not found. Please complete your registration first.');
|
|
|
|
|
} else {
|
|
|
|
|
setError('Failed to load profile. Please try again.');
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchData();
|
|
|
|
|
}, []);
|
2026-01-18 21:21:17 +05:30
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
const handleCancel = () => {
|
2026-01-18 21:21:17 +05:30
|
|
|
router.push('/agent/dashboard');
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
const handleSave = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
setFormErrors({});
|
|
|
|
|
|
|
|
|
|
// Validate required fields
|
|
|
|
|
const errors: Record<string, string> = {};
|
|
|
|
|
sections.forEach(section => {
|
|
|
|
|
section.fields?.forEach(field => {
|
|
|
|
|
if (field.isRequired && field.isActive && !field.isSearchableOnly) {
|
|
|
|
|
const value = formData[field.slug];
|
|
|
|
|
const isEmpty =
|
|
|
|
|
value === undefined ||
|
|
|
|
|
value === null ||
|
|
|
|
|
value === '' ||
|
|
|
|
|
(Array.isArray(value) && value.length === 0);
|
|
|
|
|
|
|
|
|
|
if (isEmpty) {
|
|
|
|
|
errors[field.slug] = `${field.name} is required`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-18 23:08:44 +05:30
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
if (Object.keys(errors).length > 0) {
|
|
|
|
|
setFormErrors(errors);
|
|
|
|
|
// Scroll to first error
|
|
|
|
|
const firstErrorField = Object.keys(errors)[0];
|
|
|
|
|
const errorElement = document.querySelector(`[data-field-slug="${firstErrorField}"]`);
|
|
|
|
|
errorElement?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Call API to save form data
|
|
|
|
|
console.log('Saving form data:', formData);
|
|
|
|
|
|
|
|
|
|
// Simulate save
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
|
|
|
|
|
|
router.push('/agent/dashboard');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to save:', err);
|
|
|
|
|
setError('Failed to save profile. Please try again.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
2026-01-18 23:08:44 +05:30
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
const handleFieldChange = (fieldSlug: string, value: unknown) => {
|
|
|
|
|
setFormData(prev => ({ ...prev, [fieldSlug]: value }));
|
|
|
|
|
// Clear error when field is modified
|
|
|
|
|
if (formErrors[fieldSlug]) {
|
|
|
|
|
setFormErrors(prev => {
|
|
|
|
|
const newErrors = { ...prev };
|
|
|
|
|
delete newErrors[fieldSlug];
|
|
|
|
|
return newErrors;
|
2026-01-18 23:13:56 +05:30
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-24 20:36:22 +05:30
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center h-[calc(100vh-180px)]">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#E58625]"></div>
|
|
|
|
|
<p className="text-[14px] font-serif text-[#00293D]/70">Loading profile sections...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error && sections.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center h-[calc(100vh-180px)]">
|
|
|
|
|
<div className="bg-white rounded-[20px] p-8 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] max-w-md text-center">
|
|
|
|
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-[18px] font-bold font-serif text-[#00293D] mb-2">Unable to Load Profile</h2>
|
|
|
|
|
<p className="text-[14px] font-serif text-[#00293D]/70 mb-4">{error}</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => window.location.reload()}
|
|
|
|
|
className="px-6 py-2 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Try Again
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-18 23:13:56 +05:30
|
|
|
|
2026-01-18 21:21:17 +05:30
|
|
|
return (
|
2026-01-18 21:29:52 +05:30
|
|
|
<div className="flex gap-6 h-[calc(100vh-180px)]">
|
2026-01-18 21:21:17 +05:30
|
|
|
{/* Left Sidebar - Quick Links */}
|
2026-01-18 21:29:52 +05:30
|
|
|
<div className="w-[220px] flex-shrink-0 hidden lg:block h-full overflow-y-auto scrollbar-thin">
|
2026-01-24 20:36:22 +05:30
|
|
|
<QuickLinks scrollContainerRef={scrollContainerRef} sections={sections} />
|
2026-01-18 21:21:17 +05:30
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 21:29:52 +05:30
|
|
|
{/* Main Content - Scrollable */}
|
2026-01-24 20:36:22 +05:30
|
|
|
<div
|
|
|
|
|
ref={scrollContainerRef}
|
|
|
|
|
className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent"
|
|
|
|
|
>
|
|
|
|
|
{/* Inner container with padding for shadow visibility */}
|
|
|
|
|
<div className="space-y-6 p-2">
|
|
|
|
|
{/* Error Banner */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="bg-red-50 border border-red-200 rounded-[15px] p-4 flex items-center gap-3">
|
|
|
|
|
<svg className="w-5 h-5 text-red-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-[14px] font-serif text-red-700">{error}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Dynamic Sections */}
|
|
|
|
|
{sections.map((section) => (
|
|
|
|
|
<DynamicSection
|
|
|
|
|
key={section.id}
|
|
|
|
|
section={section}
|
|
|
|
|
formData={formData}
|
|
|
|
|
onChange={handleFieldChange}
|
|
|
|
|
errors={formErrors}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{/* Empty State */}
|
|
|
|
|
{sections.length === 0 && !loading && (
|
|
|
|
|
<div className="bg-white rounded-[20px] p-8 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] text-center">
|
|
|
|
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
|
|
|
<svg className="w-8 h-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
2026-01-18 21:21:17 +05:30
|
|
|
</div>
|
2026-01-24 20:36:22 +05:30
|
|
|
<h2 className="text-[18px] font-bold font-serif text-[#00293D] mb-2">No Profile Sections Available</h2>
|
|
|
|
|
<p className="text-[14px] font-serif text-[#00293D]/70">
|
|
|
|
|
Profile sections have not been configured for your agent type yet.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-18 21:21:17 +05:30
|
|
|
|
|
|
|
|
{/* Action Buttons */}
|
2026-01-24 20:36:22 +05:30
|
|
|
{sections.length > 0 && (
|
|
|
|
|
<div className="flex justify-end gap-4 pb-6">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCancel}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
className="px-8 py-3 border border-[#00293D]/20 rounded-full text-[14px] font-semibold font-serif text-[#00293D] hover:bg-gray-50 transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
className="px-8 py-3 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors disabled:opacity-50 flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
{isSaving && (
|
|
|
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
|
|
|
)}
|
|
|
|
|
{isSaving ? 'Saving...' : 'Save'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-18 21:21:17 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|