831 lines
35 KiB
TypeScript
831 lines
35 KiB
TypeScript
import { AlertTriangle, Calendar, Plus, Eye, XCircle } from 'lucide-react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useState, useEffect } from 'react';
|
|
import { API } from '@/api/API';
|
|
import { slaService, SlaStatusSnapshot } from '@/services/sla.service';
|
|
import { SlaBadge } from '@/components/sla/SlaBadge';
|
|
import { formatDateTime } from '@/components/ui/utils';
|
|
import {
|
|
Pagination,
|
|
PaginationContent,
|
|
PaginationEllipsis,
|
|
PaginationItem,
|
|
PaginationLink,
|
|
PaginationNext,
|
|
PaginationPrevious,
|
|
} from "@/components/ui/pagination";
|
|
import { User } from '@/lib/mock-data';
|
|
import { toast } from 'sonner';
|
|
import {
|
|
formatTerminationStatusLabel,
|
|
LAST_WORKING_DAY_LABEL,
|
|
PROPOSED_LAST_WORKING_DAY_LABEL
|
|
} from '@/lib/terminationDisplay';
|
|
|
|
interface TerminationPageProps {
|
|
currentUser: User | null;
|
|
onViewDetails: (id: string) => void;
|
|
}
|
|
|
|
const getSeverityColor = (severity: string) => {
|
|
switch (severity) {
|
|
case 'Critical':
|
|
return 'bg-red-100 text-red-700 border-red-300';
|
|
case 'High':
|
|
return 'bg-orange-100 text-orange-700 border-orange-300';
|
|
case 'Medium':
|
|
return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
|
case 'Low':
|
|
return 'bg-blue-100 text-blue-700 border-blue-300';
|
|
default:
|
|
return 'bg-slate-100 text-slate-700 border-slate-300';
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
if (status.includes('Approved') || status.includes('Terminated')) return 'bg-green-100 text-green-700 border-green-300';
|
|
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
|
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
|
|
return 'bg-blue-100 text-blue-700 border-blue-300';
|
|
};
|
|
|
|
const formatStatus = formatTerminationStatusLabel;
|
|
|
|
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [dealers, setDealers] = useState<any[]>([]);
|
|
const [selectedDealerId, setSelectedDealerId] = useState('');
|
|
const [dialogDataLoading, setDialogDataLoading] = useState(false);
|
|
const [dealerCode, setDealerCode] = useState('');
|
|
const [autoFilledData, setAutoFilledData] = useState<any>(null);
|
|
const [terminations, setTerminations] = useState<any[]>([]);
|
|
const [slaById, setSlaById] = useState<Record<string, SlaStatusSnapshot | null>>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
|
const [activeTab, setActiveTab] = useState('all');
|
|
const itemsPerPage = 10;
|
|
const [formData, setFormData] = useState({
|
|
terminationCategory: '',
|
|
reason: '',
|
|
proposedLwd: '',
|
|
comments: '',
|
|
documents: [] as File[]
|
|
});
|
|
|
|
const fetchTerminations = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await API.getTerminations({
|
|
page: currentPage,
|
|
limit: itemsPerPage,
|
|
status: activeTab === 'all' ? undefined : activeTab
|
|
});
|
|
const data = response.data as any;
|
|
if (data?.success) {
|
|
setTerminations(data.terminations);
|
|
setPaginationMeta(data.meta);
|
|
const rows = data.terminations || [];
|
|
if (rows.length) {
|
|
slaService
|
|
.getBatchStatus(rows.map((t: any) => ({ entityType: 'termination', entityId: t.id })))
|
|
.then((slaRes) => {
|
|
if (slaRes?.success) {
|
|
const map: Record<string, SlaStatusSnapshot | null> = {};
|
|
rows.forEach((t: any) => {
|
|
map[t.id] = slaRes.data[`termination:${t.id}`] ?? null;
|
|
});
|
|
setSlaById(map);
|
|
}
|
|
})
|
|
.catch(() => setSlaById({}));
|
|
} else {
|
|
setSlaById({});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching terminations:', error);
|
|
toast.error('Failed to fetch termination requests');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchTerminations();
|
|
}, [currentPage, activeTab]);
|
|
|
|
const handleTabChange = (val: string) => {
|
|
setActiveTab(val);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isDialogOpen || !canCreateTermination) return;
|
|
let cancelled = false;
|
|
|
|
(async () => {
|
|
try {
|
|
setDialogDataLoading(true);
|
|
const response = await API.getDealers({ onboarded: 'true', activeOnly: 'true' });
|
|
const data = response.data as any;
|
|
if (!cancelled && data?.success) {
|
|
const activeDealers = (Array.isArray(data.data) ? data.data : []).filter((dealer: any) => {
|
|
const dealerStatus = String(dealer?.status || '').toLowerCase();
|
|
const userStatus = String(dealer?.user?.status || '').toLowerCase();
|
|
return dealerStatus === 'active' && dealer?.user?.isActive && userStatus === 'active';
|
|
});
|
|
setDealers(activeDealers);
|
|
}
|
|
} catch (error) {
|
|
if (!cancelled) {
|
|
console.error('Error fetching dealers:', error);
|
|
toast.error('Failed to load dealer list');
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
setDialogDataLoading(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [isDialogOpen]);
|
|
|
|
const mapDealerToFormData = (dealer: any) => ({
|
|
id: dealer.id,
|
|
dealerId: dealer.id,
|
|
dealerCode: dealer.dealerCode?.dealerCode || '',
|
|
legalName: dealer.legalName || 'N/A',
|
|
businessName: dealer.businessName || 'N/A',
|
|
gstNumber: dealer.gstNumber || 'N/A',
|
|
address: dealer.registeredAddress || dealer.application?.preferredLocation || 'N/A',
|
|
city: dealer.application?.city || 'N/A',
|
|
state: dealer.application?.state || 'N/A',
|
|
email: dealer.user?.email || 'N/A',
|
|
phoneNumber: dealer.user?.mobileNumber || 'N/A'
|
|
});
|
|
|
|
const handleDealerSelect = (dealerId: string) => {
|
|
setSelectedDealerId(dealerId);
|
|
const dealer = dealers.find((row: any) => String(row.id) === String(dealerId));
|
|
if (!dealer) {
|
|
setDealerCode('');
|
|
setAutoFilledData(null);
|
|
return;
|
|
}
|
|
|
|
const mappedDealer = mapDealerToFormData(dealer);
|
|
setDealerCode(mappedDealer.dealerCode);
|
|
setAutoFilledData(mappedDealer);
|
|
};
|
|
|
|
const handleDealerCodeChange = (code: string) => {
|
|
setDealerCode(code);
|
|
const normalizedCode = code.trim().toLowerCase();
|
|
|
|
if (!normalizedCode) {
|
|
setSelectedDealerId('');
|
|
setAutoFilledData(null);
|
|
return;
|
|
}
|
|
|
|
const matchedDealer = dealers.find((dealer: any) =>
|
|
String(dealer.dealerCode?.dealerCode || '').toLowerCase() === normalizedCode
|
|
);
|
|
|
|
if (!matchedDealer) {
|
|
setSelectedDealerId('');
|
|
setAutoFilledData(null);
|
|
return;
|
|
}
|
|
|
|
setSelectedDealerId(String(matchedDealer.id));
|
|
setAutoFilledData(mapDealerToFormData(matchedDealer));
|
|
};
|
|
|
|
const isSuperAdmin = currentUser?.role === 'Super Admin';
|
|
const isPresentationMandatory = !isSuperAdmin;
|
|
|
|
const isPptFile = (file: File) => {
|
|
const name = file.name.toLowerCase();
|
|
return name.endsWith('.ppt') || name.endsWith('.pptx');
|
|
};
|
|
|
|
const handleFilesPicked = (files: FileList | null, inputEl?: HTMLInputElement | null) => {
|
|
if (!files || files.length === 0) return;
|
|
|
|
setFormData((prev) => {
|
|
const existing = prev.documents;
|
|
const seen = new Set(existing.map((f) => `${f.name}::${f.size}`));
|
|
const additions: File[] = [];
|
|
Array.from(files).forEach((file) => {
|
|
const key = `${file.name}::${file.size}`;
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
additions.push(file);
|
|
}
|
|
});
|
|
return { ...prev, documents: [...existing, ...additions] };
|
|
});
|
|
|
|
if (inputEl) inputEl.value = '';
|
|
};
|
|
|
|
const removeDocumentAt = (index: number) => {
|
|
setFormData({
|
|
...formData,
|
|
documents: formData.documents.filter((_, i) => i !== index)
|
|
});
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!autoFilledData) {
|
|
toast.error('Please select a dealer');
|
|
return;
|
|
}
|
|
|
|
if (isPresentationMandatory) {
|
|
if (formData.documents.length === 0) {
|
|
toast.error('Please upload at least one Presentation (.ppt or .pptx)');
|
|
return;
|
|
}
|
|
if (!formData.documents.some(isPptFile)) {
|
|
toast.error('At least one PowerPoint file (.ppt or .pptx) is required');
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const dealerId = autoFilledData.dealerId || autoFilledData.id;
|
|
if (!dealerId) {
|
|
toast.error('Dealer record not found for the selected dealer');
|
|
return;
|
|
}
|
|
|
|
let requestBody: any;
|
|
if (formData.documents.length > 0) {
|
|
const fd = new FormData();
|
|
fd.append('dealerId', String(dealerId));
|
|
fd.append('category', formData.terminationCategory);
|
|
fd.append('reason', formData.reason);
|
|
fd.append('proposedLwd', formData.proposedLwd);
|
|
fd.append('comments', formData.comments);
|
|
formData.documents.forEach((file) => fd.append('files', file));
|
|
requestBody = fd;
|
|
} else {
|
|
requestBody = {
|
|
dealerId,
|
|
category: formData.terminationCategory,
|
|
reason: formData.reason,
|
|
proposedLwd: formData.proposedLwd,
|
|
comments: formData.comments
|
|
};
|
|
}
|
|
|
|
const response = await API.createTermination(requestBody);
|
|
const data = response.data as any;
|
|
if (data?.success) {
|
|
toast.success(formData.documents.length > 0
|
|
? 'Termination request and documents submitted'
|
|
: 'Termination request submitted successfully');
|
|
|
|
setIsDialogOpen(false);
|
|
fetchTerminations();
|
|
// Reset form
|
|
setSelectedDealerId('');
|
|
setDealerCode('');
|
|
setDealers([]);
|
|
setAutoFilledData(null);
|
|
setFormData({
|
|
terminationCategory: '',
|
|
reason: '',
|
|
proposedLwd: '',
|
|
comments: '',
|
|
documents: []
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error submitting termination:', error);
|
|
toast.error(error.response?.data?.message || 'Failed to submit termination request');
|
|
}
|
|
};
|
|
|
|
const allowedRoles = ['DD Lead', 'ASM', 'DD Admin', 'DD AM', 'Super Admin'];
|
|
const canCreateTermination = currentUser?.role && allowedRoles.includes(currentUser.role);
|
|
|
|
// Map terminations to tab-specific views (already filtered by backend, but need variables for render)
|
|
const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : [];
|
|
const completedRequests = activeTab === 'completed' || activeTab === 'all' ? terminations : [];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Warning Alert */}
|
|
|
|
|
|
{/* Header Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardDescription>All Cases</CardDescription>
|
|
<CardTitle className="text-3xl">{paginationMeta?.total || 0}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-slate-600">Total Cases</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardDescription>Open</CardDescription>
|
|
<CardTitle className="text-3xl text-orange-600">{activeTab === 'open' ? paginationMeta?.total || 0 : '...'}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-slate-600">Requires Your Action</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardDescription>Completed</CardDescription>
|
|
<CardTitle className="text-3xl text-green-600">{activeTab === 'completed' ? paginationMeta?.total || 0 : '...'}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-slate-600">Finalized</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Termination Requests</CardTitle>
|
|
<CardDescription>
|
|
Manage dealer termination proceedings and legal compliance
|
|
</CardDescription>
|
|
</div>
|
|
{canCreateTermination && (
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="bg-red-600 hover:bg-red-700">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Create Termination Request
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Create Termination Request</DialogTitle>
|
|
<DialogDescription>
|
|
Fill in the details to create a new termination request
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Dealer selection */}
|
|
<div className="space-y-2">
|
|
<Label>Select Dealer *</Label>
|
|
<Select value={selectedDealerId} onValueChange={handleDealerSelect} disabled={dialogDataLoading}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={dialogDataLoading ? 'Loading dealers...' : 'Select dealer'} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{dealers.map((dealer: any) => (
|
|
<SelectItem key={dealer.id} value={String(dealer.id)}>
|
|
{dealer.legalName || dealer.businessName || 'Unnamed Dealer'} - {dealer.dealerCode?.dealerCode || 'No Code'}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Optional dealer code lookup */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="dealerCode">Dealer Code *</Label>
|
|
<Input
|
|
id="dealerCode"
|
|
value={dealerCode}
|
|
onChange={(e) => handleDealerCodeChange(e.target.value)}
|
|
placeholder="Type dealer code to auto-select"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Auto-filled data */}
|
|
{autoFilledData && (
|
|
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg">
|
|
<div>
|
|
<Label className="text-slate-600">Dealer Name (Legal)</Label>
|
|
<p>{autoFilledData.legalName || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-600">Business Name</Label>
|
|
<p>{autoFilledData.businessName || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-600">GST</Label>
|
|
<p>{autoFilledData.gstNumber || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-600">Address</Label>
|
|
<p>{autoFilledData.address}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-600">City/State</Label>
|
|
<p>{autoFilledData.city}, {autoFilledData.state}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-600">Dealer Code</Label>
|
|
<p>{autoFilledData.dealerCode || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-slate-600">Contact</Label>
|
|
<p>{autoFilledData.email} / {autoFilledData.phoneNumber}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{/* Date fields */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Termination Category *</Label>
|
|
<Select value={formData.terminationCategory} onValueChange={(value) => setFormData({...formData, terminationCategory: value})}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select termination category" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Working Capital">Working Capital</SelectItem>
|
|
<SelectItem value="Performance Issues">Performance Issues</SelectItem>
|
|
<SelectItem value="Unethical Practice">Unethical Practice</SelectItem>
|
|
<SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem>
|
|
<SelectItem value="Others">Others</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>{PROPOSED_LAST_WORKING_DAY_LABEL} *</Label>
|
|
<Input
|
|
type="date"
|
|
value={formData.proposedLwd}
|
|
onChange={(e) => setFormData({...formData, proposedLwd: e.target.value})}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reason">Termination Reason *</Label>
|
|
<Input
|
|
id="reason"
|
|
value={formData.reason}
|
|
onChange={(e) => setFormData({...formData, reason: e.target.value})}
|
|
placeholder="Primary reason for termination"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="comments">Additional Comments *</Label>
|
|
<Textarea
|
|
id="comments"
|
|
value={formData.comments}
|
|
onChange={(e) => setFormData({...formData, comments: e.target.value})}
|
|
placeholder="Detailed observations and justification"
|
|
rows={4}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="documents">
|
|
{isPresentationMandatory ? 'Upload Documents *' : 'Upload Supporting Documents'}
|
|
</Label>
|
|
<Input
|
|
id="documents"
|
|
type="file"
|
|
multiple
|
|
accept=".ppt,.pptx,.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
|
onChange={(e) => handleFilesPicked(e.target.files, e.currentTarget)}
|
|
required={isPresentationMandatory && formData.documents.length === 0}
|
|
/>
|
|
{isPresentationMandatory && (
|
|
<p className="text-xs text-slate-500">
|
|
At least one PowerPoint (.ppt / .pptx) is mandatory. You can also attach MOM, dealer commitments, and other supporting files (PDF / DOC / XLS / image).
|
|
</p>
|
|
)}
|
|
{formData.documents.length > 0 && (
|
|
<div className="border rounded-md divide-y bg-slate-50">
|
|
{formData.documents.map((file, idx) => (
|
|
<div key={`${file.name}-${idx}`} className="flex items-center justify-between px-3 py-2 text-sm">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<span className="truncate">{file.name}</span>
|
|
{isPptFile(file) && (
|
|
<Badge className="bg-blue-100 text-blue-700 border-blue-300">Presentation</Badge>
|
|
)}
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeDocumentAt(idx)}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
Remove
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" className="bg-red-600 hover:bg-red-700">
|
|
Submit Request
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
|
<TabsList>
|
|
<TabsTrigger value="all">All Cases</TabsTrigger>
|
|
<TabsTrigger value="open">Open</TabsTrigger>
|
|
<TabsTrigger value="completed">Completed</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="all" className="mt-6">
|
|
<div className="space-y-4">
|
|
{loading ? (
|
|
<div className="text-center py-12">Loading requests...</div>
|
|
) : terminations.length > 0 ? (
|
|
terminations.map((request: any) => (
|
|
<Card key={request.id} className="border-slate-200">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-4 flex-1">
|
|
<div className="p-3 bg-red-100 rounded-lg">
|
|
<XCircle className="w-6 h-6 text-red-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
|
|
|
<Badge className={getSeverityColor(request.severity || 'Medium')}>
|
|
{request.severity || 'Normal'}
|
|
</Badge>
|
|
<Badge className={getStatusColor(request.status)}>
|
|
{formatStatus(request.status)}
|
|
</Badge>
|
|
<SlaBadge status={slaById[request.id]} compact />
|
|
</div>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-slate-600">Dealer Name</p>
|
|
<p>{request.dealer?.businessName || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Location</p>
|
|
<p>{request.dealer?.registeredAddress || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Category</p>
|
|
<p>{request.category}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Current Stage</p>
|
|
<p>{formatStatus(request.currentStage)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">{PROPOSED_LAST_WORKING_DAY_LABEL}</p>
|
|
<div className="flex items-center gap-1">
|
|
<Calendar className="w-4 h-4 text-slate-500" />
|
|
<p>{request.proposedLwd}</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Submitted On</p>
|
|
<p>{formatDateTime(request.createdAt)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
className="ml-4"
|
|
>
|
|
<Eye className="w-4 h-4 mr-2" />
|
|
View Details
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 text-slate-500">
|
|
<p>No termination requests found</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Open Tab */}
|
|
<TabsContent value="open" className="mt-6">
|
|
<div className="space-y-4">
|
|
{openRequests.length > 0 ? (
|
|
openRequests.map((request: any) => (
|
|
<Card key={request.id} className="border-slate-200">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-4 flex-1">
|
|
<div className="p-3 bg-orange-100 rounded-lg">
|
|
<AlertTriangle className="w-6 h-6 text-orange-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
|
|
|
<Badge className={getStatusColor(request.status)}>
|
|
{formatStatus(request.status)}
|
|
</Badge>
|
|
<SlaBadge status={slaById[request.id]} compact />
|
|
</div>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-slate-600">Dealer Name</p>
|
|
<p>{request.dealer?.businessName}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Reason</p>
|
|
<p className="truncate">{request.reason}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Current Stage</p>
|
|
<p>{formatStatus(request.currentStage)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Submitted On</p>
|
|
<p>{formatDateTime(request.createdAt)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
className="ml-4"
|
|
>
|
|
<Eye className="w-4 h-4 mr-2" />
|
|
View Details
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 text-slate-500">
|
|
<XCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
|
<p>No requests requiring your action</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Completed Tab */}
|
|
<TabsContent value="completed" className="mt-6">
|
|
<div className="space-y-4">
|
|
{completedRequests.length > 0 ? (
|
|
completedRequests.map((request: any) => (
|
|
<Card key={request.id} className="border-slate-200">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-4 flex-1">
|
|
<div className="p-3 bg-green-100 rounded-lg">
|
|
<XCircle className="w-6 h-6 text-green-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
|
|
|
|
<Badge className={getStatusColor(request.status)}>
|
|
{formatStatus(request.status)}
|
|
</Badge>
|
|
<SlaBadge status={slaById[request.id]} compact />
|
|
</div>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-slate-600">Dealer Name</p>
|
|
<p>{request.dealer?.businessName}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Closed On</p>
|
|
<p>{formatDateTime(request.updatedAt)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">Termination Category</p>
|
|
<p>{request.category}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-600">{LAST_WORKING_DAY_LABEL}</p>
|
|
<p>{request.proposedLwd}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
className="ml-4"
|
|
>
|
|
<Eye className="w-4 h-4 mr-2" />
|
|
View Details
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 text-slate-500">
|
|
<XCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
|
<p>No completed terminations to display</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{paginationMeta && paginationMeta.totalPages > 1 && (
|
|
<div className="py-4 border-t flex justify-center bg-white rounded-b-lg">
|
|
<Pagination>
|
|
<PaginationContent>
|
|
<PaginationItem>
|
|
<PaginationPrevious
|
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
|
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
/>
|
|
</PaginationItem>
|
|
|
|
{[...Array(paginationMeta.totalPages)].map((_, i) => {
|
|
const pageNum = i + 1;
|
|
if (
|
|
pageNum === 1 ||
|
|
pageNum === paginationMeta.totalPages ||
|
|
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
|
|
) {
|
|
return (
|
|
<PaginationItem key={pageNum}>
|
|
<PaginationLink
|
|
isActive={currentPage === pageNum}
|
|
onClick={() => setCurrentPage(pageNum)}
|
|
className="cursor-pointer"
|
|
>
|
|
{pageNum}
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
if (
|
|
(pageNum === 2 && currentPage > 3) ||
|
|
(pageNum === paginationMeta.totalPages - 1 && currentPage < paginationMeta.totalPages - 2)
|
|
) {
|
|
return <PaginationItem key={pageNum}><PaginationEllipsis /></PaginationItem>;
|
|
}
|
|
return null;
|
|
})}
|
|
|
|
<PaginationItem>
|
|
<PaginationNext
|
|
onClick={() => setCurrentPage(prev => Math.min(paginationMeta.totalPages, prev + 1))}
|
|
className={currentPage === paginationMeta.totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
/>
|
|
</PaginationItem>
|
|
</PaginationContent>
|
|
</Pagination>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|