feat: Implement dynamic agent profile editing by introducing new services and dynamic UI components.

This commit is contained in:
pradeepkumar
2026-01-24 20:36:22 +05:30
parent cb2e17c759
commit 5e2b3d8e83
12 changed files with 1133 additions and 705 deletions

View File

@@ -0,0 +1,66 @@
import api from './api';
// Types
export interface AgentType {
id: string;
name: string;
slug: string;
description: string | null;
}
export interface AgentProfile {
id: string;
userId: string;
firstName: string;
lastName: string;
slug: string;
email: string | null;
phone: string | null;
bio: string | null;
avatar: string | null;
licenseNumber: string | null;
experience: number | null;
specializations: string[];
languages: string[];
serviceAreas: string[];
rating: number | null;
reviewCount: number;
isVerified: boolean;
isFeatured: boolean;
isActive: boolean;
agentTypeId: string | null;
agentType: AgentType | null;
user?: {
id: string;
email: string;
avatar: string | null;
};
}
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
// Agents Service for Web
class AgentsService {
private basePath = '/agents';
async getMyProfile(): Promise<AgentProfile> {
const response = await api.get<ApiResponse<AgentProfile>>(`${this.basePath}/profile/me`);
return response.data.data;
}
async getAgentById(id: string): Promise<AgentProfile> {
const response = await api.get<ApiResponse<AgentProfile>>(`${this.basePath}/${id}`);
return response.data.data;
}
async updateProfile(data: Partial<AgentProfile>): Promise<AgentProfile> {
const response = await api.put<ApiResponse<AgentProfile>>(`${this.basePath}/profile`, data);
return response.data.data;
}
}
export const agentsService = new AgentsService();

View File

@@ -1,4 +1,4 @@
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
// API Response types
export interface ApiResponse<T = unknown> {
@@ -19,6 +19,23 @@ export interface ApiErrorResponse {
errors?: Array<{ field: string; errors: string[] }>;
}
// Token refresh state
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value: AxiosResponse) => void;
reject: (error: AxiosError) => void;
config: InternalAxiosRequestConfig;
}> = [];
const processQueue = (error: AxiosError | null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
}
});
failedQueue = [];
};
// Create axios instance
const api: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1',
@@ -45,22 +62,100 @@ api.interceptors.request.use(
}
);
// Response interceptor
// Response interceptor with token refresh
api.interceptors.response.use(
(response) => {
return response;
},
(error: AxiosError<ApiErrorResponse>) => {
// Handle common errors
if (error.response) {
const { status } = error.response;
async (error: AxiosError<ApiErrorResponse>) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Handle 401 Unauthorized
if (status === 401) {
// Handle 401 Unauthorized
if (error.response?.status === 401 && !originalRequest._retry) {
// Don't try to refresh if this was the refresh token request itself
if (originalRequest.url?.includes('/auth/refresh')) {
// Refresh token is also invalid, clear everything and sign out properly
if (typeof window !== 'undefined') {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
// Use NextAuth's signout endpoint to properly clear the session
window.location.href = '/api/auth/signout?callbackUrl=/login';
}
return Promise.reject(error);
}
if (isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject, config: originalRequest });
}).then(() => {
// Retry with new token
const newToken = localStorage.getItem('accessToken');
if (newToken && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
}
return api(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
if (!refreshToken) {
// No refresh token available, sign out properly
if (typeof window !== 'undefined') {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
// Use NextAuth's signout endpoint to properly clear the session
window.location.href = '/api/auth/signout?callbackUrl=/login';
}
isRefreshing = false;
return Promise.reject(error);
}
try {
// Try to refresh the token
const response = await axios.post<ApiResponse<{ accessToken: string; refreshToken: string }>>(
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'}/auth/refresh`,
{ refreshToken },
{ headers: { 'Content-Type': 'application/json' } }
);
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data.data;
// Save new tokens
if (typeof window !== 'undefined') {
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
}
// Update the failed request with new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
}
// Process queued requests
processQueue(null);
isRefreshing = false;
// Retry original request
return api(originalRequest);
} catch (refreshError) {
// Refresh failed, clear tokens and sign out properly
if (typeof window !== 'undefined') {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
// Use NextAuth's signout endpoint to properly clear the session
window.location.href = '/api/auth/signout?callbackUrl=/login';
}
processQueue(refreshError as AxiosError);
isRefreshing = false;
return Promise.reject(refreshError);
}
}

View File

@@ -14,3 +14,20 @@ export type {
VerifyEmailRequest,
MessageResponse,
} from './auth.service';
// Profile Sections Service
export { profileSectionsService } from './profile-sections.service';
export type {
FieldType,
FieldOption,
RangeConfig,
FieldValidation,
FieldUiConfig,
ProfileField,
ProfileSection,
AgentTypeSectionsResponse,
} from './profile-sections.service';
// Agents Service
export { agentsService } from './agents.service';
export type { AgentProfile, AgentType } from './agents.service';

View File

@@ -0,0 +1,115 @@
import api from './api';
// Field Types - matching backend enum
export type FieldType =
| 'TEXT'
| 'TEXTAREA'
| 'CHECKBOX'
| 'CHECKBOX_GROUP'
| 'RADIO'
| 'SELECT'
| 'MULTI_SELECT'
| 'RANGE'
| 'NUMBER'
| 'DATE'
| 'TAG_INPUT';
// Types
export interface FieldOption {
value: string;
label: string;
}
export interface RangeConfig {
min: number;
max: number;
step?: number;
unit?: string;
}
export interface FieldValidation {
min?: number;
max?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
}
export interface FieldUiConfig {
columns?: number;
showInPreview?: boolean;
helpText?: string;
}
export interface ProfileField {
id: string;
sectionId: string;
name: string;
slug: string;
fieldType: FieldType;
description: string | null;
placeholder: string | null;
defaultValue: string | null;
sortOrder: number;
isActive: boolean;
isRequired: boolean;
isSearchableOnly: boolean;
validation: FieldValidation | null;
options: FieldOption[] | null;
rangeConfig: RangeConfig | null;
uiConfig: FieldUiConfig | null;
}
export interface ProfileSection {
id: string;
name: string;
slug: string;
description: string | null;
icon: string | null;
sortOrder: number;
isActive: boolean;
isGlobal: boolean;
isSystem: boolean;
fields?: ProfileField[];
isRequired?: boolean;
effectiveSortOrder?: number;
hasCustomOrder?: boolean;
}
export interface AgentTypeSectionsResponse {
agentType: {
id: string;
name: string;
description: string | null;
};
sections: ProfileSection[];
}
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
// Profile Sections Service for Web
class ProfileSectionsService {
private basePath = '/profile-sections';
async getSectionsForAgentType(agentTypeId: string, includeFields = true): Promise<AgentTypeSectionsResponse> {
const url = includeFields
? `${this.basePath}/agent-type/${agentTypeId}`
: `${this.basePath}/agent-type/${agentTypeId}?includeFields=false`;
const response = await api.get<ApiResponse<AgentTypeSectionsResponse>>(url);
return response.data.data;
}
async getAll(includeInactive = false): Promise<ProfileSection[]> {
const url = includeInactive
? `${this.basePath}?includeInactive=true`
: this.basePath;
const response = await api.get<ApiResponse<ProfileSection[]>>(url);
return response.data.data;
}
}
export const profileSectionsService = new ProfileSectionsService();