diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 387b656..b73d5e6 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -25,5 +25,6 @@ export const API_ENDPOINTS = { PRODUCT_WISE_SCORE_TRENDS: (id: string | number) => `/v1/dealerdetails/product-wise-score-trends/${id}`, PRODUCT_WISE_SCORE_TRENDS_EXPORT: (id: string | number) => `/v1/dealerdetails/product-wise-score-trends/${id}/export`, COMPARE_DEALER_BUSINESS: (id: string | number) => `/v1/dealerdetails/compare-dealer-business/${id}`, + ACTIVITY_TIMELINE: (id: string | number) => `/v1/dealers/${id}/activity-timeline`, }, }; diff --git a/src/api/services/dealer.service.ts b/src/api/services/dealer.service.ts index be5729d..5a1a8e7 100644 --- a/src/api/services/dealer.service.ts +++ b/src/api/services/dealer.service.ts @@ -258,12 +258,15 @@ export const dealerService = { /** * Get compare dealer business data. + * @param id - Dealer ID + * @param manufacturerSchema - Optional: Manufacturer schema to filter by (for "My Business" calculation) */ - getCompareDealerBusiness: async (id: string | number): Promise> => { + getCompareDealerBusiness: async (id: string | number, manufacturerSchema?: string): Promise> => { try { - const response = await axiosInstance.get>( - API_ENDPOINTS.DEALERS.COMPARE_DEALER_BUSINESS(id) - ); + const url = manufacturerSchema + ? `${API_ENDPOINTS.DEALERS.COMPARE_DEALER_BUSINESS(id)}?manufacturer_schema=${manufacturerSchema}` + : API_ENDPOINTS.DEALERS.COMPARE_DEALER_BUSINESS(id); + const response = await axiosInstance.get>(url); return response.data; } catch (error) { throw error; @@ -292,4 +295,18 @@ export const dealerService = { throw error; } }, + + /** + * Get activity timeline for a dealer (last SMS, Call, Acknowledgment). + */ + getActivityTimeline: async (id: string | number): Promise> => { + try { + const response = await axiosInstance.get>( + API_ENDPOINTS.DEALERS.ACTIVITY_TIMELINE(id) + ); + return response.data; + } catch (error) { + throw error; + } + }, }; diff --git a/src/components/KPICard.tsx b/src/components/KPICard.tsx index a7e3416..4183f7d 100644 --- a/src/components/KPICard.tsx +++ b/src/components/KPICard.tsx @@ -2,6 +2,7 @@ import { memo } from "react"; import { Card } from "@/components/ui/card"; import type { LucideProps } from "lucide-react"; import type { ForwardRefExoticComponent, RefAttributes } from "react"; +import { TrendingUp, TrendingDown } from "lucide-react"; type LucideIcon = ForwardRefExoticComponent & RefAttributes>; @@ -11,6 +12,9 @@ interface KPICardProps { icon: LucideIcon; trend?: string; trendColor?: "success" | "danger" | "default"; + change?: number | null; + changeLabel?: string | null; // e.g., "vs last month", "vs previous period". If null/undefined, change section is hidden + changeUnit?: string; // e.g., "%", "days" } const getTrendColorClass = (trendColor: "success" | "danger" | "default"): string => { @@ -24,7 +28,37 @@ const getTrendColorClass = (trendColor: "success" | "danger" | "default"): strin } }; -export const KPICard = memo(({ title, value, icon: Icon, trend, trendColor = "default" }: KPICardProps) => { +const formatChange = (change: number | null, unit: string = ""): { text: string; isPositive: boolean | null; hasData: boolean } => { + if (change === null || change === undefined) { + return { text: "No historical data", isPositive: null, hasData: false }; + } + + const isPositive = change > 0; + const sign = isPositive ? "+" : ""; + const formattedValue = Math.abs(change).toFixed(1); + + return { + text: `${sign}${formattedValue}${unit ? ` ${unit}` : ""}`, + isPositive, + hasData: true + }; +}; + +export const KPICard = memo(({ + title, + value, + icon: Icon, + trend, + trendColor = "default", + change = null, + changeLabel, + changeUnit = "" +}: KPICardProps) => { + const changeInfo = formatChange(change, changeUnit); + // Show change section only if changeLabel is explicitly provided (not null/undefined) + // This allows cards to opt-out of showing historical data + const showChange = changeLabel !== undefined && changeLabel !== null; + return (
@@ -35,8 +69,42 @@ export const KPICard = memo(({ title, value, icon: Icon, trend, trendColor = "de

{value}

{trend &&

{trend}

} + {showChange && ( +
+ {changeInfo.hasData && changeInfo.isPositive !== null && ( + changeInfo.isPositive ? ( + + ) : ( + + ) + )} +

+ {changeInfo.hasData ? `${changeInfo.text} ${changeLabel}` : changeInfo.text} +

+
+ )}
); +}, (prevProps, nextProps) => { + // Custom comparison to prevent unnecessary re-renders + return ( + prevProps.title === nextProps.title && + prevProps.value === nextProps.value && + prevProps.trend === nextProps.trend && + prevProps.trendColor === nextProps.trendColor && + prevProps.icon === nextProps.icon && + prevProps.change === nextProps.change && + prevProps.changeLabel === nextProps.changeLabel && + prevProps.changeUnit === nextProps.changeUnit + ); }); KPICard.displayName = 'KPICard'; diff --git a/src/components/SearchFilters.tsx b/src/components/SearchFilters.tsx index f9a821e..31ae8e0 100644 --- a/src/components/SearchFilters.tsx +++ b/src/components/SearchFilters.tsx @@ -319,7 +319,7 @@ const SearchFiltersComponent = ({ onSearch, onFilter, onDownload, isLoading = fa ); +}, (prevProps, nextProps) => { + // Custom comparison to prevent unnecessary re-renders + return ( + prevProps.creditScore === nextProps.creditScore && + prevProps.compact === nextProps.compact && + JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) + ); }); CreditScoreTrendChart.displayName = 'CreditScoreTrendChart'; diff --git a/src/components/charts/SalesPurchaseChart.tsx b/src/components/charts/SalesPurchaseChart.tsx index 070f49d..40f6cc5 100644 --- a/src/components/charts/SalesPurchaseChart.tsx +++ b/src/components/charts/SalesPurchaseChart.tsx @@ -38,6 +38,14 @@ export const SalesPurchaseChart = memo(({ ); +}, (prevProps, nextProps) => { + // Custom comparison to prevent unnecessary re-renders + return ( + prevProps.totalSales6M === nextProps.totalSales6M && + prevProps.totalPurchase6M === nextProps.totalPurchase6M && + prevProps.compact === nextProps.compact && + JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) + ); }); SalesPurchaseChart.displayName = 'SalesPurchaseChart'; diff --git a/src/components/charts/StockAgeChart.tsx b/src/components/charts/StockAgeChart.tsx index c03544b..1176b7c 100644 --- a/src/components/charts/StockAgeChart.tsx +++ b/src/components/charts/StockAgeChart.tsx @@ -64,6 +64,9 @@ export const StockAgeChart = memo(({ data }: { data?: any[] }) => { ); +}, (prevProps, nextProps) => { + // Custom comparison to prevent unnecessary re-renders + return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data); }); StockAgeChart.displayName = 'StockAgeChart'; diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx index cb4a49d..1ea9a85 100644 --- a/src/components/common/Pagination.tsx +++ b/src/components/common/Pagination.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo, useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; interface PaginationProps { @@ -14,8 +14,23 @@ export const Pagination = memo(({ onPageChange, maxVisiblePages = 10 }: PaginationProps) => { - const isMobile = typeof window !== 'undefined' ? window.innerWidth < 640 : false; - const effectiveMaxPages = isMobile ? 3 : maxVisiblePages; + // Use state and effect to track window width instead of calculating on every render + const [isMobile, setIsMobile] = useState(() => + typeof window !== 'undefined' ? window.innerWidth < 640 : false + ); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const handleResize = () => { + setIsMobile(window.innerWidth < 640); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const effectiveMaxPages = useMemo(() => isMobile ? 3 : maxVisiblePages, [isMobile, maxVisiblePages]); const handlePrevious = useCallback(() => { onPageChange(Math.max(1, currentPage - 1)); diff --git a/src/components/dealer/ActivityTimeline.tsx b/src/components/dealer/ActivityTimeline.tsx index 481647d..29de73a 100644 --- a/src/components/dealer/ActivityTimeline.tsx +++ b/src/components/dealer/ActivityTimeline.tsx @@ -1,15 +1,80 @@ -import { memo, useState } from 'react'; +import { memo, useState, useEffect } from 'react'; import { MessageSquare, Phone, CheckCircle, ChevronDown, ChevronUp } from 'lucide-react'; import { Card } from '@/components/ui/card'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { useAuth } from '@/hooks/useAuth'; +import { useParams } from 'react-router-dom'; +import { dealerService } from '@/api/services/dealer.service'; +import { LoadingSpinner } from '@/components/common/LoadingSpinner'; + +interface ActivityTimelineData { + lastSMS: { + date: string; + customer: string; + mobile_number?: string; + message?: string; + sms_type?: string; + } | null; + lastCall: { + date: string; + customer: string; + dealer_id?: string | number; + } | null; + lastAcknowledgment: { + date: string; + customer: string; + dealer_id?: string | number; + txn_date?: string; + } | null; +} export const ActivityTimeline = memo(() => { const [isOpen, setIsOpen] = useState(false); + const [activityData, setActivityData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const { userRole } = useAuth(); + const { id: dealerId } = useParams<{ id: string }>(); + + useEffect(() => { + const fetchActivityTimeline = async () => { + if (!dealerId || userRole !== 'helpdesk') return; + + setLoading(true); + setError(null); + try { + const response = await dealerService.getActivityTimeline(dealerId); + if (response.success && response.data) { + setActivityData(response.data); + } + } catch (err: any) { + console.error('Error fetching activity timeline:', err); + setError(err.message || 'Failed to load activity timeline'); + } finally { + setLoading(false); + } + }; + + if (isOpen && dealerId) { + fetchActivityTimeline(); + } + }, [isOpen, dealerId, userRole]); if (userRole !== 'helpdesk') return null; + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return 'N/A'; + try { + return new Date(dateString).toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + } catch { + return 'N/A'; + } + }; + return ( @@ -18,38 +83,70 @@ export const ActivityTimeline = memo(() => { {isOpen ? : } -
-
- -
-

Last SMS

-

- Date: {new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toLocaleDateString()} -

-

Customer: Rahul Sharma

+ {loading ? ( +
+ +
+ ) : error ? ( +
{error}
+ ) : ( +
+
+ +
+

Last SMS

+ {activityData?.lastSMS ? ( + <> +

+ Date: {formatDate(activityData.lastSMS.date)} +

+

+ Customer: {activityData.lastSMS.customer} +

+ + ) : ( +

No SMS data available

+ )} +
+
+
+ +
+

Last Call

+ {activityData?.lastCall ? ( + <> +

+ Date: {formatDate(activityData.lastCall.date)} +

+

+ Customer: {activityData.lastCall.customer} +

+ + ) : ( +

No call data available

+ )} +
+
+
+ +
+

Last Acknowledgment

+ {activityData?.lastAcknowledgment ? ( + <> +

+ Date: {formatDate(activityData.lastAcknowledgment.date)} +

+

+ Customer: {activityData.lastAcknowledgment.customer} +

+ + ) : ( +

No acknowledgment data available

+ )} +
-
- -
-

Last Call

-

- Date: {new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toLocaleDateString()} -

-

Customer: Priya Patel

-
-
-
- -
-

Last Acknowledgment

-

- Date: {new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toLocaleDateString()} -

-

Customer: Amit Kumar

-
-
-
+ )} diff --git a/src/components/dealer/CreditScoreBreakdown.tsx b/src/components/dealer/CreditScoreBreakdown.tsx index 3b28fac..ac08eef 100644 --- a/src/components/dealer/CreditScoreBreakdown.tsx +++ b/src/components/dealer/CreditScoreBreakdown.tsx @@ -1,6 +1,7 @@ import { memo } from 'react'; import { ClipboardList } from 'lucide-react'; import { Card } from '@/components/ui/card'; +import { getCreditScoreColor } from '@/lib/mockData'; import type { ScoreParameter } from '@/lib/mockData'; interface CreditScoreBreakdownProps { @@ -8,6 +9,18 @@ interface CreditScoreBreakdownProps { compact?: boolean; } +const colorClasses = { + success: "bg-success", + warning: "bg-warning", + danger: "bg-danger", +} as const; + +const textColorClasses = { + success: "text-success", + warning: "text-warning", + danger: "text-danger", +} as const; + export const CreditScoreBreakdown = memo(({ scoreBreakdown, compact = false @@ -17,23 +30,26 @@ export const CreditScoreBreakdown = memo(({

Credit Score Breakdown

- {scoreBreakdown.map((param, idx) => ( -
-
- {param.parameter} -
- {param.weight}% - {param.dealerScore} + {scoreBreakdown.map((param, idx) => { + const color = getCreditScoreColor(param.dealerScore); + return ( +
+
+ {param.parameter} +
+ {param.weight}% + {param.dealerScore} +
+
+
+
-
-
-
-
- ))} + ); + })}
); @@ -46,24 +62,27 @@ export const CreditScoreBreakdown = memo(({

Credit Score Breakdown

- {scoreBreakdown.map((param, idx) => ( -
-
- {param.parameter} -
- {param.weight}% - {param.dealerScore} + {scoreBreakdown.map((param, idx) => { + const color = getCreditScoreColor(param.dealerScore); + return ( +
+
+ {param.parameter} +
+ {param.weight}% + {param.dealerScore} +
+
+
+
+

{param.remarks}

-
-
-
-

{param.remarks}

-
- ))} + ); + })}
); diff --git a/src/components/dealer/DealerProfileHeader.tsx b/src/components/dealer/DealerProfileHeader.tsx index 5f93644..a8ffaec 100644 --- a/src/components/dealer/DealerProfileHeader.tsx +++ b/src/components/dealer/DealerProfileHeader.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -24,14 +24,15 @@ export const DealerProfileHeader = memo(({ onViewScoreCard, }: DealerProfileHeaderProps) => { const { userRole } = useAuth(); - const lastUpdated = new Date().toLocaleString('en-GB', { + // Memoize lastUpdated to prevent recalculation on every render + const lastUpdated = useMemo(() => new Date().toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', - }); + }), []); return (
@@ -43,6 +44,37 @@ export const DealerProfileHeader = memo(({

{dealer.dealerName}

+
+
+ MFMS ID: {dealer.mfmsId} + + + {dealer.district} + {dealer.city && dealer.city !== dealer.district ? `, ${dealer.city}` : ''} + {dealer.state ? `, ${dealer.state}` : ''} + + + {dealer.dealerType} + + + + {isActive ? "Active" : "Inactive"} + + +
+ {showScoreCard && ( +
+ +
+ )} +
@@ -65,36 +97,19 @@ export const DealerProfileHeader = memo(({
-
-
- MFMS ID: {dealer.mfmsId} - - {dealer.district}, {dealer.city}, {dealer.state} - - {dealer.dealerType} - - - - {isActive ? "Active" : "Inactive"} - - -
-
- {showScoreCard && ( -
- -
- )}
); +}, (prevProps, nextProps) => { + // Custom comparison function to prevent unnecessary re-renders + return ( + prevProps.dealer.id === nextProps.dealer.id && + prevProps.dealer.creditScore === nextProps.dealer.creditScore && + prevProps.creditColor === nextProps.creditColor && + prevProps.isEligible === nextProps.isEligible && + prevProps.isActive === nextProps.isActive && + prevProps.showScoreCard === nextProps.showScoreCard + ); }); DealerProfileHeader.displayName = 'DealerProfileHeader'; diff --git a/src/components/dealer/DealerSnapshot.tsx b/src/components/dealer/DealerSnapshot.tsx index 7161d65..e5ba311 100644 --- a/src/components/dealer/DealerSnapshot.tsx +++ b/src/components/dealer/DealerSnapshot.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useMemo, useCallback } from 'react'; import { User } from 'lucide-react'; import { Card } from '@/components/ui/card'; import type { Dealer } from '@/lib/mockData'; @@ -9,7 +9,8 @@ interface DealerSnapshotProps { } export const DealerSnapshot = memo(({ dealer, title = "Dealer Snapshot" }: DealerSnapshotProps) => { - const snapshotItems = [ + // Memoize snapshotItems to prevent recreation on every render + const snapshotItems = useMemo(() => [ { label: "Type", value: dealer.dealerType }, { label: "Total Companies Associated", value: dealer.noOfCompanies }, { label: "Active Products", value: dealer.noOfProducts }, @@ -20,7 +21,7 @@ export const DealerSnapshot = memo(({ dealer, title = "Dealer Snapshot" }: Deale { label: "Avg. Stock Age", value: `${dealer.stockAge} Days` }, { label: "Aged Stock (>90 Days)", value: `${dealer.agedStock} MT` }, { label: "Current Stock Quantity", value: `${dealer.currentStock} MT` }, - ]; + ], [dealer]); return ( @@ -69,44 +70,40 @@ interface CompareBusinessSnapshotProps { data: ManufacturerData; } +// Helper function outside component to prevent recreation +const getValue = (value: number | { value: number; unit: string } | undefined, defaultVal: number = 0): number => { + if (value === undefined) return defaultVal; + if (typeof value === 'number') return value; + return value.value || defaultVal; +}; + export const CompareBusinessSnapshot = memo(({ data }: CompareBusinessSnapshotProps) => { - // Helper function to extract value from API response (handles both formats) - const getValue = (value: number | { value: number; unit: string } | undefined, defaultVal: number = 0): number => { - if (value === undefined) return defaultVal; - if (typeof value === 'number') return value; - return value.value || defaultVal; - }; - - // Helper function to get unit from API response - const getUnit = (value: number | { value: number; unit: string } | undefined, defaultUnit: string = ''): string => { - if (value === undefined || typeof value === 'number') return defaultUnit; - return value.unit || defaultUnit; - }; - // Extract values from API response format or fallback to direct values - const type = data.type || 'N/A'; - const totalCompanies = data.total_companies_associated ?? data.totalCompanies ?? 0; - const activeProducts = data.active_products ?? data.activeProducts ?? 0; - const totalSales = getValue(data.total_sales_6m_rolling ?? data.totalSales, 0); - const totalPurchase = getValue(data.total_purchase_6m_rolling ?? data.totalPurchase, 0); - const avgLiquidityCycle = getValue(data.avg_liquidity_cycle_3m_weighted ?? data.avgLiquidityCycle, 0); - const avgAcknowledgmentCycle = getValue(data.avg_acknowledgment_cycle_3m_weighted ?? data.avgAcknowledgmentCycle, 0); - const avgStockAge = getValue(data.avg_stock_age ?? data.avgStockAge, 0); - const agedStock = getValue(data.aged_stock_over_90_days ?? data.agedStock, 0); - const currentStock = getValue(data.current_stock_quantity ?? data.currentStock, 0); + const snapshotItems = useMemo(() => { + const type = data.type || 'N/A'; + const totalCompanies = data.total_companies_associated ?? data.totalCompanies ?? 0; + const activeProducts = data.active_products ?? data.activeProducts ?? 0; + const totalSales = getValue(data.total_sales_6m_rolling ?? data.totalSales, 0); + const totalPurchase = getValue(data.total_purchase_6m_rolling ?? data.totalPurchase, 0); + const avgLiquidityCycle = getValue(data.avg_liquidity_cycle_3m_weighted ?? data.avgLiquidityCycle, 0); + const avgAcknowledgmentCycle = getValue(data.avg_acknowledgment_cycle_3m_weighted ?? data.avgAcknowledgmentCycle, 0); + const avgStockAge = getValue(data.avg_stock_age ?? data.avgStockAge, 0); + const agedStock = getValue(data.aged_stock_over_90_days ?? data.agedStock, 0); + const currentStock = getValue(data.current_stock_quantity ?? data.currentStock, 0); - const snapshotItems = [ - { label: "Type", value: type }, - { label: "Total Companies Associated", value: totalCompanies }, - { label: "Active Products", value: activeProducts }, - { label: "Total Sales (6M Rolling)", value: `${totalSales} MT` }, - { label: "Total Purchase (6M Rolling)", value: `${totalPurchase} MT` }, - { label: "Avg. Liquidity Cycle (3M Weighted)", value: `${avgLiquidityCycle > 0 ? avgLiquidityCycle.toFixed(1) : 0} Days` }, - { label: "Avg. Acknowledgment Cycle (3M Weighted)", value: `${avgAcknowledgmentCycle > 0 ? avgAcknowledgmentCycle.toFixed(1) : 0} Days` }, - { label: "Avg. Stock Age", value: `${avgStockAge} Days` }, - { label: "Aged Stock (>90 Days)", value: `${agedStock} MT` }, - { label: "Current Stock Quantity", value: `${currentStock} MT` }, - ]; + return [ + { label: "Type", value: type }, + { label: "Total Companies Associated", value: totalCompanies }, + { label: "Active Products", value: activeProducts }, + { label: "Total Sales (6M Rolling)", value: `${totalSales} MT` }, + { label: "Total Purchase (6M Rolling)", value: `${totalPurchase} MT` }, + { label: "Avg. Liquidity Cycle (3M Weighted)", value: `${avgLiquidityCycle > 0 ? avgLiquidityCycle.toFixed(1) : 0} Days` }, + { label: "Avg. Acknowledgment Cycle (3M Weighted)", value: `${avgAcknowledgmentCycle > 0 ? avgAcknowledgmentCycle.toFixed(1) : 0} Days` }, + { label: "Avg. Stock Age", value: `${avgStockAge} Days` }, + { label: "Aged Stock (>90 Days)", value: `${agedStock} MT` }, + { label: "Current Stock Quantity", value: `${currentStock} MT` }, + ]; + }, [data]); return ( diff --git a/src/components/dealer/DealerTable.tsx b/src/components/dealer/DealerTable.tsx index 772a57c..cd962b8 100644 --- a/src/components/dealer/DealerTable.tsx +++ b/src/components/dealer/DealerTable.tsx @@ -18,10 +18,10 @@ const TableHeader = memo(() => ( Mobile Number Products Companies - Sales Rating - Buy Rating - Liquidity Cycle - Ack. Cycle + Total Sales (MT) + Total Buy (MT) + LIQ Cycle + ACK Cycle Current Stock Aged Stock Credit Score diff --git a/src/components/dealer/DealerTableRow.tsx b/src/components/dealer/DealerTableRow.tsx index c335946..70ab3a6 100644 --- a/src/components/dealer/DealerTableRow.tsx +++ b/src/components/dealer/DealerTableRow.tsx @@ -32,8 +32,12 @@ export const DealerTableRow = memo(({ {dealer.mobile || "N/A"} {dealer.noOfProducts} {dealer.noOfCompanies} - {dealer.salesRating} - {dealer.buyRating} + + {dealer.totalSales6M > 0 ? parseFloat(Number(dealer.totalSales6M).toFixed(2)).toString() : '0'} MT + + + {dealer.totalPurchase6M > 0 ? parseFloat(Number(dealer.totalPurchase6M).toFixed(2)).toString() : '0'} MT + {dealer.avgLiquidityCycle > 0 ? `${dealer.avgLiquidityCycle.toFixed(1)} Days` : '0 Days'} @@ -51,6 +55,26 @@ export const DealerTableRow = memo(({ ); +}, (prevProps, nextProps) => { + // Custom comparison to prevent unnecessary re-renders + return ( + prevProps.dealer.id === nextProps.dealer.id && + prevProps.dealer.creditScore === nextProps.dealer.creditScore && + prevProps.dealer.state === nextProps.dealer.state && + prevProps.dealer.district === nextProps.dealer.district && + prevProps.dealer.dealerName === nextProps.dealer.dealerName && + prevProps.dealer.mfmsId === nextProps.dealer.mfmsId && + prevProps.dealer.mobile === nextProps.dealer.mobile && + prevProps.dealer.noOfProducts === nextProps.dealer.noOfProducts && + prevProps.dealer.noOfCompanies === nextProps.dealer.noOfCompanies && + prevProps.dealer.totalSales6M === nextProps.dealer.totalSales6M && + prevProps.dealer.totalPurchase6M === nextProps.dealer.totalPurchase6M && + prevProps.dealer.avgLiquidityCycle === nextProps.dealer.avgLiquidityCycle && + prevProps.dealer.avgAcknowledgmentCycle === nextProps.dealer.avgAcknowledgmentCycle && + prevProps.dealer.currentStock === nextProps.dealer.currentStock && + prevProps.dealer.agedStock === nextProps.dealer.agedStock && + prevProps.isHovered === nextProps.isHovered + ); }); DealerTableRow.displayName = 'DealerTableRow'; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index c9b9c5e..7f7cc6b 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'; import type { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { authService, type AppUser, type UserRole } from '@/api'; @@ -157,8 +157,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { navigate('/login'); }; + // Memoize context value to prevent unnecessary re-renders + const contextValue = useMemo( + () => ({ user, session, userRole, loading, signIn, signOut }), + [user, session, userRole, loading, signIn, signOut] + ); + return ( - + {children} ); diff --git a/src/hooks/useDealerOverview.ts b/src/hooks/useDealerOverview.ts index e78fa97..7a06b5d 100644 --- a/src/hooks/useDealerOverview.ts +++ b/src/hooks/useDealerOverview.ts @@ -6,11 +6,10 @@ import { useDebounce } from './useDebounce'; interface DealerOverviewMetrics { totalDealers: number; avgCreditScore: number; + avgCreditScoreChange: number | null; highRiskPercentage: string; avgLiquidityCycle: number; - // Add trend details if available in API response, for now we map what we can - // The UI expects detailed trends which might not be in the simple overview stats - // We'll see what the API returns. + avgLiquidityCycleChange: number | null; } interface UseDealerOverviewReturn { @@ -111,8 +110,10 @@ export function useDealerOverview(filters: FilterState, searchQuery: string): Us setMetrics({ totalDealers: data.totalDealers || data.total_dealers || 0, avgCreditScore: data.avgCreditScore || data.avg_credit_score || 0, + avgCreditScoreChange: data.avgCreditScoreChange !== undefined ? data.avgCreditScoreChange : (data.avg_credit_score_change !== undefined ? data.avg_credit_score_change : null), highRiskPercentage: data.highRiskPercentage || data.high_risk_dealers_percentage || "0.0", - avgLiquidityCycle: data.avgLiquidityCycle || data.avg_liquidity_cycle_days || data.avg_liquidity_cycle || 0 + avgLiquidityCycle: data.avgLiquidityCycle || data.avg_liquidity_cycle_days || data.avg_liquidity_cycle || 0, + avgLiquidityCycleChange: data.avgLiquidityCycleChange !== undefined ? data.avgLiquidityCycleChange : (data.avg_liquidity_cycle_change !== undefined ? data.avg_liquidity_cycle_change : null) }); } else { setError(response.message || 'Failed to fetch overview data'); diff --git a/src/lib/mockData.ts b/src/lib/mockData.ts index cf3a6cb..c425da6 100644 --- a/src/lib/mockData.ts +++ b/src/lib/mockData.ts @@ -23,6 +23,7 @@ export interface Dealer { mobile?: string; aadhaar?: string; dealerLicense?: string; + isActive?: boolean; } // Get all Indian states using country-state-city diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 5de9d2f..74df99c 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -52,10 +52,65 @@ const Dashboard = () => { const displayKPIs = useMemo(() => metrics || { totalDealers: 0, avgCreditScore: 0, + avgCreditScoreChange: null, highRiskPercentage: "0.0", - avgLiquidityCycle: 0 + avgLiquidityCycle: 0, + avgLiquidityCycleChange: null }, [metrics]); + // Generate dynamic labels based on active filters + const getDynamicLabels = useMemo(() => { + const hasFilters = filters.states.length > 0 || + filters.districts.length > 0 || + filters.dealerType !== 'all' || + filters.minCreditScore > 0 || + filters.maxCreditScore < 1000 || + searchQuery.trim().length > 0; + + if (!hasFilters) { + return { + totalDealersTrend: "Active in system", + highRiskTrend: "Score below 500" + }; + } + + // Build filter description + const filterParts: string[] = []; + + if (filters.states.length > 0) { + if (filters.states.length === 1) { + filterParts.push(filters.states[0]); + } else { + filterParts.push(`${filters.states.length} states`); + } + } + + if (filters.districts.length > 0) { + if (filters.districts.length === 1) { + filterParts.push(filters.districts[0]); + } else { + filterParts.push(`${filters.districts.length} districts`); + } + } + + if (filters.dealerType !== 'all') { + filterParts.push(filters.dealerType); + } + + if (searchQuery.trim().length > 0) { + filterParts.push("search results"); + } + + const filterDescription = filterParts.length > 0 + ? filterParts.slice(0, 2).join(", ") + (filterParts.length > 2 ? "..." : "") + : "selected filters"; + + return { + totalDealersTrend: `Active in ${filterDescription}`, + highRiskTrend: "Score below 500" + }; + }, [filters, searchQuery]); + // Redirect to login if not authenticated useEffect(() => { if (!loading && !user) { @@ -181,24 +236,30 @@ const Dashboard = () => { title="Total Dealers" value={displayKPIs.totalDealers?.toLocaleString() || "0"} icon={Users} - trend="Active in system" + trend={getDynamicLabels.totalDealersTrend} /> )} @@ -238,10 +299,10 @@ const Dashboard = () => { Mobile Number Products Companies - Sales Rating - Buy Rating - Liquidity Cycle - Ack. Cycle + Total Sales (MT) + Total Buy (MT) + LIQ Cycle + ACK Cycle Current Stock Aged Stock Credit Score diff --git a/src/pages/DealerProfile.tsx b/src/pages/DealerProfile.tsx index dd1a882..27bac97 100644 --- a/src/pages/DealerProfile.tsx +++ b/src/pages/DealerProfile.tsx @@ -1,5 +1,5 @@ import { useParams, useNavigate } from "react-router-dom"; -import { useEffect, useMemo, useCallback } from "react"; +import { useEffect, useMemo, useCallback, memo } from "react"; import { dealers, getScoreBreakdown, getCreditScoreColor, type Dealer, type ScoreParameter } from "@/lib/mockData"; import { dealerService } from "@/api/services/dealer.service"; import { useState } from "react"; @@ -85,7 +85,8 @@ const DealerProfile = () => { stockAge: d.stock_age || 0, mobile: d.mobile_number || d.mobile, aadhaar: d.aadhaar, - dealerLicense: d.dealer_license || d.license_number + dealerLicense: d.dealer_license || d.license_number, + isActive: d.is_active !== undefined ? d.is_active : true }; } @@ -184,8 +185,7 @@ const DealerProfile = () => { console.log('Setting compareBusinessData from API'); setCompareBusinessData(compareRes.data); } else { - console.log('Compare API failed or no data, will use manufacturerData fallback'); - console.log('manufacturerData:', manufacturerData); + console.log('Compare API failed or no data, will use fallback data from dealer snapshot'); } if (mappedDealer) { @@ -224,11 +224,11 @@ const DealerProfile = () => { [dealer] ); - // Use seeded random for consistent active status + // Get active status from backend data (defaults to true if not provided) const isActive = useMemo(() => { if (!dealer) return false; - const seed = parseInt(dealer.id.replace('DLR', ''), 10) || 1; - return Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) > 0.3; + // Use isActive from dealer data if available, otherwise default to true + return dealer.isActive !== undefined ? dealer.isActive : true; }, [dealer]); const isEligible = dealer ? dealer.creditScore >= 500 : false; @@ -378,4 +378,6 @@ const DealerProfile = () => { ); }; -export default DealerProfile; +// Memoize the component to prevent unnecessary re-renders +const DealerProfileMemo = memo(DealerProfile); +export default DealerProfileMemo; diff --git a/src/pages/ScoreCard.tsx b/src/pages/ScoreCard.tsx index 962dad1..9bbbf5d 100644 --- a/src/pages/ScoreCard.tsx +++ b/src/pages/ScoreCard.tsx @@ -213,6 +213,7 @@ const ScoreCard = () => {

Month-on-Month Product-wise Score Trends

+ {/* TODO: Update explanatory note below to reflect only parameters currently considered in score calculation */}

Scores are calculated based on various performance metrics. Total weightage per product per month: 1000 points

@@ -276,6 +277,7 @@ const ScoreCard = () => { {/* Card 4: Footer Note */} + {/* TODO: Update note below to reflect only parameters currently considered in score calculation */}

Note: This score card provides a detailed view of product-wise performance trends over the last 6 months. Scores are calculated based on sales velocity, purchase consistency, stock management, and payment timeliness for each product category.

diff --git a/vite.config.ts b/vite.config.ts index 40ee368..60db003 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,24 +14,5 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, - /* server: { - proxy: { - '/api': { - target: 'http://localhost:8003 ', - changeOrigin: true, - secure: false, - configure: (proxy, _options) => { - proxy.on('error', (err, _req, _res) => { - console.log('proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req, _res) => { - console.log('Sending Request to the Target:', req.method, req.url); - }); - proxy.on('proxyRes', (proxyRes, req, _res) => { - console.log('Received Response from the Target:', proxyRes.statusCode, req.url); - }); - }, - }, - }, - }, */ + })