254 lines
9.4 KiB
TypeScript
254 lines
9.4 KiB
TypeScript
import { useParams, useNavigate } from "react-router-dom";
|
|
import { useEffect, useState } from "react";
|
|
import { ArrowLeft, Download } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import { dealerService } from "@/api/services/dealer.service";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
|
|
|
interface ProductScore {
|
|
month: string;
|
|
month_key: string;
|
|
score: number;
|
|
}
|
|
|
|
interface ProductTrend {
|
|
product: string;
|
|
scores: ProductScore[];
|
|
}
|
|
|
|
interface DealerData {
|
|
dealer_id: string;
|
|
dealer_name: string;
|
|
mfms_id: string;
|
|
location: string;
|
|
overall_credit_score: number;
|
|
period: string;
|
|
products: ProductTrend[];
|
|
}
|
|
|
|
const ScoreCard = () => {
|
|
const { id } = useParams();
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const { user, userRole, loading } = useAuth();
|
|
|
|
const [dealerData, setDealerData] = useState<DealerData | null>(null);
|
|
const [fetchingData, setFetchingData] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Fetch data from API
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
if (!id) {
|
|
setFetchingData(false);
|
|
return;
|
|
}
|
|
|
|
setFetchingData(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await dealerService.getProductWiseScoreTrends(id);
|
|
|
|
if (response.success && response.data) {
|
|
setDealerData(response.data);
|
|
} else {
|
|
setError('Failed to fetch dealer score trends');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching score trends:', err);
|
|
setError('Failed to load dealer data');
|
|
} finally {
|
|
setFetchingData(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) {
|
|
navigate('/login');
|
|
}
|
|
// Redirect bank customers away from scorecard
|
|
if (!loading && userRole === 'bank_customer') {
|
|
navigate(`/dealer/${id}`);
|
|
toast({
|
|
title: "Access Denied",
|
|
description: "Score card is not available for your role",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}, [user, userRole, loading, navigate, id, toast]);
|
|
|
|
const handleDownload = () => {
|
|
toast({
|
|
title: "Download Started",
|
|
description: "Exporting score card as Excel...",
|
|
});
|
|
};
|
|
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 750) return "text-[#16A34A] bg-[#E8F5E9]";
|
|
if (score >= 500) return "text-[#F59E0B] bg-[#FEF3C7]";
|
|
return "text-[#EF4444] bg-[#FEE2E2]";
|
|
};
|
|
|
|
const getScoreColorText = (score: number) => {
|
|
if (score >= 750) return "text-[#16A34A]";
|
|
if (score >= 500) return "text-[#F59E0B]";
|
|
return "text-[#EF4444]";
|
|
};
|
|
|
|
// Loading state
|
|
if (loading || fetchingData) {
|
|
return (
|
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
<LoadingSpinner size="lg" label="Loading Score Card..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Error or not found state
|
|
if (error || !dealerData) {
|
|
return (
|
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h2 className="text-2xl font-bold mb-4">{error || 'Dealer Not Found'}</h2>
|
|
<Button onClick={() => navigate("/")}>Back to Dashboard</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Get months from first product's scores
|
|
const months = dealerData.products[0]?.scores?.map(s => s.month) || [];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background font-poppins">
|
|
{/* Header */}
|
|
<header className="bg-[#E8F5E9] border-b border-border py-4 px-4 sm:px-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="icon" onClick={() => navigate(`/dealer/${id}`)} className="rounded-full">
|
|
<ArrowLeft className="h-5 w-5" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-xl sm:text-2xl font-bold text-foreground">Dealer Score Card</h1>
|
|
<p className="text-sm text-muted-foreground">Product-wise Performance Trends</p>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" onClick={handleDownload} className="h-9 px-3 sm:h-10 sm:px-4">
|
|
<Download className="h-4 w-4 sm:mr-2" />
|
|
<span className="hidden sm:inline">Download Excel</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-8 py-4 sm:py-8 space-y-6">
|
|
{/* Card 1: Dealer Info Header */}
|
|
<Card className="p-4 sm:p-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-xl sm:text-2xl font-bold text-foreground mb-1">{dealerData.dealer_name}</h2>
|
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs sm:text-sm text-muted-foreground">
|
|
<span><strong>MFMS ID:</strong> {dealerData.mfms_id}</span>
|
|
<span className="hidden sm:inline">•</span>
|
|
<span><strong>Location:</strong> {dealerData.location}</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-left sm:text-right">
|
|
<p className="text-xs sm:text-sm text-muted-foreground mb-1">Overall Credit Score</p>
|
|
<p className={`text-3xl sm:text-4xl font-bold ${getScoreColorText(dealerData.overall_credit_score)}`}>
|
|
{dealerData.overall_credit_score}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Card 2: Table with Header */}
|
|
<Card className="p-4 sm:p-6">
|
|
<div className="mb-6">
|
|
<h3 className="text-lg sm:text-xl font-bold text-foreground mb-2">
|
|
Month-on-Month Product-wise Score Trends
|
|
</h3>
|
|
<p className="text-xs sm:text-sm text-muted-foreground">
|
|
Scores are calculated based on various performance metrics. Total weightage per product per month: 1000 points
|
|
</p>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="bg-gray-50">
|
|
<th className="border border-gray-300 px-3 sm:px-6 py-3 sm:py-4 text-left font-semibold text-foreground text-xs sm:text-sm">
|
|
Product
|
|
</th>
|
|
{months.map((month: string, idx: number) => (
|
|
<th key={idx} className="border border-gray-300 px-3 sm:px-6 py-3 sm:py-4 text-center font-semibold text-foreground text-xs sm:text-sm">
|
|
{month}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{dealerData.products.map((product: ProductTrend, idx: number) => (
|
|
<tr key={idx} className="hover:bg-gray-50 transition-colors">
|
|
<td className="border border-gray-300 px-3 sm:px-6 py-3 sm:py-4 text-xs sm:text-sm font-medium text-foreground">
|
|
{product.product}
|
|
</td>
|
|
{product.scores.map((scoreData: ProductScore, scoreIdx: number) => (
|
|
<td key={scoreIdx} className="border border-gray-300 px-3 sm:px-6 py-3 sm:py-4 text-center">
|
|
<span className={`inline-block px-2 sm:px-3 py-1 rounded font-semibold text-[10px] sm:text-xs ${getScoreColor(scoreData.score)}`}>
|
|
{scoreData.score}
|
|
</span>
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Card 3: Legend */}
|
|
<Card className="p-4">
|
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-3 text-xs sm:text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 sm:w-4 sm:h-4 bg-[#16A34A] rounded"></div>
|
|
<span className="text-foreground">Excellent (750-1000)</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 sm:w-4 sm:h-4 bg-[#F59E0B] rounded"></div>
|
|
<span className="text-foreground">Good (500-749)</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 sm:w-4 sm:h-4 bg-[#EF4444] rounded"></div>
|
|
<span className="text-foreground">Needs Attention (<500)</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 sm:w-4 sm:h-4 bg-gray-300 rounded"></div>
|
|
<span className="text-foreground">No Data</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Card 4: Footer Note */}
|
|
<Card className="p-6">
|
|
<p className="text-sm text-muted-foreground">
|
|
<strong>Note:</strong> 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.
|
|
</p>
|
|
</Card>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ScoreCard;
|
|
|