Files
DentalManagementE/apps/Frontend/src/components/patients/patient-table.tsx
2025-05-21 16:54:25 +05:30

265 lines
9.6 KiB
TypeScript

import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Delete, Edit, Eye, MoreVertical } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod";
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
appointments: true,
});
type Patient = z.infer<typeof PatientSchema>;
interface PatientTableProps {
patients: Patient[];
onEdit: (patient: Patient) => void;
onView: (patient: Patient) => void;
onDelete: (patient: Patient) => void;
}
export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const patientsPerPage = 5;
// Get current patients
const indexOfLastPatient = currentPage * patientsPerPage;
const indexOfFirstPatient = indexOfLastPatient - patientsPerPage;
const currentPatients = patients.slice(indexOfFirstPatient, indexOfLastPatient);
const totalPages = Math.ceil(patients.length / patientsPerPage);
const getInitials = (firstName: string, lastName: string) => {
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
};
const getAvatarColor = (id: number) => {
const colorClasses = [
"bg-blue-500",
"bg-teal-500",
"bg-amber-500",
"bg-rose-500",
"bg-indigo-500",
"bg-green-500",
"bg-purple-500",
];
// This returns a literal string from above — not a generated string
return colorClasses[id % colorClasses.length];
};
const formatDate = (dateString: string | Date) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
day: '2-digit',
month: 'short',
year: 'numeric'
}).format(date);
};
return (
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Patient</TableHead>
<TableHead>DOB / Gender</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Insurance</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentPatients.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
No patients found. Add your first patient to get started.
</TableCell>
</TableRow>
) : (
currentPatients.map((patient) => (
<TableRow key={patient.id} className="hover:bg-gray-50">
<TableCell>
<div className="flex items-center">
<Avatar className={`h-10 w-10 ${getAvatarColor(patient.id)}`}>
<AvatarFallback className="text-white">
{getInitials(patient.firstName, patient.lastName)}
</AvatarFallback>
</Avatar>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{patient.firstName} {patient.lastName}
</div>
<div className="text-sm text-gray-500">
PID-{patient.id.toString().padStart(4, '0')}
</div>
</div>
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{formatDate(patient.dateOfBirth)}
</div>
<div className="text-sm text-gray-500 capitalize">
{patient.gender}
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">{patient.phone}</div>
<div className="text-sm text-gray-500">{patient.email}</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{patient.insuranceProvider ? (
patient.insuranceProvider === 'delta'
? 'Delta Dental'
: patient.insuranceProvider === 'metlife'
? 'MetLife'
: patient.insuranceProvider === 'cigna'
? 'Cigna'
: patient.insuranceProvider === 'aetna'
? 'Aetna'
: patient.insuranceProvider === 'none'
? 'No Insurance'
: patient.insuranceProvider
) : (
'Not specified'
)}
</div>
{patient.insuranceId && (
<div className="text-sm text-gray-500">
ID: {patient.insuranceId}
</div>
)}
</TableCell>
<TableCell>
<Badge
variant={patient.status === 'active' ? 'success' : 'warning'}
className="capitalize"
>
{patient.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button
onClick={() =>
onDelete(patient)
}
className="text-red-600 hover:text-red-900"
aria-label="Delete Staff"
variant="ghost"
size="icon"
>
<Delete/>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(patient)}
className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => onView(patient)}
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
>
<Eye className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{patients.length > patientsPerPage && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200">
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{indexOfFirstPatient + 1}</span> to{" "}
<span className="font-medium">
{Math.min(indexOfLastPatient, patients.length)}
</span>{" "}
of <span className="font-medium">{patients.length}</span> results
</p>
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
{Array.from({ length: totalPages }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage(i + 1);
}}
isActive={currentPage === i + 1}
>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
}}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
)}
</div>
);
}