first commit

This commit is contained in:
2025-05-08 21:27:29 +05:30
commit 230d5c89f0
343 changed files with 42391 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogTitle,
DialogDescription,
DialogHeader,
DialogContent,
DialogFooter,
} from "@/components/ui/dialog";
import { PatientForm } from "./patient-form";
import { InsertPatient, Patient, UpdatePatient } from "@shared/schema";
import { useToast } from "@/hooks/use-toast";
import { X, Calendar } from "lucide-react";
import { useLocation } from "wouter";
interface AddPatientModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: InsertPatient | UpdatePatient) => void;
isLoading: boolean;
patient?: Patient;
extractedInfo?: {
firstName: string;
lastName: string;
dateOfBirth: string;
insuranceId: string;
};
}
// Define the ref type
export type AddPatientModalRef = {
shouldSchedule: boolean;
navigateToSchedule: (patientId: number) => void;
};
export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalProps>(function AddPatientModal(props, ref) {
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } = props;
const { toast } = useToast();
const [formData, setFormData] = useState<InsertPatient | UpdatePatient | null>(null);
const isEditing = !!patient;
const [, navigate] = useLocation();
const [saveAndSchedule, setSaveAndSchedule] = useState(false);
// Set up the imperativeHandle to expose functionality to the parent component
useImperativeHandle(ref, () => ({
shouldSchedule: saveAndSchedule,
navigateToSchedule: (patientId: number) => {
navigate(`/appointments?newPatient=${patientId}`);
}
}));
const handleFormSubmit = (data: InsertPatient | UpdatePatient) => {
setFormData(data);
onSubmit(data);
};
const handleSaveAndSchedule = () => {
setSaveAndSchedule(true);
if (formData) {
onSubmit(formData);
} else {
// Trigger form validation by clicking the hidden submit button
document.querySelector('form')?.requestSubmit();
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle>
{isEditing ? "Edit Patient" : "Add New Patient"}
</DialogTitle>
<Button
variant="ghost"
size="icon"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogDescription>
{isEditing
? "Update patient information in the form below."
: "Fill out the patient information to add them to your records."}
</DialogDescription>
</DialogHeader>
<PatientForm
patient={patient}
extractedInfo={extractedInfo}
onSubmit={handleFormSubmit}
/>
<DialogFooter className="mt-6">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
{!isEditing && (
<Button
variant="outline"
className="gap-1"
onClick={handleSaveAndSchedule}
disabled={isLoading}
>
<Calendar className="h-4 w-4" />
Save & Schedule
</Button>
)}
<Button
type="submit"
onClick={() => {
if (formData) {
onSubmit(formData);
} else {
// Trigger form validation by clicking the hidden submit button
document.querySelector('form')?.requestSubmit();
}
}}
disabled={isLoading}
>
{isLoading
? isEditing ? "Updating..." : "Saving..."
: isEditing ? "Update Patient" : "Save Patient"
}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
});

View File

@@ -0,0 +1,309 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { insertPatientSchema, InsertPatient, Patient, updatePatientSchema, UpdatePatient } from "@shared/schema";
import { useAuth } from "@/hooks/use-auth";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface PatientFormProps {
patient?: Patient;
extractedInfo?: {
firstName: string;
lastName: string;
dateOfBirth: string;
insuranceId: string;
};
onSubmit: (data: InsertPatient | UpdatePatient) => void;
}
export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormProps) {
const { user } = useAuth();
const isEditing = !!patient;
const schema = isEditing ? updatePatientSchema : insertPatientSchema.extend({
userId: z.number().optional(),
});
// Merge extracted info into default values if available
const defaultValues = {
firstName: extractedInfo?.firstName || "",
lastName: extractedInfo?.lastName || "",
dateOfBirth: extractedInfo?.dateOfBirth || "",
gender: "",
phone: "",
email: "",
address: "",
city: "",
zipCode: "",
insuranceProvider: "",
insuranceId: extractedInfo?.insuranceId || "",
groupNumber: "",
policyHolder: "",
allergies: "",
medicalConditions: "",
status: "active",
userId: user?.id,
};
const form = useForm<InsertPatient | UpdatePatient>({
resolver: zodResolver(schema),
defaultValues: patient || defaultValues,
});
const handleSubmit = (data: InsertPatient | UpdatePatient) => {
onSubmit(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
{/* Personal Information */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-3">Personal Information</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dateOfBirth"
render={({ field }) => (
<FormItem>
<FormLabel>Date of Birth *</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gender"
render={({ field }) => (
<FormItem>
<FormLabel>Gender *</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value as string}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select gender" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Contact Information */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-3">Contact Information</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone Number *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Address</FormLabel>
<FormControl>
<Input {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>City</FormLabel>
<FormControl>
<Input {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="zipCode"
render={({ field }) => (
<FormItem>
<FormLabel>ZIP Code</FormLabel>
<FormControl>
<Input {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Insurance Information */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-3">Insurance Information</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="insuranceProvider"
render={({ field }) => (
<FormItem>
<FormLabel>Insurance Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value as string || ''}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="placeholder">Select provider</SelectItem>
<SelectItem value="delta">Delta Dental</SelectItem>
<SelectItem value="metlife">MetLife</SelectItem>
<SelectItem value="cigna">Cigna</SelectItem>
<SelectItem value="aetna">Aetna</SelectItem>
<SelectItem value="other">Other</SelectItem>
<SelectItem value="none">None</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="insuranceId"
render={({ field }) => (
<FormItem>
<FormLabel>Insurance ID</FormLabel>
<FormControl>
<Input {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="groupNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Group Number</FormLabel>
<FormControl>
<Input {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="policyHolder"
render={({ field }) => (
<FormItem>
<FormLabel>Policy Holder (if not self)</FormLabel>
<FormControl>
<Input {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Hidden submit button for form validation */}
<button type="submit" className="hidden" aria-hidden="true"></button>
</form>
</Form>
);
}

View File

@@ -0,0 +1,185 @@
import { useState } from "react";
import { Search, X } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog";
export type SearchCriteria = {
searchTerm: string;
searchBy: "name" | "insuranceProvider" | "phone" | "insuranceId" | "all";
};
interface PatientSearchProps {
onSearch: (criteria: SearchCriteria) => void;
onClearSearch: () => void;
isSearchActive: boolean;
}
export function PatientSearch({
onSearch,
onClearSearch,
isSearchActive,
}: PatientSearchProps) {
const [searchTerm, setSearchTerm] = useState("");
const [searchBy, setSearchBy] = useState<SearchCriteria["searchBy"]>("all");
const [showAdvanced, setShowAdvanced] = useState(false);
const [advancedCriteria, setAdvancedCriteria] = useState<SearchCriteria>({
searchTerm: "",
searchBy: "all",
});
const handleSearch = () => {
onSearch({
searchTerm,
searchBy,
});
};
const handleClear = () => {
setSearchTerm("");
setSearchBy("all");
onClearSearch();
};
const handleAdvancedSearch = () => {
onSearch(advancedCriteria);
setShowAdvanced(false);
};
const updateAdvancedCriteria = (field: keyof SearchCriteria, value: string) => {
setAdvancedCriteria({
...advancedCriteria,
[field]: value,
});
};
return (
<div className="w-full">
<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();
}
}}
/>
{searchTerm && (
<button
className="absolute right-10 top-3 text-gray-400 hover:text-gray-600"
onClick={() => {
setSearchTerm("");
if (isSearchActive) onClearSearch();
}}
>
<X size={16} />
</button>
)}
<button
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
onClick={handleSearch}
>
<Search size={16} />
</button>
</div>
<Select
value={searchBy}
onValueChange={(value) => setSearchBy(value as SearchCriteria["searchBy"])}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Search by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Fields</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="phone">Phone</SelectItem>
<SelectItem value="insuranceProvider">Insurance Provider</SelectItem>
<SelectItem value="insuranceId">Insurance ID</SelectItem>
</SelectContent>
</Select>
<Dialog open={showAdvanced} onOpenChange={setShowAdvanced}>
<DialogTrigger asChild>
<Button variant="outline">Advanced</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[550px]">
<DialogHeader>
<DialogTitle>Advanced Search</DialogTitle>
<DialogDescription>
Search for patients using multiple criteria
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label className="text-right text-sm font-medium">Search by</label>
<Select
value={advancedCriteria.searchBy}
onValueChange={(value) =>
updateAdvancedCriteria("searchBy", value as SearchCriteria["searchBy"])
}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="All Fields" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Fields</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="phone">Phone</SelectItem>
<SelectItem value="insuranceProvider">Insurance Provider</SelectItem>
<SelectItem value="insuranceId">Insurance ID</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<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..."
/>
</div>
</div>
<div className="flex justify-end gap-2">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={handleAdvancedSearch}>Search</Button>
</div>
</DialogContent>
</Dialog>
{isSearchActive && (
<Button variant="outline" onClick={handleClear}>
Clear
</Button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,242 @@
import { useState } from "react";
import { Patient } from "@shared/schema";
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 { 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";
interface PatientTableProps {
patients: Patient[];
onEdit: (patient: Patient) => void;
onView: (patient: Patient) => void;
}
export function PatientTable({ patients, onEdit, onView }: 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 colors = [
"bg-blue-500",
"bg-teal-500",
"bg-amber-500",
"bg-rose-500",
"bg-indigo-500",
"bg-green-500",
"bg-purple-500",
];
return colors[id % colors.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
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>
);
}