D360-Frontend/src/pages/ScoreCard.tsx

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 (&lt;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;