patient table done, patient form done
This commit is contained in:
@@ -21,6 +21,16 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Calendar } from "../ui/calendar";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { Button } from "../ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
@@ -116,7 +126,9 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submit() {
|
||||
(document.getElementById("patient-form") as HTMLFormElement | null)?.requestSubmit();
|
||||
(
|
||||
document.getElementById("patient-form") as HTMLFormElement | null
|
||||
)?.requestSubmit();
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -195,9 +207,41 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Date of Birth *</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
</FormControl>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
const localDate = format(date, "yyyy-MM-dd");
|
||||
field.onChange(localDate);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
(date) => date > new Date() // Prevent future dates
|
||||
}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -319,6 +363,32 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
||||
Insurance Information
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status *</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue="active"
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="insuranceProvider"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { CalendarIcon, Search, X } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -18,6 +18,14 @@ import {
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
|
||||
export type SearchCriteria = {
|
||||
searchTerm: string;
|
||||
@@ -75,17 +83,54 @@ export function PatientSearch({
|
||||
<div className="w-full pt-8 pb-4 px-4">
|
||||
<div className="flex gap-2 mb-4">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
placeholder="Search patients..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pr-10"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{searchBy === "dob" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSearch();
|
||||
}}
|
||||
className={cn(
|
||||
"w-full pl-3 pr-20 text-left font-normal",
|
||||
!searchTerm && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{searchTerm ? (
|
||||
format(new Date(searchTerm), "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={searchTerm ? new Date(searchTerm) : undefined}
|
||||
onSelect={(date: Date | undefined) => {
|
||||
if (date) {
|
||||
const formattedDate = format(date, "yyyy-MM-dd");
|
||||
setSearchTerm(String(formattedDate));
|
||||
}
|
||||
}}
|
||||
disabled={(date) => date > new Date()}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
placeholder="Search patients..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pr-10"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{searchTerm && (
|
||||
<button
|
||||
className="absolute right-10 top-3 text-gray-400 hover:text-gray-600"
|
||||
@@ -97,6 +142,7 @@ export function PatientSearch({
|
||||
<X size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
|
||||
onClick={handleSearch}
|
||||
@@ -168,14 +214,59 @@ export function PatientSearch({
|
||||
<label className="text-right text-sm font-medium">
|
||||
Search term
|
||||
</label>
|
||||
<Input
|
||||
className="col-span-3"
|
||||
value={advancedCriteria.searchTerm}
|
||||
onChange={(e) =>
|
||||
updateAdvancedCriteria("searchTerm", e.target.value)
|
||||
}
|
||||
placeholder="Enter search term..."
|
||||
/>
|
||||
{advancedCriteria.searchBy === "dob" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSearch();
|
||||
}}
|
||||
className={cn(
|
||||
"col-span-3 text-left font-normal",
|
||||
!advancedCriteria.searchTerm &&
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{advancedCriteria.searchTerm ? (
|
||||
format(new Date(advancedCriteria.searchTerm), "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={
|
||||
advancedCriteria.searchTerm
|
||||
? new Date(advancedCriteria.searchTerm)
|
||||
: undefined
|
||||
}
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
const formattedDate = format(date, "yyyy-MM-dd");
|
||||
updateAdvancedCriteria(
|
||||
"searchTerm",
|
||||
String(formattedDate)
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={(date) => date > new Date()}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
className="col-span-3"
|
||||
value={advancedCriteria.searchTerm}
|
||||
onChange={(e) =>
|
||||
updateAdvancedCriteria("searchTerm", e.target.value)
|
||||
}
|
||||
placeholder="Enter search term..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Delete, Edit, Eye } from "lucide-react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@@ -38,6 +37,7 @@ import { useAuth } from "@/hooks/use-auth";
|
||||
import { PatientSearch, SearchCriteria } from "./patient-search";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
@@ -67,12 +67,16 @@ interface PatientTableProps {
|
||||
allowEdit?: boolean;
|
||||
allowView?: boolean;
|
||||
allowDelete?: boolean;
|
||||
allowCheckbox?: boolean;
|
||||
onSelectPatient?: (patient: Patient) => void;
|
||||
}
|
||||
|
||||
export function PatientTable({
|
||||
allowEdit,
|
||||
allowView,
|
||||
allowDelete,
|
||||
allowCheckbox,
|
||||
onSelectPatient,
|
||||
}: PatientTableProps) {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
@@ -94,12 +98,32 @@ export function PatientTable({
|
||||
);
|
||||
const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500);
|
||||
|
||||
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleSelectPatient = (patient: Patient) => {
|
||||
const isSelected = selectedPatientId === patient.id;
|
||||
const newSelectedId = isSelected ? null : patient.id;
|
||||
setSelectedPatientId(newSelectedId);
|
||||
|
||||
if (!isSelected && onSelectPatient) {
|
||||
onSelectPatient(patient);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: patientsData,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery<PatientApiResponse, Error>({
|
||||
queryKey: ["patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent" }],
|
||||
queryKey: [
|
||||
"patients",
|
||||
{
|
||||
page: currentPage,
|
||||
search: debouncedSearchCriteria?.searchTerm || "recent",
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim();
|
||||
const isSearch = trimmedTerm && trimmedTerm.length > 0;
|
||||
@@ -149,8 +173,7 @@ export function PatientTable({
|
||||
patients: [],
|
||||
totalCount: 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Update patient mutation
|
||||
const updatePatientMutation = useMutation({
|
||||
@@ -166,7 +189,15 @@ export function PatientTable({
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsAddPatientOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["patients", currentPage] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
"patients",
|
||||
{
|
||||
page: currentPage,
|
||||
search: debouncedSearchCriteria?.searchTerm || "recent",
|
||||
},
|
||||
],
|
||||
});
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient updated successfully!",
|
||||
@@ -189,7 +220,15 @@ export function PatientTable({
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsDeletePatientOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["patients", currentPage] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
"patients",
|
||||
{
|
||||
page: currentPage,
|
||||
search: debouncedSearchCriteria?.searchTerm || "recent",
|
||||
},
|
||||
],
|
||||
});
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient deleted successfully!",
|
||||
@@ -305,6 +344,7 @@ export function PatientTable({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{allowCheckbox && <TableHead>Select</TableHead>}
|
||||
<TableHead>Patient</TableHead>
|
||||
<TableHead>DOB / Gender</TableHead>
|
||||
<TableHead>Contact</TableHead>
|
||||
@@ -344,6 +384,15 @@ export function PatientTable({
|
||||
) : (
|
||||
patientsData?.patients.map((patient) => (
|
||||
<TableRow key={patient.id} className="hover:bg-gray-50">
|
||||
{allowCheckbox && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedPatientId === patient.id}
|
||||
onCheckedChange={() => handleSelectPatient(patient)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
@@ -393,7 +442,7 @@ export function PatientTable({
|
||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
||||
patient.status === "active"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
)}
|
||||
>
|
||||
{patient.status === "active" ? "Active" : "Inactive"}
|
||||
@@ -498,7 +547,7 @@ export function PatientTable({
|
||||
className={`${
|
||||
currentPatient.status === "active"
|
||||
? "text-green-600"
|
||||
: "text-amber-600"
|
||||
: "text-red-600"
|
||||
} font-medium`}
|
||||
>
|
||||
{currentPatient.status.charAt(0).toUpperCase() +
|
||||
|
||||
Reference in New Issue
Block a user