Dealer_Onboard_Frontend/src/features/termination/pages/TerminationPage.tsx

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>
);
}