Files
gg-backend/frontend/src/components/PackageInquiriesTab/PackageInquiriesTab.tsx
T
2026-07-02 16:48:25 +05:30

310 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import { getAllInquiriesApi, HealthInquiry } from '@/api/healthCheck';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Loader2, RefreshCw, ChevronLeft, ChevronRight, CalendarDays } from 'lucide-react';
export default function PackageInquiriesTab() {
const [inquiries, setInquiries] = useState<HealthInquiry[]>([]);
const [loading, setLoading] = useState(true);
const [searchText, setSearchText] = useState('');
const [dateMode, setDateMode] = useState<'preferred' | 'booked'>('preferred');
const [filterDate, setFilterDate] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [createdDate, setCreatedDate] = useState('');
const [createdStartDate, setCreatedStartDate] = useState('');
const [createdEndDate, setCreatedEndDate] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const fetchInquiries = useCallback(async () => {
setLoading(true);
try {
const res = await getAllInquiriesApi(
currentPage,
itemsPerPage,
filterDate,
startDate,
endDate,
createdDate,
createdStartDate,
createdEndDate,
searchText
);
setInquiries(res.data || []);
setTotalItems(res.pagination?.total || 0);
setTotalPages(res.pagination?.totalPages || 1);
} catch (err) {
console.error('Failed to fetch inquiries', err);
} finally {
setLoading(false);
}
}, [
currentPage,
itemsPerPage,
filterDate,
startDate,
endDate,
createdDate,
createdStartDate,
createdEndDate,
searchText,
]);
useEffect(() => {
fetchInquiries();
}, [fetchInquiries]);
const handleModeChange = (newMode: 'preferred' | 'booked') => {
setDateMode(newMode);
setFilterDate('');
setStartDate('');
setEndDate('');
setCreatedDate('');
setCreatedStartDate('');
setCreatedEndDate('');
setCurrentPage(1);
};
useEffect(() => {
setCurrentPage(1);
}, [searchText, filterDate, startDate, endDate, createdDate, createdStartDate, createdEndDate, itemsPerPage]);
const indexOfFirstItem = (currentPage - 1) * itemsPerPage;
const indexOfLastItem = Math.min(currentPage * itemsPerPage, totalItems);
return (
<Card className="shadow-sm">
<CardHeader className="flex flex-col gap-4 border-b pb-4 w-full">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 w-full">
<CardTitle className="text-xl flex items-center gap-2">
<span>Package Inquiries</span>
</CardTitle>
<Button variant="outline" size="sm" onClick={fetchInquiries} disabled={loading} className="text-sm">
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
<div className="grid grid-cols-1 xl:grid-cols-4 gap-4 bg-muted/20 p-4 rounded-lg border w-full">
<div className="flex flex-col gap-1.5 w-full">
<label className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Search Inquiry</label>
<Input
placeholder="Search name, phone, email, or package name..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="text-base h-10 bg-background w-full"
/>
</div>
<div className="flex flex-col gap-1.5 w-full">
<label className="text-xs font-bold text-muted-foreground uppercase tracking-wider flex items-center gap-1">
<CalendarDays className="h-3.5 w-3.5 text-primary" /> Filter Date By
</label>
<select
value={dateMode}
onChange={(e) => handleModeChange(e.target.value as 'preferred' | 'booked')}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-medium focus:ring-2 focus:ring-primary focus:outline-none"
>
<option value="preferred">🗓 Preferred Date</option>
<option value="booked"> Booked Date (Created)</option>
</select>
</div>
<div className="flex items-end gap-2 p-1.5 bg-background rounded-md border border-dashed flex-wrap sm:flex-nowrap w-full">
<div className="flex flex-col gap-1 flex-1 min-w-[80px]">
<span className="text-[10px] font-bold text-muted-foreground uppercase">Specific Day</span>
<Input
type="date"
value={dateMode === 'preferred' ? filterDate : createdDate}
onChange={(e) =>
dateMode === 'preferred' ? setFilterDate(e.target.value) : setCreatedDate(e.target.value)
}
className="text-sm h-8"
disabled={dateMode === 'preferred' ? !!startDate || !!endDate : !!createdStartDate || !!createdEndDate}
/>
</div>
<div className="flex flex-col gap-1 flex-1 min-w-[80px]">
<span className="text-[10px] font-bold text-muted-foreground uppercase">From Range</span>
<Input
type="date"
value={dateMode === 'preferred' ? startDate : createdStartDate}
onChange={(e) =>
dateMode === 'preferred' ? setStartDate(e.target.value) : setCreatedStartDate(e.target.value)
}
className="text-sm h-8"
disabled={dateMode === 'preferred' ? !!filterDate : !!createdDate}
/>
</div>
<div className="flex flex-col gap-1 flex-1 min-w-[80px]">
<span className="text-[10px] font-bold text-muted-foreground uppercase">To Range</span>
<Input
type="date"
value={dateMode === 'preferred' ? endDate : createdEndDate}
onChange={(e) =>
dateMode === 'preferred' ? setEndDate(e.target.value) : setCreatedEndDate(e.target.value)
}
className="text-sm h-8"
disabled={dateMode === 'preferred' ? !!filterDate : !!createdDate}
/>
</div>
</div>
<div className="flex flex-col gap-1.5 w-full">
<label className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Page Sizing</label>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:ring-2 focus:ring-primary focus:outline-none"
>
<option value={5}>5 Rows</option>
<option value={10}>10 Rows</option>
<option value={20}>20 Rows</option>
<option value={50}>50 Rows</option>
</select>
</div>
</div>
</CardHeader>
<CardContent className="p-0 sm:p-6 sm:pt-4">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1100px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[145px] font-bold bg-background text-sm">Preferred Date</TableHead>
<TableHead className="w-[145px] font-bold bg-background text-sm">Booked Date</TableHead>
<TableHead className="w-[220px] font-bold bg-background text-sm">Patient Details</TableHead>
<TableHead className="w-[240px] font-bold bg-background text-sm">Requested Package</TableHead>
<TableHead className="w-[120px] font-bold bg-background text-sm">Demographics</TableHead>
<TableHead className="w-[230px] font-bold bg-background text-sm">Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
</TableCell>
</TableRow>
) : inquiries.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-10 text-base">
No package inquiries matched your active search filters.
</TableCell>
</TableRow>
) : (
inquiries.map((inq) => (
<TableRow key={inq.id} className="hover:bg-muted/50 transition-colors">
<TableCell>
{inq.preferredDate ? (
<div
className={`text-sm font-semibold rounded px-1.5 py-0.5 inline-block ${dateMode === 'preferred' ? 'bg-primary/10 text-primary' : 'bg-muted'}`}
>
{new Date(inq.preferredDate).toLocaleDateString('en-GB')}
</div>
) : (
<span className="text-xs text-muted-foreground italic">Not specified</span>
)}
</TableCell>
<TableCell>
<div
className={`text-sm rounded px-1.5 py-0.5 inline-block ${dateMode === 'booked' ? 'bg-sky-500/10 text-sky-600 font-semibold' : 'text-muted-foreground'}`}
>
{new Date(inq.createdAt).toLocaleDateString('en-GB')}
</div>
</TableCell>
<TableCell>
<div className="font-semibold text-base text-gray-900 truncate">{inq.fullName}</div>
<div className="text-sm text-gray-700 mt-0.5">{inq.mobileNumber}</div>
{inq.email && <div className="text-xs text-muted-foreground truncate">{inq.email}</div>}
</TableCell>
<TableCell>
<div className="font-semibold text-sm line-clamp-2 text-gray-800" title={inq.healthPackage?.name}>
{inq.healthPackage?.name || 'N/A'}
</div>
{inq.healthPackage?.category?.name && (
<Badge variant="secondary" className="text-[10px] font-normal px-2 py-0 mt-1">
{inq.healthPackage.category.name}
</Badge>
)}
</TableCell>
<TableCell>
<div className="font-medium text-sm text-gray-800">{inq.age ? `${inq.age} yrs` : '—'}</div>
<div className="text-xs text-muted-foreground capitalize mt-0.5">
{inq.gender || 'unspecified'}
</div>
</TableCell>
<TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="text-sm italic line-clamp-3 text-muted-foreground whitespace-pre-wrap cursor-pointer leading-relaxed hover:text-foreground transition-colors">
{inq.message || 'No message provided.'}
</div>
</TooltipTrigger>
<TooltipContent className="max-w-md whitespace-pre-wrap p-3 bg-popover text-popover-foreground border shadow-md rounded-md">
{inq.message || 'No message provided.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{!loading && totalItems > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between px-2 py-4 border-t gap-4 mt-2">
<div className="text-sm text-muted-foreground">
Showing <span className="font-semibold">{indexOfFirstItem + 1}</span> to{' '}
<span className="font-semibold">{indexOfLastItem}</span> of{' '}
<span className="font-semibold">{totalItems}</span> inquiries
</div>
<div className="flex items-center gap-6">
<div className="text-sm font-medium text-gray-700">
Page {currentPage} of {totalPages || 1}
</div>
<div className="flex gap-1">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}