270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { State, City } from 'country-state-city';
|
|
|
|
export interface Dealer {
|
|
id: string;
|
|
state: string;
|
|
district: string;
|
|
city: string;
|
|
dealerName: string;
|
|
mfmsId: string;
|
|
noOfProducts: number;
|
|
noOfCompanies: number;
|
|
salesRating: number;
|
|
buyRating: number;
|
|
avgLiquidityCycle: number;
|
|
avgAcknowledgmentCycle: number;
|
|
currentStock: number;
|
|
agedStock: number;
|
|
creditScore: number;
|
|
dealerType: "Retailer" | "Wholesaler";
|
|
totalSales6M: number;
|
|
totalPurchase6M: number;
|
|
stockAge: number;
|
|
mobile?: string;
|
|
aadhaar?: string;
|
|
dealerLicense?: string;
|
|
}
|
|
|
|
// Get all Indian states using country-state-city
|
|
const allInStates = State.getStatesOfCountry('IN');
|
|
|
|
export const indianStates = allInStates.map(s => s.name);
|
|
|
|
// Map state names to their cities (districts)
|
|
export const stateDistricts: Record<string, string[]> = allInStates.reduce((acc, state) => {
|
|
acc[state.name] = City.getCitiesOfState('IN', state.isoCode).map(city => city.name);
|
|
return acc;
|
|
}, {} as Record<string, string[]>);
|
|
|
|
// Dealer names for generation
|
|
const DEALER_NAMES = [
|
|
"Krishna Agro Traders", "Sharma Seeds & Fertilizers", "Patel Farm Solutions",
|
|
"Reddy Agriculture Supplies", "Singh Crop Care", "Kumar Agri Inputs",
|
|
"Gupta Farm Depot", "Verma Seeds Co.", "Joshi Agricultural Store",
|
|
"Mehta Fertilizer House", "Yadav Agro Center", "Shah Farm Products",
|
|
"Desai Agro Services", "Iyer Fertilizers", "Nair Seeds & Supply",
|
|
"Pillai Farm Inputs", "Rao Agri Solutions", "Shetty Crop Care",
|
|
"Malhotra Agricultural Hub", "Chopra Seeds Depot", "Agarwal Farm Center",
|
|
"Jain Agro Supplies", "Bansal Fertilizer Store", "Khanna Agriculture Products",
|
|
"Bhatia Farm Solutions", "Kapoor Agri Depot", "Sethi Seeds Trading",
|
|
"Mittal Crop Care Center", "Arora Farm Supplies", "Sinha Agricultural Store"
|
|
] as const;
|
|
|
|
// Seeded random number generator for consistent data
|
|
const seededRandom = (seed: number): number => {
|
|
const x = Math.sin(seed) * 10000;
|
|
return x - Math.floor(x);
|
|
};
|
|
|
|
// Generate a single dealer with seeded randomness for consistency
|
|
const generateDealer = (index: number): Dealer => {
|
|
const seed = index + 1;
|
|
const stateIndex = Math.floor(seededRandom(seed * 1) * indianStates.length);
|
|
const state = indianStates[stateIndex];
|
|
const districts = stateDistricts[state] || [];
|
|
const districtIndex = districts.length > 0 ? Math.floor(seededRandom(seed * 2) * districts.length) : 0;
|
|
const district = districts[districtIndex] || `District ${state}`;
|
|
|
|
const creditScore = Math.floor(seededRandom(seed * 3) * 951) + 50;
|
|
const dealerType: "Retailer" | "Wholesaler" = seededRandom(seed * 4) > 0.3 ? "Retailer" : "Wholesaler";
|
|
|
|
return {
|
|
id: `DLR${String(index + 1).padStart(6, '0')}`,
|
|
state,
|
|
district,
|
|
city: district,
|
|
dealerName: `${DEALER_NAMES[index % DEALER_NAMES.length]} ${district}`,
|
|
mfmsId: `MFMS${String(Math.floor(seededRandom(seed * 5) * 999999)).padStart(6, '0')}`,
|
|
noOfProducts: Math.floor(seededRandom(seed * 6) * 15) + 3,
|
|
noOfCompanies: Math.floor(seededRandom(seed * 7) * 8) + 2,
|
|
salesRating: Math.floor(seededRandom(seed * 8) * 401) + 600,
|
|
buyRating: Math.floor(seededRandom(seed * 9) * 401) + 600,
|
|
avgLiquidityCycle: Math.floor(seededRandom(seed * 10) * 40) + 10,
|
|
avgAcknowledgmentCycle: Math.floor(seededRandom(seed * 11) * 7) + 1,
|
|
currentStock: Math.floor(seededRandom(seed * 12) * 800) + 100,
|
|
agedStock: Math.floor(seededRandom(seed * 13) * 200),
|
|
creditScore,
|
|
dealerType,
|
|
totalSales6M: Math.floor(seededRandom(seed * 14) * 500) + 100,
|
|
totalPurchase6M: Math.floor(seededRandom(seed * 15) * 700) + 200,
|
|
stockAge: Math.floor(seededRandom(seed * 16) * 60) + 20,
|
|
mobile: `+91 ${Math.floor(seededRandom(seed * 17) * 9000000000) + 1000000000}`,
|
|
aadhaar: `${Math.floor(seededRandom(seed * 18) * 9000) + 1000} ${Math.floor(seededRandom(seed * 19) * 9000) + 1000} ${Math.floor(seededRandom(seed * 20) * 9000) + 1000}`,
|
|
dealerLicense: `DL${String(Math.floor(seededRandom(seed * 21) * 999999)).padStart(6, '0')}`
|
|
};
|
|
};
|
|
|
|
// Lazy dealer cache
|
|
let dealersCache: Dealer[] | null = null;
|
|
const DEALER_COUNT = 2600;
|
|
|
|
// Generate dealers lazily (on first access)
|
|
export const getDealers = (): Dealer[] => {
|
|
if (dealersCache === null) {
|
|
dealersCache = Array.from({ length: DEALER_COUNT }, (_, i) => generateDealer(i));
|
|
}
|
|
return dealersCache;
|
|
};
|
|
|
|
// For backwards compatibility - but prefer getDealers() for lazy loading
|
|
export const dealers = getDealers();
|
|
|
|
// Credit score utilities
|
|
export const getCreditScoreColor = (score: number): 'success' | 'warning' | 'danger' => {
|
|
if (score >= 750) return "success";
|
|
if (score >= 500) return "warning";
|
|
return "danger";
|
|
};
|
|
|
|
export const getCreditScoreLabel = (score: number): string => {
|
|
if (score >= 750) return "Excellent";
|
|
if (score >= 650) return "Good";
|
|
if (score >= 500) return "Fair";
|
|
return "Poor";
|
|
};
|
|
|
|
// Mock score breakdown
|
|
export interface ScoreParameter {
|
|
parameter: string;
|
|
weight: number;
|
|
dealerScore: number;
|
|
remarks: string;
|
|
}
|
|
|
|
// Memoization cache for score breakdown
|
|
const scoreBreakdownCache = new Map<string, ScoreParameter[]>();
|
|
|
|
export const getScoreBreakdown = (dealerId: string, creditScore: number): ScoreParameter[] => {
|
|
const cacheKey = `${dealerId}-${creditScore}`;
|
|
|
|
if (scoreBreakdownCache.has(cacheKey)) {
|
|
return scoreBreakdownCache.get(cacheKey)!;
|
|
}
|
|
|
|
const proportion = creditScore / 1000;
|
|
const breakdown: ScoreParameter[] = [
|
|
{ parameter: "Sales Performance", weight: 25, dealerScore: Math.round(600 * proportion), remarks: "Steady movement" },
|
|
{ parameter: "Purchase Rating", weight: 15, dealerScore: Math.round(700 * proportion), remarks: "Slight inconsistency" },
|
|
{ parameter: "Liquidity Cycle", weight: 25, dealerScore: Math.round(800 * proportion), remarks: "Good cycle" },
|
|
{ parameter: "Acknowledgment Cycle", weight: 10, dealerScore: Math.round(990 * proportion), remarks: "Delay within limit" },
|
|
{ parameter: "Current Stock", weight: 10, dealerScore: Math.round(500 * proportion), remarks: "Healthy inventory" },
|
|
{ parameter: "Ageing", weight: 10, dealerScore: Math.round(150 * proportion), remarks: "Hoarding not noticed" },
|
|
{ parameter: "Regional Risk Factor", weight: 5, dealerScore: 0, remarks: "Contextual adjustment" },
|
|
];
|
|
|
|
scoreBreakdownCache.set(cacheKey, breakdown);
|
|
return breakdown;
|
|
};
|
|
|
|
// Type definitions for chart data
|
|
interface CreditTrendItem {
|
|
month: string;
|
|
score: number;
|
|
}
|
|
|
|
interface SalesPurchaseTrendItem {
|
|
month: string;
|
|
sales: number;
|
|
purchase: number;
|
|
}
|
|
|
|
// Memoization for chart data
|
|
const creditTrendCache = new Map<number, CreditTrendItem[]>();
|
|
|
|
export const getCreditScoreTrend = (creditScore: number): CreditTrendItem[] => {
|
|
if (creditTrendCache.has(creditScore)) {
|
|
return creditTrendCache.get(creditScore)!;
|
|
}
|
|
|
|
const months = ["Jul 2024", "Aug 2024", "Sep 2024", "Oct 2024", "Nov 2024", "Dec 2024"];
|
|
const variation = 20;
|
|
const trend: CreditTrendItem[] = [
|
|
{ month: months[0], score: Math.max(50, Math.min(1000, creditScore - variation)) },
|
|
{ month: months[1], score: Math.max(50, Math.min(1000, creditScore - variation / 2)) },
|
|
{ month: months[2], score: Math.max(50, Math.min(1000, creditScore - variation / 3)) },
|
|
{ month: months[3], score: Math.max(50, Math.min(1000, creditScore - variation / 4)) },
|
|
{ month: months[4], score: Math.max(50, Math.min(1000, creditScore - 2)) },
|
|
{ month: months[5], score: creditScore },
|
|
];
|
|
|
|
creditTrendCache.set(creditScore, trend);
|
|
return trend;
|
|
};
|
|
|
|
const salesPurchaseCache = new Map<string, SalesPurchaseTrendItem[]>();
|
|
|
|
export const getSalesPurchaseTrend = (totalSales6M: number, totalPurchase6M: number): SalesPurchaseTrendItem[] => {
|
|
const cacheKey = `${totalSales6M}-${totalPurchase6M}`;
|
|
|
|
if (salesPurchaseCache.has(cacheKey)) {
|
|
return salesPurchaseCache.get(cacheKey)!;
|
|
}
|
|
|
|
const months = ["Jul 2024", "Aug 2024", "Sep 2024", "Oct 2024", "Nov 2024", "Dec 2024"];
|
|
const avgSales = totalSales6M / 6;
|
|
const avgPurchase = totalPurchase6M / 6;
|
|
|
|
const trend: SalesPurchaseTrendItem[] = [
|
|
{ month: months[0], sales: Math.round(avgSales * 0.93), purchase: Math.round(avgPurchase * 0.95) },
|
|
{ month: months[1], sales: Math.round(avgSales * 1.03), purchase: Math.round(avgPurchase * 1.00) },
|
|
{ month: months[2], sales: Math.round(avgSales * 0.98), purchase: Math.round(avgPurchase * 0.97) },
|
|
{ month: months[3], sales: Math.round(avgSales * 1.07), purchase: Math.round(avgPurchase * 1.05) },
|
|
{ month: months[4], sales: Math.round(avgSales * 1.02), purchase: Math.round(avgPurchase * 1.02) },
|
|
{ month: months[5], sales: Math.round(avgSales * 0.97), purchase: Math.round(avgPurchase * 1.01) },
|
|
];
|
|
|
|
salesPurchaseCache.set(cacheKey, trend);
|
|
return trend;
|
|
};
|
|
|
|
// Static stock age distribution (doesn't need memoization)
|
|
const STOCK_AGE_DISTRIBUTION = [
|
|
{ range: "0-30 Days", value: 35 },
|
|
{ range: "31-60 Days", value: 25 },
|
|
{ range: "61-90 Days", value: 15 },
|
|
{ range: "91-120 Days", value: 10 },
|
|
{ range: "121-150 Days", value: 8 },
|
|
{ range: "151-180 Days", value: 4 },
|
|
{ range: "181+ Days", value: 3 },
|
|
] as const;
|
|
|
|
export const getStockAgeDistribution = () => [...STOCK_AGE_DISTRIBUTION];
|
|
|
|
// Monthly product scores types and memoization
|
|
interface MonthlyProductScore {
|
|
product: string;
|
|
months: string[];
|
|
m1: number;
|
|
m2: number;
|
|
m3: number;
|
|
m4: number;
|
|
m5: number;
|
|
}
|
|
|
|
const monthlyScoresCache = new Map<string, MonthlyProductScore[]>();
|
|
|
|
export const getMonthlyProductScores = (dealerId: string): MonthlyProductScore[] => {
|
|
if (monthlyScoresCache.has(dealerId)) {
|
|
return monthlyScoresCache.get(dealerId)!;
|
|
}
|
|
|
|
const products = ["Urea", "NPK", "DAP", "SSP"];
|
|
const months = ["Aug 2024", "Sep 2024", "Oct 2024", "Nov 2024", "Dec 2024"];
|
|
|
|
// Use seeded random based on dealer ID for consistency
|
|
const seed = parseInt(dealerId.replace('DLR', ''), 10) || 1;
|
|
|
|
const scores: MonthlyProductScore[] = products.map((product, i) => ({
|
|
product,
|
|
months,
|
|
m1: i === 0 ? 925 : Math.floor(seededRandom(seed * (i + 1) * 100) * 200) + 750,
|
|
m2: Math.floor(seededRandom(seed * (i + 1) * 101) * 200) + 750,
|
|
m3: Math.floor(seededRandom(seed * (i + 1) * 102) * 200) + 750,
|
|
m4: Math.floor(seededRandom(seed * (i + 1) * 103) * 200) + 750,
|
|
m5: Math.floor(seededRandom(seed * (i + 1) * 104) * 200) + 750,
|
|
}));
|
|
|
|
monthlyScoresCache.set(dealerId, scores);
|
|
return scores;
|
|
};
|