diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index fc54f51..d28c01e 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -5,29 +5,47 @@ import axios from 'axios'; * In development, this relies on Vite's proxy (see vite.config.ts) to avoid CORS issues. */ const axiosInstance = axios.create({ - baseURL: '/api', // This matches the proxy path in vite.config.ts - timeout: 10000, + baseURL: 'http://localhost:8003/api', // Direct backend URL (requires CORS on backend) + timeout: 30000, // Default timeout (30 seconds) headers: { 'Content-Type': 'application/json', }, }); +/** + * Create a custom axios instance with extended timeout for heavy operations + * like fetching large dealer lists + */ +export const createExtendedTimeoutInstance = (timeout: number = 120000) => { + return axios.create({ + baseURL: 'http://localhost:8003/api', + timeout, + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +// Helper function to add auth token to config +const addAuthToken = (config: any) => { + const session = localStorage.getItem('dealer360_session'); + if (session) { + try { + const { access_token } = JSON.parse(session); + if (access_token) { + config.headers.Authorization = `Bearer ${access_token}`; + } + } catch (error) { + console.error('Error parsing session for Authorization header', error); + } + } + return config; +}; + // Request interceptor for adding auth tokens, etc. axiosInstance.interceptors.request.use( (config) => { - // You can get the token from localStorage or a store (e.g., Zustand) - const session = localStorage.getItem('dealer360_session'); - if (session) { - try { - const { access_token } = JSON.parse(session); - if (access_token) { - config.headers.Authorization = `Bearer ${access_token}`; - } - } catch (error) { - console.error('Error parsing session for Authorization header', error); - } - } - return config; + return addAuthToken(config); }, (error) => { return Promise.reject(error); @@ -65,4 +83,39 @@ axiosInstance.interceptors.response.use( } ); +// Setup interceptors for extended timeout instance +export const setupExtendedInstanceInterceptors = (instance: typeof axiosInstance) => { + // Request interceptor + instance.interceptors.request.use( + (config) => { + return addAuthToken(config); + }, + (error) => { + return Promise.reject(error); + } + ); + + // Response interceptor + instance.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + if (error.response.status === 401) { + localStorage.removeItem('dealer360_session'); + if (!window.location.pathname.includes('/login')) { + console.warn('Session expired. Redirecting to login.'); + window.location.href = '/login?expired=true'; + } + } + console.error(`API Error [${error.response.status}]:`, error.response.data); + } else if (error.request) { + console.error('API No Response:', error.request); + } else { + console.error('API Request Setup Error:', error.message); + } + return Promise.reject(error); + } + ); +}; + export default axiosInstance; diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 73e33c0..121618c 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -9,8 +9,18 @@ export const API_ENDPOINTS = { REFRESH_TOKEN: '/v1/auth/refresh-token', }, DEALERS: { - LIST: '/dealers', - DETAIL: (id: string | number) => `/dealers/${id}`, - KPI: '/dealers/kpi', + LIST: '/v1/dealers/list', + DETAIL: (id: string | number) => `/v1/dealers/${id}`, + KPI: '/v1/dealers/kpi', + OVERVIEW: '/v1/dealers/overview', + STATES: '/v1/dealers/states', + DISTRICTS: '/v1/dealers/districts', + SNAPSHOT: (id: string | number) => `/v1/dealerdetails/dealer-snapshot/${id}`, + CREDIT_SCORE_TREND: (id: string | number) => `/v1/dealerdetails/credit-score-trend/${id}`, + SALES_VS_PURCHASE: (id: string | number) => `/v1/dealerdetails/sales-vs-purchase/${id}`, + STOCK_AGE_DISTRIBUTION: (id: string | number) => `/v1/dealerdetails/stock-age-distribution/${id}`, + CREDIT_SCORE_BREAKDOWN: (id: string | number) => `/v1/dealerdetails/credit-score-breakdown/${id}`, + KEY_INSIGHTS: (id: string | number) => `/v1/dealerdetails/key-insights/${id}`, + PRODUCT_WISE_SCORE_TRENDS: (id: string | number) => `/v1/dealerdetails/product-wise-score-trends/${id}`, }, }; diff --git a/src/api/index.ts b/src/api/index.ts index 5eb9fd7..08c7d6d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,6 @@ export { default as axiosInstance } from './axiosInstance'; export * from './endpoints'; export * from './services/auth.service'; +export * from './services/dealer.service'; export * from './types'; // Export other services as they are created diff --git a/src/api/services/dealer.service.ts b/src/api/services/dealer.service.ts new file mode 100644 index 0000000..b0f0e57 --- /dev/null +++ b/src/api/services/dealer.service.ts @@ -0,0 +1,236 @@ +import axiosInstance, { createExtendedTimeoutInstance, setupExtendedInstanceInterceptors } from '../axiosInstance'; +import { API_ENDPOINTS } from '../endpoints'; +import type { ApiResponse, DealerOverviewParams, DealerListParams } from '../types'; + +// Create extended timeout instance for dealers list (120 seconds for large queries) +const extendedTimeoutInstance = createExtendedTimeoutInstance(120000); +setupExtendedInstanceInterceptors(extendedTimeoutInstance); + +/** + * Service for handling dealer related API calls. + */ +export const dealerService = { + /** + * Get dealer overview metrics. + * Supports filtering by state, district, dealer_type, and credit score range. + * Supports AbortController for request cancellation. + */ + getOverview: async ( + params: DealerOverviewParams, + signal?: AbortSignal + ): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.OVERVIEW, + { params, signal } + ); + return response.data; + } catch (error: any) { + // Handle AbortError gracefully (request was cancelled) + if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') { + throw new Error('Request cancelled'); + } + throw error; + } + }, + + /** + * Get list of dealers with pagination and filtering. + * Uses extended timeout (120s) to handle large queries efficiently. + * Supports AbortController for request cancellation. + */ + getDealersList: async ( + params: DealerListParams, + signal?: AbortSignal + ): Promise> => { + try { + const response = await extendedTimeoutInstance.get>( + API_ENDPOINTS.DEALERS.LIST, + { + params, + signal // Support request cancellation + } + ); + return response.data; + } catch (error: any) { + // Handle AbortError gracefully (request was cancelled) + if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') { + throw new Error('Request cancelled'); + } + throw error; + } + }, + + /** + * Get list of unique states + */ + getStates: async (): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.STATES + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get list of districts, optionally filtered by state(s) + * Supports AbortController for request cancellation + */ + getDistricts: async ( + states?: string[], + signal?: AbortSignal + ): Promise> => { + try { + const params: any = {}; + if (states && states.length > 0) { + params.state = states.join(','); + } + + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.DISTRICTS, + { params, signal } + ); + + console.log("getDistricts response:", response.data); + + let data = response.data.data; + let districts: string[] = []; + + if (Array.isArray(data)) { + districts = data; + } else if (typeof data === 'object' && data !== null) { + const arrays = Object.values(data) as string[][]; + districts = arrays.flat(); + districts = Array.from(new Set(districts)); + } + + // Sort districts alphabetically + districts.sort((a, b) => a.localeCompare(b)); + + return { + ...response.data, + data: districts + }; + } catch (error: any) { + // Handle AbortError gracefully (request was cancelled) + if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') { + throw new Error('Request cancelled'); + } + throw error; + } + }, + /** + * Get single dealer details by ID. + */ + getDealerDetails: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.DETAIL(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get dealer snapshot details. + */ + getDealerSnapshot: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.SNAPSHOT(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get credit score trend. + */ + getCreditScoreTrend: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.CREDIT_SCORE_TREND(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get sales vs purchase trend. + */ + getSalesVsPurchase: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.SALES_VS_PURCHASE(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get stock age distribution. + */ + getStockAgeDistribution: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.STOCK_AGE_DISTRIBUTION(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get credit score breakdown. + */ + getCreditScoreBreakdown: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.CREDIT_SCORE_BREAKDOWN(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get key insights. + */ + getKeyInsights: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.KEY_INSIGHTS(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, + + /** + * Get product-wise score trends. + */ + getProductWiseScoreTrends: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.PRODUCT_WISE_SCORE_TRENDS(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, +}; diff --git a/src/api/types.ts b/src/api/types.ts index 235e0a3..a6374f8 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -3,6 +3,12 @@ export interface ApiResponse { message: string; data: T; timestamp: string; + pagination?: { + page: number; + limit: number; + total: number; + totalPages: number; + }; } export interface UserData { @@ -29,3 +35,17 @@ export interface AppUser { role: UserRole; company_name: string; } + +export interface DealerOverviewParams { + state?: string | null; + district?: string | null; + dealer_type?: string; + credit_score_min?: number; + credit_score_max?: number; +} + +export interface DealerListParams extends DealerOverviewParams { + search?: string; + page?: number; + limit?: number; +} diff --git a/src/components/SearchFilters.tsx b/src/components/SearchFilters.tsx index 5669f93..40759de 100644 --- a/src/components/SearchFilters.tsx +++ b/src/components/SearchFilters.tsx @@ -1,5 +1,5 @@ -import { memo, useState, useCallback, useMemo, useEffect } from "react"; -import { Download, ChevronDown } from "lucide-react"; +import { memo, useState, useCallback, useEffect, useMemo } from "react"; +import { Download, ChevronDown, Loader2 } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -7,13 +7,14 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Card } from "@/components/ui/card"; -import { indianStates, stateDistricts } from "@/lib/mockData"; import { useDebounce } from "@/hooks/useDebounce"; +import { dealerService } from "@/api/services/dealer.service"; interface SearchFiltersProps { onSearch: (query: string) => void; onFilter: (filters: FilterState) => void; onDownload: () => void; + isLoading?: boolean; } export interface FilterState { @@ -39,6 +40,7 @@ const StateCheckbox = memo(({ id={`state-${state}`} checked={isChecked} onCheckedChange={(checked) => onToggle(state, checked === true)} + className="!border-[#16a34a] !border-2 data-[state=checked]:!bg-[#16a34a] data-[state=checked]:!border-[#16a34a]" />