849 lines
36 KiB
TypeScript
849 lines
36 KiB
TypeScript
import { FileText, Calendar, Building, Eye, MapPin, Navigation, Loader2 } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
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 { Plus } from 'lucide-react';
|
|
import { useState, useEffect } from 'react';
|
|
import { User } from '@/lib/mock-data';
|
|
import { toast } from 'sonner';
|
|
import { API } from '@/api/API';
|
|
import { SlaBadge } from '@/components/sla/SlaBadge';
|
|
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
|
|
import { formatDateTime } from '@/components/ui/utils';
|
|
import {
|
|
getCurrentStageBadgeClass,
|
|
getStatusProgressBarClass,
|
|
} from '@/lib/statusProgressTheme';
|
|
import {
|
|
Pagination,
|
|
PaginationContent,
|
|
PaginationEllipsis,
|
|
PaginationItem,
|
|
PaginationLink,
|
|
PaginationNext,
|
|
PaginationPrevious,
|
|
} from "@/components/ui/pagination";
|
|
|
|
interface RelocationRequestPageProps {
|
|
currentUser: User | null;
|
|
onViewDetails: (id: string) => void;
|
|
}
|
|
|
|
const getApiErrorMessage = (error: any, fallback: string) =>
|
|
error?.response?.data?.message || error?.data?.message || error?.message || fallback;
|
|
|
|
const getStageBadgeClass = (stage: string, requestStatus?: string) =>
|
|
getCurrentStageBadgeClass(stage, requestStatus);
|
|
|
|
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
|
|
const [requests, setRequests] = useState<any[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
|
const [activeTab, setActiveTab] = useState('all');
|
|
const itemsPerPage = 10;
|
|
|
|
// Relocation Creation State (for Super Admin)
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [outlets, setOutlets] = useState<any[]>([]);
|
|
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
|
|
const [newCity, setNewCity] = useState('');
|
|
const [newState, setNewState] = useState('');
|
|
const [newAddress, setNewAddress] = useState('');
|
|
const [reason, setReason] = useState('');
|
|
const [distance, setDistance] = useState('');
|
|
const [propertyType, setPropertyType] = useState('');
|
|
const [expectedDate, setExpectedDate] = useState('');
|
|
const [newLat, setNewLat] = useState('');
|
|
const [newLong, setNewLong] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
// Master Data
|
|
const [states, setStates] = useState<any[]>([]);
|
|
const [districts, setDistricts] = useState<any[]>([]);
|
|
const [selectedStateId, setSelectedStateId] = useState('');
|
|
const [selectedDistrictId, setSelectedDistrictId] = useState('');
|
|
const [masterDataLoading, setMasterDataLoading] = useState(false);
|
|
|
|
// Constants
|
|
const isSuperAdmin = currentUser?.role === 'Super Admin' || currentUser?.roleCode === 'Super Admin';
|
|
|
|
const slaItems = requests.map((r: any) => ({
|
|
entityType: 'relocation',
|
|
entityId: r.id || r.requestId
|
|
}));
|
|
const { get: getSla } = useSlaBatchStatus(slaItems, requests.length > 0);
|
|
|
|
|
|
const isCompletedRequest = (request: any) =>
|
|
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
|
|
|
|
const isRejectedRequest = (request: any) =>
|
|
request.status === 'Rejected' || request.status === 'Revoked' || request.currentStage === 'Rejected';
|
|
|
|
const isPendingReviewRequest = (request: any) =>
|
|
!isCompletedRequest(request) && !isRejectedRequest(request) && String(request.status || '').startsWith('Pending');
|
|
|
|
useEffect(() => {
|
|
fetchRequests();
|
|
}, [currentPage, activeTab]);
|
|
|
|
useEffect(() => {
|
|
if (isSuperAdmin) {
|
|
fetchOutlets();
|
|
fetchMasterData();
|
|
}
|
|
}, [isSuperAdmin]);
|
|
|
|
const handleTabChange = (val: string) => {
|
|
setActiveTab(val);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const fetchOutlets = async () => {
|
|
try {
|
|
const response = await API.getOutlets() as any;
|
|
if (response.data.success) {
|
|
setOutlets(response.data.outlets || response.data.data?.outlets || response.data.data || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fetch outlets error:', error);
|
|
}
|
|
};
|
|
|
|
const fetchMasterData = async () => {
|
|
try {
|
|
setMasterDataLoading(true);
|
|
const [statesRes, districtsRes] = await Promise.all([
|
|
API.getStates().catch(() => ({ success: false })) as Promise<any>,
|
|
API.getDistricts({ limit: 'all' }).catch(() => ({ success: false })) as Promise<any>
|
|
]);
|
|
|
|
const statesData = statesRes?.data?.success ? (statesRes.data.data?.states || statesRes.data.data || []) : [];
|
|
const districtsData = districtsRes?.data?.success ? (districtsRes.data.data?.districts || districtsRes.data.data || []) : [];
|
|
|
|
setStates(statesData);
|
|
setDistricts(districtsData);
|
|
} catch (error) {
|
|
console.error('Fetch master data error:', error);
|
|
} finally {
|
|
setMasterDataLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleStateChange = (stateId: string) => {
|
|
setSelectedStateId(stateId);
|
|
setSelectedDistrictId('');
|
|
const selectedState = states.find(s => s.id === stateId);
|
|
if (selectedState) {
|
|
setNewState(selectedState.name);
|
|
}
|
|
};
|
|
|
|
const handleDistrictChange = (districtId: string) => {
|
|
setSelectedDistrictId(districtId);
|
|
const selectedDistrict = districts.find(d => d.id === districtId);
|
|
if (selectedDistrict) {
|
|
setNewCity(selectedDistrict.name);
|
|
}
|
|
};
|
|
|
|
const handleSubmitRequest = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedOutlet || !newCity || !newState || !newAddress || !reason || !distance || !propertyType || !expectedDate) {
|
|
toast.error('Please fill all mandatory fields');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
const payload = {
|
|
outletId: selectedOutlet.id,
|
|
relocationType: 'Intercity',
|
|
currentAddress: selectedOutlet.address || '',
|
|
currentCity: selectedOutlet.city || '',
|
|
currentState: selectedOutlet.state || '',
|
|
newAddress,
|
|
newCity,
|
|
newState,
|
|
newDistrictId: selectedDistrictId || null,
|
|
newStateId: selectedStateId || null,
|
|
reason,
|
|
distance,
|
|
propertyType,
|
|
proposedDate: expectedDate,
|
|
newLatitude: newLat ? parseFloat(newLat) : null,
|
|
newLongitude: newLong ? parseFloat(newLong) : null,
|
|
currentLatitude: selectedOutlet.latitude || null,
|
|
currentLongitude: selectedOutlet.longitude || null
|
|
};
|
|
|
|
const response = await API.createRelocationRequest(payload) as any;
|
|
if (response.data.success) {
|
|
toast.success(`Relocation request submitted successfully for ${selectedOutlet.name}`);
|
|
setIsDialogOpen(false);
|
|
fetchRequests();
|
|
|
|
// Reset form
|
|
setSelectedOutlet(null);
|
|
setNewCity('');
|
|
setNewState('');
|
|
setNewAddress('');
|
|
setReason('');
|
|
setDistance('');
|
|
setPropertyType('');
|
|
setExpectedDate('');
|
|
setNewLat('');
|
|
setNewLong('');
|
|
}
|
|
} catch (error) {
|
|
console.error('Submit relocation error:', error);
|
|
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const filteredDistricts = selectedStateId
|
|
? districts.filter(d => d.stateId === selectedStateId)
|
|
: districts;
|
|
|
|
|
|
const fetchRequests = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await API.getRelocationRequests({
|
|
page: currentPage,
|
|
limit: itemsPerPage,
|
|
status: activeTab === 'all' ? undefined : activeTab
|
|
}) as any;
|
|
if (response.data.success) {
|
|
setRequests(response.data.requests);
|
|
setPaginationMeta(response.meta);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fetch relocation requests error:', error);
|
|
toast.error('Failed to fetch relocation requests');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// Statistics
|
|
const stats = [
|
|
{
|
|
title: 'Total Requests',
|
|
value: requests.length,
|
|
icon: FileText,
|
|
color: 'bg-blue-500',
|
|
},
|
|
{
|
|
title: 'Pending Review',
|
|
value: requests.filter((r: any) => isPendingReviewRequest(r)).length,
|
|
icon: Calendar,
|
|
color: 'bg-re-red',
|
|
},
|
|
{
|
|
title: 'Completed',
|
|
value: requests.filter((r: any) => isCompletedRequest(r)).length,
|
|
icon: MapPin,
|
|
color: 'bg-green-500',
|
|
},
|
|
{
|
|
title: 'Rejected / Revoked',
|
|
value: requests.filter((r: any) => isRejectedRequest(r)).length,
|
|
icon: Building,
|
|
color: 'bg-red-500',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-slate-900 mb-2">Relocation Request Management</h1>
|
|
<p className="text-slate-600">
|
|
Manage dealer relocation requests - Moving dealership to a new location
|
|
</p>
|
|
<span className="block mt-1 text-slate-500 text-sm">
|
|
• Note: Relocation requests are initiated by the dealer.
|
|
</span>
|
|
</div>
|
|
|
|
{isSuperAdmin && (
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="bg-re-red hover:bg-re-red-hover text-white">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
New Relocation Request
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Submit Relocation Request</DialogTitle>
|
|
<DialogDescription>
|
|
Create a new relocation request on behalf of a dealer
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
|
{/* Select Outlet */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="outlet">Select Outlet to Relocate *</Label>
|
|
<Select
|
|
value={selectedOutlet?.id}
|
|
onValueChange={(val) => setSelectedOutlet(outlets.find(o => o.id === val))}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select an outlet" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{outlets.map((outlet) => (
|
|
<SelectItem key={outlet.id} value={outlet.id}>
|
|
{outlet.name} ({outlet.code})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{selectedOutlet && (
|
|
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2 text-sm">
|
|
<h3 className="text-slate-900 font-medium">Current Location</h3>
|
|
<p className="text-slate-600">{selectedOutlet.address}, {selectedOutlet.city}, {selectedOutlet.state} - {selectedOutlet.pincode}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* New Location Details - State/District Dropdowns */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newState">Proposed State *</Label>
|
|
<Select
|
|
value={selectedStateId}
|
|
onValueChange={handleStateChange}
|
|
required
|
|
disabled={masterDataLoading}
|
|
>
|
|
<SelectTrigger id="newState">
|
|
<SelectValue placeholder={masterDataLoading ? "Loading..." : "Select state"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{states.map((state) => (
|
|
<SelectItem key={state.id} value={state.id}>
|
|
{state.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newCity">Proposed City/District *</Label>
|
|
<Select
|
|
value={selectedDistrictId}
|
|
onValueChange={handleDistrictChange}
|
|
required
|
|
disabled={!selectedStateId || masterDataLoading}
|
|
>
|
|
<SelectTrigger id="newCity">
|
|
<SelectValue placeholder={!selectedStateId ? "Select state first" : masterDataLoading ? "Loading..." : "Select district"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{filteredDistricts.map((district) => (
|
|
<SelectItem key={district.id} value={district.id}>
|
|
{district.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newAddress">Proposed Full Address *</Label>
|
|
<Textarea
|
|
id="newAddress"
|
|
placeholder="Enter detailed address of the proposed new location..."
|
|
value={newAddress}
|
|
onChange={(e) => setNewAddress(e.target.value)}
|
|
rows={3}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="distance">Estimated Distance from Current Location (in km) *</Label>
|
|
<Input
|
|
id="distance"
|
|
type="text"
|
|
placeholder="e.g. 5.5 km"
|
|
value={distance}
|
|
onChange={(e) => setDistance(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="propertyType">Property Type *</Label>
|
|
<Select value={propertyType} onValueChange={setPropertyType} required>
|
|
<SelectTrigger id="propertyType">
|
|
<SelectValue placeholder="Select type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Owned">Owned</SelectItem>
|
|
<SelectItem value="Leased">Leased</SelectItem>
|
|
<SelectItem value="Rented">Rented</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="expectedDate">Expected Relocation Date *</Label>
|
|
<Input
|
|
id="expectedDate"
|
|
type="date"
|
|
value={expectedDate}
|
|
onChange={(e) => setExpectedDate(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Reason */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reason">Reason for Relocation *</Label>
|
|
<Textarea
|
|
id="reason"
|
|
placeholder="Why is this relocation requested?"
|
|
value={reason}
|
|
onChange={(e) => setReason(e.target.value)}
|
|
rows={4}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
className="bg-re-red hover:bg-re-red-hover text-white"
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Submitting...
|
|
</>
|
|
) : (
|
|
'Submit Relocation Request'
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
|
|
</div>
|
|
|
|
{/* Statistics Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{stats.map((stat, index) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<Card key={index}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-slate-600 text-sm">{stat.title}</p>
|
|
<p className="text-slate-900 text-2xl mt-1">{stat.value}</p>
|
|
</div>
|
|
<div className={`${stat.color} w-12 h-12 rounded-lg flex items-center justify-center`}>
|
|
<Icon className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Requests Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Relocation Requests</CardTitle>
|
|
<CardDescription>
|
|
Track and manage all dealership relocation requests across all stages
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
|
<TabsList className="grid w-full grid-cols-4">
|
|
<TabsTrigger value="all">All Requests</TabsTrigger>
|
|
<TabsTrigger value="pending">Pending Review</TabsTrigger>
|
|
<TabsTrigger value="rejected">Rejected / Revoked</TabsTrigger>
|
|
<TabsTrigger value="completed">Completed</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="all" className="mt-4">
|
|
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50">
|
|
<TableHead>Request ID</TableHead>
|
|
<TableHead>Dealer Details</TableHead>
|
|
<TableHead>Relocation Details</TableHead>
|
|
<TableHead>Distance</TableHead>
|
|
<TableHead>Current Stage</TableHead>
|
|
<TableHead>Progress</TableHead>
|
|
<TableHead>Submitted On</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="h-32 text-center">
|
|
<div className="flex flex-col items-center justify-center space-y-2">
|
|
<Loader2 className="w-6 h-6 text-re-red animate-spin" />
|
|
<p className="text-slate-500 text-sm">Loading requests...</p>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : requests.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="h-32 text-center text-slate-500">
|
|
No relocation requests found
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
requests.map((request: any) => (
|
|
<TableRow key={request.id}>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
|
|
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-slate-600 text-sm">
|
|
<span className="text-slate-500">From:</span>
|
|
<span>{request.currentLocation}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-slate-900 text-sm">
|
|
<Navigation className="w-3 h-3 text-re-red" />
|
|
<span className="text-slate-500">To:</span>
|
|
<span>{request.proposedLocation}</span>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
|
{request.distance}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<SlaBadge status={getSla('relocation', request.id || request.requestId)} compact />
|
|
<Badge className={getStageBadgeClass(request.currentStage, request.status)}>
|
|
{request.currentStage}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
|
|
style={{ width: `${request.progressPercentage || 0}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-slate-600 text-sm">{request.progressPercentage || 0}%</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
|
|
<div className="text-slate-600 text-sm">By {request.dealer?.fullName || 'Dealer'}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
>
|
|
<Eye className="w-4 h-4 mr-1" />
|
|
View
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="pending" className="mt-4">
|
|
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50">
|
|
<TableHead>Request ID</TableHead>
|
|
<TableHead>Dealer Details</TableHead>
|
|
<TableHead>Relocation Details</TableHead>
|
|
<TableHead>Current Stage</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="h-32 text-center">
|
|
<Loader2 className="w-6 h-6 text-re-red animate-spin mx-auto" />
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
requests
|
|
.filter((r: any) => isPendingReviewRequest(r))
|
|
.map((request: any) => (
|
|
<TableRow key={request.id}>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
|
|
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
|
|
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<SlaBadge status={getSla('relocation', request.id || request.requestId)} compact />
|
|
<Badge className={getStageBadgeClass(request.currentStage, request.status)}>
|
|
{request.currentStage}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
>
|
|
<Eye className="w-4 h-4 mr-1" />
|
|
View
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="in-progress" className="mt-4">
|
|
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50">
|
|
<TableHead>Request ID</TableHead>
|
|
<TableHead>Dealer Details</TableHead>
|
|
<TableHead>Relocation Details</TableHead>
|
|
<TableHead>Progress</TableHead>
|
|
<TableHead>Current Stage</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="h-32 text-center">
|
|
<Loader2 className="w-6 h-6 text-re-red animate-spin mx-auto" />
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
requests
|
|
.filter((r: any) => isRejectedRequest(r))
|
|
.map((request: any) => (
|
|
<TableRow key={request.id}>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
|
|
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
|
|
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
|
|
style={{ width: `${request.progressPercentage || 0}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-slate-600 text-sm">{request.progressPercentage || 0}%</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<SlaBadge status={getSla('relocation', request.id || request.requestId)} compact />
|
|
<Badge className={getStageBadgeClass(request.currentStage, request.status)}>
|
|
{request.currentStage}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
>
|
|
<Eye className="w-4 h-4 mr-1" />
|
|
View
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="completed" className="mt-4">
|
|
<div className="border border-slate-200 rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50">
|
|
<TableHead>Request ID</TableHead>
|
|
<TableHead>Dealer Details</TableHead>
|
|
<TableHead>Relocation Details</TableHead>
|
|
<TableHead>Submitted On</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="h-32 text-center">
|
|
<Loader2 className="w-6 h-6 text-re-red animate-spin mx-auto" />
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
requests
|
|
.filter((r: any) => isCompletedRequest(r))
|
|
.map((request: any) => (
|
|
<TableRow key={request.id}>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.requestId || request.id}</div>
|
|
<div className="text-slate-600 text-sm">{request.outlet?.code || request.dealerCode}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="font-medium text-slate-900">{request.outlet?.name || request.dealerName}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="text-slate-600 text-sm">From: {request.currentLocation}</div>
|
|
<div className="text-slate-900 text-sm">To: {request.proposedLocation}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="text-slate-900">{formatDateTime(request.createdAt)}</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => onViewDetails(request.id)}
|
|
>
|
|
<Eye className="w-4 h-4 mr-1" />
|
|
View
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</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>
|
|
);
|
|
}
|