335 lines
15 KiB
TypeScript
335 lines
15 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
import { Bell, Mail, Phone, Calendar, User, Loader2 } from 'lucide-react';
|
|
import { useState, useEffect } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { listNonSubmittedDealers, notifyNonSubmittedDealer, type NonSubmittedDealerItem } from '@/services/form16Api';
|
|
|
|
function formatDisplayDate(isoDate: string | null): string {
|
|
if (!isoDate) return '—';
|
|
try {
|
|
const d = new Date(isoDate + 'Z');
|
|
const day = d.getUTCDate();
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
const month = months[d.getUTCMonth()];
|
|
const year = d.getUTCFullYear();
|
|
return `${day}-${month}-${year}`;
|
|
} catch {
|
|
return isoDate;
|
|
}
|
|
}
|
|
|
|
const FY_OPTIONS = [
|
|
{ value: '2024-25', label: '2024-25' },
|
|
{ value: '2023-24', label: '2023-24' },
|
|
{ value: '2022-23', label: '2022-23' },
|
|
{ value: '', label: 'All Years' },
|
|
];
|
|
|
|
export function Form16NonSubmittedDealers() {
|
|
const [notifyingDealer, setNotifyingDealer] = useState<string | null>(null);
|
|
const [dealers, setDealers] = useState<NonSubmittedDealerItem[]>([]);
|
|
const [summary, setSummary] = useState<{
|
|
totalDealers: number;
|
|
nonSubmittedCount: number;
|
|
neverSubmittedCount: number;
|
|
overdue90Count: number;
|
|
}>({ totalDealers: 0, nonSubmittedCount: 0, neverSubmittedCount: 0, overdue90Count: 0 });
|
|
const [loading, setLoading] = useState(true);
|
|
const [financialYearFilter, setFinancialYearFilter] = useState<string>('2024-25');
|
|
|
|
useEffect(() => {
|
|
fetchNonSubmittedDealers();
|
|
}, [financialYearFilter]);
|
|
|
|
const fetchNonSubmittedDealers = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await listNonSubmittedDealers(
|
|
financialYearFilter || undefined
|
|
);
|
|
setDealers(response.dealers);
|
|
setSummary(response.summary);
|
|
} catch (error) {
|
|
console.error('Error fetching non-submitted dealers:', error);
|
|
toast.error('Failed to load non-submitted dealers');
|
|
setDealers([]);
|
|
setSummary({ totalDealers: 0, nonSubmittedCount: 0, neverSubmittedCount: 0, overdue90Count: 0 });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleNotifyDealer = async (dealer: NonSubmittedDealerItem) => {
|
|
setNotifyingDealer(dealer.id);
|
|
try {
|
|
await notifyNonSubmittedDealer(dealer.dealerCode, financialYearFilter || undefined);
|
|
toast.success(`Notification sent to ${dealer.dealerName}`, {
|
|
description: `Reminder sent for missing quarters: ${dealer.missingQuarters.join(', ')}. Last notified column updated.`,
|
|
});
|
|
await fetchNonSubmittedDealers();
|
|
} catch (err) {
|
|
console.error('Notify dealer error:', err);
|
|
toast.error('Failed to send notification');
|
|
} finally {
|
|
setNotifyingDealer(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 min-h-screen bg-gray-50 p-4 md:p-6 w-full">
|
|
<div className="w-full min-w-0">
|
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">
|
|
Non-Submitted Dealers
|
|
</h1>
|
|
<p className="text-sm text-gray-600">
|
|
Dealers who have not submitted Form 16A for one or more quarters
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={financialYearFilter}
|
|
onChange={(e) => setFinancialYearFilter(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 bg-white"
|
|
>
|
|
{FY_OPTIONS.map((opt) => (
|
|
<option key={opt.value || 'all'} value={opt.value}>
|
|
{opt.value ? `FY ${opt.label}` : opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardDescription>Non-Submitted / Total Dealers</CardDescription>
|
|
<CardTitle className="text-3xl text-red-600">
|
|
{loading
|
|
? '...'
|
|
: `${summary.nonSubmittedCount}/${summary.totalDealers}`}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardDescription>Never Submitted</CardDescription>
|
|
<CardTitle className="text-3xl text-amber-600">
|
|
{loading ? '...' : summary.neverSubmittedCount}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardDescription>Overdue (90+ days)</CardDescription>
|
|
<CardTitle className="text-3xl text-orange-600">
|
|
{loading ? '...' : summary.overdue90Count}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Dealers Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Dealer List</CardTitle>
|
|
<CardDescription>
|
|
Dealers with missing Form 16A submissions
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="rounded-lg border overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Dealer Details</TableHead>
|
|
<TableHead>Contact Information</TableHead>
|
|
<TableHead>Location</TableHead>
|
|
<TableHead>Missing Quarters</TableHead>
|
|
<TableHead>Last Submission</TableHead>
|
|
<TableHead>Last Notified</TableHead>
|
|
<TableHead className="text-right">Action</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-12">
|
|
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-2 text-teal-600" />
|
|
<p className="text-gray-500">Loading dealers...</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : dealers.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-12 text-gray-500">
|
|
<p>No dealers with missing quarters found</p>
|
|
<p className="text-sm mt-1">
|
|
All dealers have submitted Form 16A for the selected period
|
|
</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
dealers.map((dealer) => (
|
|
<TableRow
|
|
key={dealer.id}
|
|
className="hover:bg-gray-50 transition-colors"
|
|
>
|
|
<TableCell>
|
|
<div>
|
|
<p className="text-sm font-medium">{dealer.dealerName}</p>
|
|
<p className="text-xs text-gray-500">{dealer.dealerCode}</p>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
|
<Mail className="w-3.5 h-3.5 shrink-0" />
|
|
<span>{dealer.email || '—'}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
|
<Phone className="w-3.5 h-3.5 shrink-0" />
|
|
<span>{dealer.phone || '—'}</span>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-sm">{dealer.location || '—'}</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-wrap gap-1">
|
|
{dealer.missingQuarters.map((quarter, index) => (
|
|
<Badge
|
|
key={index}
|
|
variant="destructive"
|
|
className="text-xs"
|
|
>
|
|
{quarter}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
{dealer.lastSubmissionDate ? (
|
|
<div>
|
|
<p className="text-sm">{formatDisplayDate(dealer.lastSubmissionDate)}</p>
|
|
<p
|
|
className={`text-xs ${
|
|
dealer.daysSinceLastSubmission != null &&
|
|
dealer.daysSinceLastSubmission > 90
|
|
? 'text-red-600'
|
|
: 'text-gray-500'
|
|
}`}
|
|
>
|
|
{dealer.daysSinceLastSubmission} days ago
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-red-50 text-red-700 border-red-200"
|
|
>
|
|
Never Submitted
|
|
</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{dealer.lastNotifiedDate ? (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="space-y-1 cursor-help">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
|
<Calendar className="w-3.5 h-3.5 shrink-0" />
|
|
<span>{formatDisplayDate(dealer.lastNotifiedDate)}</span>
|
|
</div>
|
|
{dealer.notificationCount > 1 && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-blue-50 text-blue-700 border-blue-200"
|
|
>
|
|
{dealer.notificationCount}x
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
|
<User className="w-3.5 h-3.5 shrink-0" />
|
|
<span>{dealer.lastNotifiedBy}</span>
|
|
</div>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="left" className="max-w-xs">
|
|
<div className="space-y-2">
|
|
<p className="text-xs font-medium">
|
|
Notification History ({dealer.notificationCount})
|
|
</p>
|
|
<div className="space-y-1.5">
|
|
{dealer.notificationHistory?.map((notification, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="text-xs border-l-2 border-blue-400 pl-2 py-0.5"
|
|
>
|
|
<div className="flex justify-between gap-3">
|
|
<span className="text-gray-600">
|
|
{notification.date}
|
|
</span>
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs h-4 px-1"
|
|
>
|
|
{notification.method}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-gray-500 text-xs">
|
|
by {notification.notifiedBy}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
) : (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-gray-50 text-gray-600 border-gray-200"
|
|
>
|
|
Never Notified
|
|
</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleNotifyDealer(dealer)}
|
|
disabled={notifyingDealer === dealer.id}
|
|
className="bg-teal-600 hover:bg-teal-700 text-white"
|
|
>
|
|
<Bell className="w-4 h-4 mr-1" />
|
|
{notifyingDealer === dealer.id ? 'Sending...' : 'Notify'}
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|