major functionalities are fixed
This commit is contained in:
2
apps/Frontend/.env.example
Normal file
2
apps/Frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
|
||||
@@ -2,38 +2,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
// import { InsertAppointment, UpdateAppointment, Appointment, Patient } from "@repo/db/shared/schemas";
|
||||
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||
|
||||
import {z} from "zod";
|
||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||
|
||||
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
||||
|
||||
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
}).partial();
|
||||
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
||||
|
||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
|
||||
// Define staff members (should match those in appointments-page.tsx)
|
||||
const staffMembers = [
|
||||
{ id: "doctor1", name: "Dr. Kai Gao", role: "doctor" },
|
||||
{ id: "doctor2", name: "Dr. Jane Smith", role: "doctor" },
|
||||
{ id: "hygienist1", name: "Hygienist One", role: "hygienist" },
|
||||
{ id: "hygienist2", name: "Hygienist Two", role: "hygienist" },
|
||||
{ id: "hygienist3", name: "Hygienist Three", role: "hygienist" },
|
||||
];
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
@@ -53,29 +22,52 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { CalendarIcon, Clock } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
|
||||
const appointmentSchema = z.object({
|
||||
patientId: z.coerce.number().positive(),
|
||||
title: z.string().optional(),
|
||||
date: z.date({
|
||||
required_error: "Appointment date is required",
|
||||
}),
|
||||
startTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
|
||||
message: "Start time must be in format HH:MM",
|
||||
}),
|
||||
endTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
|
||||
message: "End time must be in format HH:MM",
|
||||
}),
|
||||
type: z.string().min(1, "Appointment type is required"),
|
||||
notes: z.string().optional(),
|
||||
status: z.string().default("scheduled"),
|
||||
staff: z.string().default(staffMembers?.[0]?.id ?? "default-id"),
|
||||
import {
|
||||
AppointmentUncheckedCreateInputObjectSchema,
|
||||
PatientUncheckedCreateInputObjectSchema,
|
||||
StaffUncheckedCreateInputObjectSchema,
|
||||
} from "@repo/db/shared/schemas";
|
||||
|
||||
import { z } from "zod";
|
||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
|
||||
|
||||
const insertAppointmentSchema = (
|
||||
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
});
|
||||
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
||||
|
||||
export type AppointmentFormValues = z.infer<typeof appointmentSchema>;
|
||||
const updateAppointmentSchema = (
|
||||
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
)
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial();
|
||||
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
||||
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
interface AppointmentFormProps {
|
||||
appointment?: Appointment;
|
||||
@@ -84,55 +76,77 @@ interface AppointmentFormProps {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function AppointmentForm({
|
||||
appointment,
|
||||
patients,
|
||||
onSubmit,
|
||||
isLoading = false
|
||||
export function AppointmentForm({
|
||||
appointment,
|
||||
patients,
|
||||
onSubmit,
|
||||
isLoading = false,
|
||||
}: AppointmentFormProps) {
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
|
||||
useQuery<Staff[]>({
|
||||
queryKey: ["/api/staffs/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/staffs/");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
"Dr. Kai Gao": "bg-blue-600",
|
||||
"Dr. Jane Smith": "bg-emerald-600",
|
||||
};
|
||||
|
||||
const staffMembers = staffMembersRaw.map((staff) => ({
|
||||
...staff,
|
||||
color: colorMap[staff.name] || "bg-gray-400",
|
||||
}));
|
||||
|
||||
// Get the stored data from session storage
|
||||
const storedDataString = sessionStorage.getItem('newAppointmentData');
|
||||
const storedDataString = sessionStorage.getItem("newAppointmentData");
|
||||
let parsedStoredData = null;
|
||||
|
||||
|
||||
// Try to parse it if it exists
|
||||
if (storedDataString) {
|
||||
try {
|
||||
parsedStoredData = JSON.parse(storedDataString);
|
||||
console.log('Initial appointment data from storage:', parsedStoredData);
|
||||
|
||||
// LOG the specific time values for debugging
|
||||
console.log('Time values in stored data:', {
|
||||
startTime: parsedStoredData.startTime,
|
||||
endTime: parsedStoredData.endTime
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing stored appointment data:', error);
|
||||
console.error("Error parsing stored appointment data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Format the date and times for the form
|
||||
const defaultValues: Partial<AppointmentFormValues> = appointment
|
||||
const defaultValues: Partial<Appointment> = appointment
|
||||
? {
|
||||
patientId: appointment.patientId,
|
||||
title: appointment.title,
|
||||
date: new Date(appointment.date),
|
||||
startTime: typeof appointment.startTime === 'string' ? appointment.startTime.slice(0, 5) : "",
|
||||
endTime: typeof appointment.endTime === 'string' ? appointment.endTime.slice(0, 5) : "",
|
||||
startTime: appointment.startTime || "09:00", // Default "09:00"
|
||||
endTime: appointment.endTime || "09:30", // Default "09:30"
|
||||
type: appointment.type,
|
||||
notes: appointment.notes || "",
|
||||
status: appointment.status || "scheduled",
|
||||
staffId:
|
||||
typeof appointment.staffId === "number"
|
||||
? appointment.staffId
|
||||
: undefined,
|
||||
}
|
||||
: parsedStoredData
|
||||
? {
|
||||
patientId: parsedStoredData.patientId,
|
||||
patientId: Number(parsedStoredData.patientId),
|
||||
date: new Date(parsedStoredData.date),
|
||||
title: parsedStoredData.title || "",
|
||||
startTime: parsedStoredData.startTime, // This should now be correctly applied
|
||||
startTime: parsedStoredData.startTime,
|
||||
endTime: parsedStoredData.endTime,
|
||||
type: parsedStoredData.type || "checkup",
|
||||
status: parsedStoredData.status || "scheduled",
|
||||
notes: parsedStoredData.notes || "",
|
||||
staff: parsedStoredData.staff || (staffMembers?.[0]?.id ?? "default-id")
|
||||
staffId:
|
||||
typeof parsedStoredData.staff === "number"
|
||||
? parsedStoredData.staff
|
||||
: (staffMembers?.[0]?.id ?? undefined),
|
||||
}
|
||||
: {
|
||||
date: new Date(),
|
||||
@@ -141,342 +155,384 @@ export function AppointmentForm({
|
||||
endTime: "09:30",
|
||||
type: "checkup",
|
||||
status: "scheduled",
|
||||
staff: "doctor1",
|
||||
staffId: staffMembers?.[0]?.id ?? undefined,
|
||||
};
|
||||
|
||||
const form = useForm<AppointmentFormValues>({
|
||||
resolver: zodResolver(appointmentSchema),
|
||||
const form = useForm<InsertAppointment>({
|
||||
resolver: zodResolver(insertAppointmentSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
|
||||
// Force form field values to update and clean up storage
|
||||
useEffect(() => {
|
||||
if (parsedStoredData) {
|
||||
// Force-update the form with the stored values
|
||||
console.log("Force updating form fields with:", parsedStoredData);
|
||||
|
||||
// Update form field values directly
|
||||
if (parsedStoredData.startTime) {
|
||||
form.setValue('startTime', parsedStoredData.startTime);
|
||||
console.log(`Setting startTime to: ${parsedStoredData.startTime}`);
|
||||
form.setValue("startTime", parsedStoredData.startTime);
|
||||
}
|
||||
|
||||
|
||||
if (parsedStoredData.endTime) {
|
||||
form.setValue('endTime', parsedStoredData.endTime);
|
||||
console.log(`Setting endTime to: ${parsedStoredData.endTime}`);
|
||||
form.setValue("endTime", parsedStoredData.endTime);
|
||||
}
|
||||
|
||||
|
||||
if (parsedStoredData.staff) {
|
||||
form.setValue('staff', parsedStoredData.staff);
|
||||
form.setValue("staffId", parsedStoredData.staff);
|
||||
}
|
||||
|
||||
|
||||
if (parsedStoredData.date) {
|
||||
form.setValue('date', new Date(parsedStoredData.date));
|
||||
form.setValue("date", new Date(parsedStoredData.date));
|
||||
}
|
||||
|
||||
|
||||
// Clean up session storage
|
||||
sessionStorage.removeItem('newAppointmentData');
|
||||
sessionStorage.removeItem("newAppointmentData");
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
const handleSubmit = (data: AppointmentFormValues) => {
|
||||
// Convert date to string format for the API and ensure patientId is properly parsed as a number
|
||||
console.log("Form data before submission:", data);
|
||||
|
||||
const handleSubmit = (data: InsertAppointment) => {
|
||||
// Make sure patientId is a number
|
||||
const patientId = typeof data.patientId === 'string'
|
||||
? parseInt(data.patientId, 10)
|
||||
: data.patientId;
|
||||
|
||||
const patientId =
|
||||
typeof data.patientId === "string"
|
||||
? parseInt(data.patientId, 10)
|
||||
: data.patientId;
|
||||
|
||||
// Get patient name for the title
|
||||
const patient = patients.find(p => p.id === patientId);
|
||||
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Patient';
|
||||
|
||||
const patient = patients.find((p) => p.id === patientId);
|
||||
const patientName = patient
|
||||
? `${patient.firstName} ${patient.lastName}`
|
||||
: "Patient";
|
||||
|
||||
// Auto-create title if it's empty
|
||||
let title = data.title;
|
||||
if (!title || title.trim() === '') {
|
||||
if (!title || title.trim() === "") {
|
||||
// Format: "April 19" - just the date
|
||||
title = format(data.date, 'MMMM d');
|
||||
title = format(data.date, "MMMM d");
|
||||
}
|
||||
|
||||
// Make sure notes include staff information (needed for appointment display in columns)
|
||||
let notes = data.notes || '';
|
||||
|
||||
// Get the selected staff member
|
||||
const selectedStaff = staffMembers.find(staff => staff.id === data.staff) || staffMembers[0];
|
||||
|
||||
|
||||
let notes = data.notes || "";
|
||||
|
||||
const selectedStaff =
|
||||
staffMembers.find((staff) => staff.id?.toString() === data.staffId) ||
|
||||
staffMembers[0];
|
||||
|
||||
if (!selectedStaff) {
|
||||
console.error("No staff selected and no available staff in the list");
|
||||
return; // Handle this case as well
|
||||
}
|
||||
|
||||
// If there's no staff information in the notes, add it
|
||||
if (!notes.includes('Appointment with')) {
|
||||
notes = notes ? `${notes}\nAppointment with ${selectedStaff?.name}` : `Appointment with ${selectedStaff?.name}`;
|
||||
if (!notes.includes("Appointment with")) {
|
||||
notes = notes
|
||||
? `${notes}\nAppointment with ${selectedStaff?.name}`
|
||||
: `Appointment with ${selectedStaff?.name}`;
|
||||
}
|
||||
|
||||
|
||||
// 👇 Use current date if none provided
|
||||
const appointmentDate = data.date ? new Date(data.date) : new Date();
|
||||
|
||||
if (isNaN(appointmentDate.getTime())) {
|
||||
console.error("Invalid date:", data.date);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
...data,
|
||||
title,
|
||||
notes,
|
||||
patientId, // Ensure patientId is a number
|
||||
date: format(data.date, 'yyyy-MM-dd'),
|
||||
patientId,
|
||||
date: format(appointmentDate, "yyyy-MM-dd"),
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="patientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Patient</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value?.toString()}
|
||||
defaultValue={field.value?.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a patient" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{patients.map((patient) => (
|
||||
<SelectItem key={patient.id} value={patient.id.toString()}>
|
||||
{patient.firstName} {patient.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<div className="form-container">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(
|
||||
(data) => {
|
||||
handleSubmit(data);
|
||||
},
|
||||
(errors) => {
|
||||
console.error("Validation failed:", errors);
|
||||
}
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Appointment Title <span className="text-muted-foreground text-xs">(optional)</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Leave blank to auto-fill with date"
|
||||
{...field}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="patientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Patient</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
onValueChange={(val) => field.onChange(Number(val))}
|
||||
value={field.value?.toString()}
|
||||
defaultValue={field.value?.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a patient" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date < new Date(new Date().setHours(0, 0, 0, 0))
|
||||
}
|
||||
initialFocus
|
||||
<SelectContent>
|
||||
{patients.map((patient) => (
|
||||
<SelectItem
|
||||
key={patient.id}
|
||||
value={patient.id.toString()}
|
||||
>
|
||||
{patient.firstName} {patient.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Appointment Title{" "}
|
||||
<span className="text-muted-foreground text-xs">
|
||||
(optional)
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Leave blank to auto-fill with date"
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="startTime"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Start Time</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="09:00"
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="endTime"
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>End Time</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="09:30"
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
className="pl-10"
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{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-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date < new Date(new Date().setHours(0, 0, 0, 0))
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Appointment Type</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="checkup">Checkup</SelectItem>
|
||||
<SelectItem value="cleaning">Cleaning</SelectItem>
|
||||
<SelectItem value="filling">Filling</SelectItem>
|
||||
<SelectItem value="extraction">Extraction</SelectItem>
|
||||
<SelectItem value="root-canal">Root Canal</SelectItem>
|
||||
<SelectItem value="crown">Crown</SelectItem>
|
||||
<SelectItem value="dentures">Dentures</SelectItem>
|
||||
<SelectItem value="consultation">Consultation</SelectItem>
|
||||
<SelectItem value="emergency">Emergency</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="scheduled">Scheduled</SelectItem>
|
||||
<SelectItem value="confirmed">Confirmed</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
<SelectItem value="no-show">No Show</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="staff"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Doctor/Hygienist</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select staff member" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{staffMembers.map((staff) => (
|
||||
<SelectItem key={staff.id} value={staff.id}>
|
||||
{staff.name} ({staff.role})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Enter any notes about the appointment"
|
||||
{...field}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="startTime"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Start Time</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="09:00"
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
className="pl-10"
|
||||
value={
|
||||
typeof field.value === "string" ? field.value : ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="endTime"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>End Time</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Clock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="09:30"
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
className="pl-10"
|
||||
value={
|
||||
typeof field.value === "string" ? field.value : ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Appointment Type</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
className="min-h-24"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading} className="w-full">
|
||||
{appointment ? "Update Appointment" : "Create Appointment"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="checkup">Checkup</SelectItem>
|
||||
<SelectItem value="cleaning">Cleaning</SelectItem>
|
||||
<SelectItem value="filling">Filling</SelectItem>
|
||||
<SelectItem value="extraction">Extraction</SelectItem>
|
||||
<SelectItem value="root-canal">Root Canal</SelectItem>
|
||||
<SelectItem value="crown">Crown</SelectItem>
|
||||
<SelectItem value="dentures">Dentures</SelectItem>
|
||||
<SelectItem value="consultation">Consultation</SelectItem>
|
||||
<SelectItem value="emergency">Emergency</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="scheduled">Scheduled</SelectItem>
|
||||
<SelectItem value="confirmed">Confirmed</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
<SelectItem value="no-show">No Show</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="staffId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Doctor/Hygienist</FormLabel>
|
||||
<Select
|
||||
disabled={isLoading}
|
||||
onValueChange={(val) => field.onChange(Number(val))}
|
||||
value={field.value ? String(field.value) : undefined}
|
||||
defaultValue={field.value ? String(field.value) : undefined}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select staff member" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{staffMembers.map((staff) => (
|
||||
<SelectItem
|
||||
key={staff.id}
|
||||
value={staff.id?.toString() || ""}
|
||||
>
|
||||
{staff.name} ({staff.role})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Enter any notes about the appointment"
|
||||
{...field}
|
||||
disabled={isLoading}
|
||||
className="min-h-24"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading} className="w-full">
|
||||
{appointment ? "Update Appointment" : "Create Appointment"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,14 +134,8 @@ export function AppointmentTable({
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
{appointment.startTime instanceof Date
|
||||
? appointment.startTime.toISOString().slice(11, 16)
|
||||
: appointment.startTime.slice(0, 5)}{" "}
|
||||
-
|
||||
{appointment.endTime instanceof Date
|
||||
? appointment.endTime.toISOString().slice(11, 16)
|
||||
: appointment.endTime.slice(0, 5)}
|
||||
{/* {appointment.startTime.slice(0, 5)} - {appointment.endTime.slice(0, 5)} */}
|
||||
{appointment.startTime.slice(0, 5)} -{" "}
|
||||
{appointment.endTime.slice(0, 5)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="capitalize">
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { useState, forwardRef, useImperativeHandle } from "react";
|
||||
import {
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -8,38 +14,44 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { PatientForm } from "./patient-form";
|
||||
import { PatientForm, PatientFormRef } from "./patient-form";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { X, Calendar } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
// import { InsertPatient, Patient, UpdatePatient } from "@repo/db/shared/schemas";
|
||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const insertPatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||
|
||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
}).partial();
|
||||
const updatePatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
)
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial();
|
||||
|
||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||
|
||||
|
||||
interface AddPatientModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (data: InsertPatient | UpdatePatient) => void;
|
||||
onSubmit: (data: InsertPatient | (UpdatePatient & { id?: number })) => void;
|
||||
isLoading: boolean;
|
||||
patient?: Patient;
|
||||
extractedInfo?: {
|
||||
@@ -56,35 +68,49 @@ export type AddPatientModalRef = {
|
||||
navigateToSchedule: (patientId: number) => void;
|
||||
};
|
||||
|
||||
export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalProps>(function AddPatientModal(props, ref) {
|
||||
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } = props;
|
||||
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 [formData, setFormData] = useState<
|
||||
InsertPatient | UpdatePatient | null
|
||||
>(null);
|
||||
const isEditing = !!patient;
|
||||
const [, navigate] = useLocation();
|
||||
const [saveAndSchedule, setSaveAndSchedule] = useState(false);
|
||||
const patientFormRef = useRef<PatientFormRef>(null); // Ref for PatientForm
|
||||
|
||||
// Set up the imperativeHandle to expose functionality to the parent component
|
||||
useEffect(() => {
|
||||
if (isEditing && patient) {
|
||||
const { id, userId, createdAt, ...sanitized } = patient;
|
||||
setFormData(sanitized); // Update the form data with the patient data for editing
|
||||
} else {
|
||||
setFormData(null); // Reset form data when not editing
|
||||
}
|
||||
}, [isEditing, patient]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
shouldSchedule: saveAndSchedule,
|
||||
navigateToSchedule: (patientId: number) => {
|
||||
navigate(`/appointments?newPatient=${patientId}`);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const handleFormSubmit = (data: InsertPatient | UpdatePatient) => {
|
||||
setFormData(data);
|
||||
onSubmit(data);
|
||||
if (patient && patient.id) {
|
||||
onSubmit({ ...data, id: patient.id });
|
||||
} else {
|
||||
onSubmit(data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveAndSchedule = () => {
|
||||
setSaveAndSchedule(true);
|
||||
if (formData) {
|
||||
onSubmit(formData);
|
||||
} else {
|
||||
// Trigger form validation by clicking the hidden submit button
|
||||
document.querySelector('form')?.requestSubmit();
|
||||
}
|
||||
document.querySelector("form")?.requestSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -117,41 +143,41 @@ export const AddPatientModal = forwardRef<AddPatientModalRef, AddPatientModalPro
|
||||
/>
|
||||
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
|
||||
{!isEditing && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-1"
|
||||
onClick={handleSaveAndSchedule}
|
||||
onClick={() => {
|
||||
handleSaveAndSchedule();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
Save & Schedule
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
form="patient-form"
|
||||
onClick={() => {
|
||||
if (formData) {
|
||||
onSubmit(formData);
|
||||
} else {
|
||||
// Trigger form validation by clicking the hidden submit button
|
||||
document.querySelector('form')?.requestSubmit();
|
||||
if (patientFormRef.current) {
|
||||
patientFormRef.current.submit();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? isEditing ? "Updating..." : "Saving..."
|
||||
: isEditing ? "Update Patient" : "Save Patient"
|
||||
}
|
||||
{isLoading
|
||||
? patient
|
||||
? "Updating..."
|
||||
: "Saving..."
|
||||
: patient
|
||||
? "Update Patient"
|
||||
: "Save Patient"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||
// import { insertPatientSchema, InsertPatient, Patient, updatePatientSchema, UpdatePatient } from "@repo/db/shared/schemas";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
Form,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -21,27 +19,36 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||
|
||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const insertPatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
}).partial();
|
||||
});
|
||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||
|
||||
const updatePatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
)
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial();
|
||||
|
||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||
|
||||
|
||||
interface PatientFormProps {
|
||||
patient?: Patient;
|
||||
extractedInfo?: {
|
||||
@@ -53,50 +60,103 @@ interface PatientFormProps {
|
||||
onSubmit: (data: InsertPatient | UpdatePatient) => void;
|
||||
}
|
||||
|
||||
export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormProps) {
|
||||
export type PatientFormRef = {
|
||||
submit: () => 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 schema = useMemo(
|
||||
() =>
|
||||
isEditing
|
||||
? updatePatientSchema
|
||||
: insertPatientSchema.extend({ userId: z.number().optional() }),
|
||||
[isEditing]
|
||||
);
|
||||
|
||||
const computedDefaultValues = useMemo(() => {
|
||||
if (isEditing && patient) {
|
||||
const { id, userId, createdAt, ...sanitizedPatient } = patient;
|
||||
return {
|
||||
...sanitizedPatient,
|
||||
dateOfBirth: patient.dateOfBirth
|
||||
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
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,
|
||||
};
|
||||
}, [isEditing, patient, extractedInfo, user?.id]);
|
||||
|
||||
const form = useForm<InsertPatient | UpdatePatient>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: patient || defaultValues,
|
||||
defaultValues: computedDefaultValues,
|
||||
});
|
||||
|
||||
const handleSubmit = (data: InsertPatient | UpdatePatient) => {
|
||||
// Debug form errors
|
||||
useEffect(() => {
|
||||
const errors = form.formState.errors;
|
||||
if (Object.keys(errors).length > 0) {
|
||||
console.log("❌ Form validation errors:", errors);
|
||||
}
|
||||
}, [form.formState.errors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (patient) {
|
||||
const { id, userId, createdAt, ...sanitizedPatient } = patient;
|
||||
const resetValues: Partial<Patient> = {
|
||||
...sanitizedPatient,
|
||||
dateOfBirth: patient.dateOfBirth
|
||||
? new Date(patient.dateOfBirth).toISOString().split("T")[0]
|
||||
: "",
|
||||
};
|
||||
form.reset(resetValues);
|
||||
}
|
||||
}, [patient, computedDefaultValues, form]);
|
||||
|
||||
const handleSubmit2 = (data: InsertPatient | UpdatePatient) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
<form
|
||||
id="patient-form"
|
||||
key={patient?.id || "new"}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
handleSubmit2(data);
|
||||
})}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Personal Information */}
|
||||
<div>
|
||||
<h4 className="text-md font-medium text-gray-700 mb-3">Personal Information</h4>
|
||||
<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}
|
||||
@@ -111,7 +171,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
@@ -125,7 +185,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dateOfBirth"
|
||||
@@ -139,15 +199,15 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gender"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gender *</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value as string}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -167,10 +227,12 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Contact Information */}
|
||||
<div>
|
||||
<h4 className="text-md font-medium text-gray-700 mb-3">Contact Information</h4>
|
||||
<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}
|
||||
@@ -185,7 +247,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
@@ -193,13 +255,13 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} value={field.value || ''} />
|
||||
<Input type="email" {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address"
|
||||
@@ -207,13 +269,13 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormLabel>Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ''} />
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="city"
|
||||
@@ -221,13 +283,13 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem>
|
||||
<FormLabel>City</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ''} />
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="zipCode"
|
||||
@@ -235,7 +297,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem>
|
||||
<FormLabel>ZIP Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ''} />
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -243,10 +305,12 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Insurance Information */}
|
||||
<div>
|
||||
<h4 className="text-md font-medium text-gray-700 mb-3">Insurance Information</h4>
|
||||
<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}
|
||||
@@ -254,9 +318,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Insurance Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value as string || ''}
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={(field.value as string) || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
@@ -264,7 +328,9 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="placeholder">Select provider</SelectItem>
|
||||
<SelectItem value="placeholder">
|
||||
Select provider
|
||||
</SelectItem>
|
||||
<SelectItem value="delta">Delta Dental</SelectItem>
|
||||
<SelectItem value="metlife">MetLife</SelectItem>
|
||||
<SelectItem value="cigna">Cigna</SelectItem>
|
||||
@@ -277,7 +343,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="insuranceId"
|
||||
@@ -285,13 +351,13 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem>
|
||||
<FormLabel>Insurance ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ''} />
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupNumber"
|
||||
@@ -299,13 +365,13 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem>
|
||||
<FormLabel>Group Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ''} />
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="policyHolder"
|
||||
@@ -313,7 +379,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
<FormItem>
|
||||
<FormLabel>Policy Holder (if not self)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ''} />
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -321,7 +387,7 @@ export function PatientForm({ patient, extractedInfo, onSubmit }: PatientFormPro
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Hidden submit button for form validation */}
|
||||
<button type="submit" className="hidden" aria-hidden="true"></button>
|
||||
</form>
|
||||
|
||||
@@ -44,10 +44,10 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
{/* <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Close> */}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
|
||||
@@ -6,24 +6,24 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
// import { insertUserSchema, User as SelectUser, InsertUser } from "@repo/db/shared/schemas";
|
||||
import { UserUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
import { getQueryFn, apiRequest, queryClient } from "../lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
type SelectUser = z.infer<typeof UserUncheckedCreateInputObjectSchema>;
|
||||
|
||||
const insertUserSchema = (UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<{
|
||||
const insertUserSchema = (
|
||||
UserUncheckedCreateInputObjectSchema as unknown as z.ZodObject<{
|
||||
username: z.ZodString;
|
||||
password: z.ZodString;
|
||||
}>).pick({
|
||||
}>
|
||||
).pick({
|
||||
username: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
|
||||
|
||||
|
||||
type AuthContextType = {
|
||||
user: SelectUser | null;
|
||||
isLoading: boolean;
|
||||
@@ -40,6 +40,7 @@ type LoginData = {
|
||||
};
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
@@ -47,17 +48,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
error,
|
||||
isLoading,
|
||||
} = useQuery<SelectUser | undefined, Error>({
|
||||
queryKey: ["/api/user"],
|
||||
queryKey: ["/api/users/"],
|
||||
queryFn: getQueryFn({ on401: "returnNull" }),
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: async (credentials: LoginData) => {
|
||||
const res = await apiRequest("POST", "/api/login", credentials);
|
||||
return await res.json();
|
||||
const res = await apiRequest("POST", "/api/auth/login", credentials);
|
||||
|
||||
const data = await res.json();
|
||||
localStorage.setItem("token", data.token);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (user: SelectUser) => {
|
||||
queryClient.setQueryData(["/api/user"], user);
|
||||
queryClient.setQueryData(["/api/users/"], user);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
@@ -70,11 +75,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async (credentials: InsertUser) => {
|
||||
const res = await apiRequest("POST", "/api/register", credentials);
|
||||
return await res.json();
|
||||
const res = await apiRequest("POST", "/api/auth/register", credentials);
|
||||
const data = await res.json();
|
||||
localStorage.setItem("token", data.token);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (user: SelectUser) => {
|
||||
queryClient.setQueryData(["/api/user"], user);
|
||||
queryClient.setQueryData(["/api/users/"], user);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
@@ -87,10 +94,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await apiRequest("POST", "/api/logout");
|
||||
// Remove token from localStorage when logging out
|
||||
localStorage.removeItem("token");
|
||||
await apiRequest("POST", "/api/auth/logout");
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.setQueryData(["/api/user"], null);
|
||||
queryClient.setQueryData(["/api/users/"], null);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
|
||||
.form-container {
|
||||
max-height: 80vh; /* Set a max height (80% of the viewport height or adjust as needed) */
|
||||
overflow-y: auto; /* Enable vertical scrolling when the content overflows */
|
||||
padding: 20px; /* Optional, for some spacing */
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { QueryClient, QueryFunction } from "@tanstack/react-query";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "";
|
||||
|
||||
async function throwIfResNotOk(res: Response) {
|
||||
if (!res.ok) {
|
||||
const text = (await res.text()) || res.statusText;
|
||||
@@ -10,11 +12,17 @@ async function throwIfResNotOk(res: Response) {
|
||||
export async function apiRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
data?: unknown | undefined,
|
||||
data?: unknown | undefined
|
||||
): Promise<Response> {
|
||||
const res = await fetch(url, {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}${url}`, {
|
||||
method,
|
||||
headers: data ? { "Content-Type": "application/json" } : {},
|
||||
// headers: data ? { "Content-Type": "application/json" } : {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}), // Include JWT token if available
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
credentials: "include",
|
||||
});
|
||||
@@ -24,12 +32,20 @@ export async function apiRequest(
|
||||
}
|
||||
|
||||
type UnauthorizedBehavior = "returnNull" | "throw";
|
||||
|
||||
export const getQueryFn: <T>(options: {
|
||||
on401: UnauthorizedBehavior;
|
||||
}) => QueryFunction<T> =
|
||||
({ on401: unauthorizedBehavior }) =>
|
||||
async ({ queryKey }) => {
|
||||
const res = await fetch(queryKey[0] as string, {
|
||||
const url = `${API_BASE_URL}${queryKey[0] as string}`;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,23 @@ import { StatCard } from "@/components/ui/stat-card";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { AppointmentUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||
// import { InsertPatient, Patient, UpdatePatient, Appointment, InsertAppointment, UpdateAppointment } from "@repo/db/shared/schemas";
|
||||
import { Users, Calendar, CheckCircle, CreditCard, Plus, Clock } from "lucide-react";
|
||||
import {
|
||||
AppointmentUncheckedCreateInputObjectSchema,
|
||||
PatientUncheckedCreateInputObjectSchema,
|
||||
} from "@repo/db/shared/schemas";
|
||||
import {
|
||||
Users,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
CreditCard,
|
||||
Plus,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,78 +32,105 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
//creating types out of schema auto generated.
|
||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||
|
||||
const insertAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const insertAppointmentSchema = (
|
||||
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
type InsertAppointment = z.infer<typeof insertAppointmentSchema>;
|
||||
|
||||
const updateAppointmentSchema = (AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
}).partial();
|
||||
const updateAppointmentSchema = (
|
||||
AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
)
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
})
|
||||
.partial();
|
||||
type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
|
||||
|
||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const insertPatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||
|
||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
}).partial();
|
||||
const updatePatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
)
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial();
|
||||
|
||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||
|
||||
|
||||
export default function Dashboard() {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
||||
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
||||
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
|
||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | undefined>(undefined);
|
||||
|
||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<
|
||||
Appointment | undefined
|
||||
>(undefined);
|
||||
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
// Fetch patients
|
||||
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({
|
||||
queryKey: ["/api/patients"],
|
||||
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||
Patient[]
|
||||
>({
|
||||
queryKey: ["/api/patients/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/patients/");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
|
||||
// Fetch appointments
|
||||
const {
|
||||
data: appointments = [] as Appointment[],
|
||||
isLoading: isLoadingAppointments
|
||||
const {
|
||||
data: appointments = [] as Appointment[],
|
||||
isLoading: isLoadingAppointments,
|
||||
} = useQuery<Appointment[]>({
|
||||
queryKey: ["/api/appointments"],
|
||||
queryKey: ["/api/appointments/all"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/appointments/all");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Add patient mutation
|
||||
const addPatientMutation = useMutation({
|
||||
mutationFn: async (patient: InsertPatient) => {
|
||||
const res = await apiRequest("POST", "/api/patients", patient);
|
||||
const res = await apiRequest("POST", "/api/patients/", patient);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsAddPatientOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient added successfully!",
|
||||
@@ -112,13 +148,19 @@ export default function Dashboard() {
|
||||
|
||||
// Update patient mutation
|
||||
const updatePatientMutation = useMutation({
|
||||
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => {
|
||||
mutationFn: async ({
|
||||
id,
|
||||
patient,
|
||||
}: {
|
||||
id: number;
|
||||
patient: UpdatePatient;
|
||||
}) => {
|
||||
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsAddPatientOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient updated successfully!",
|
||||
@@ -142,14 +184,25 @@ export default function Dashboard() {
|
||||
if (user) {
|
||||
addPatientMutation.mutate({
|
||||
...patient,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePatient = (patient: UpdatePatient) => {
|
||||
if (currentPatient) {
|
||||
updatePatientMutation.mutate({ id: currentPatient.id, patient });
|
||||
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
|
||||
if (currentPatient && user) {
|
||||
const { id, ...sanitizedPatient } = patient;
|
||||
updatePatientMutation.mutate({
|
||||
id: currentPatient.id,
|
||||
patient: sanitizedPatient,
|
||||
});
|
||||
} else {
|
||||
console.error("No current patient or user found for update");
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Cannot update patient: No patient or user found",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,11 +215,11 @@ export default function Dashboard() {
|
||||
setCurrentPatient(patient);
|
||||
setIsViewPatientOpen(true);
|
||||
};
|
||||
|
||||
|
||||
// Create appointment mutation
|
||||
const createAppointmentMutation = useMutation({
|
||||
mutationFn: async (appointment: InsertAppointment) => {
|
||||
const res = await apiRequest("POST", "/api/appointments", appointment);
|
||||
const res = await apiRequest("POST", "/api/appointments/", appointment);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -175,7 +228,9 @@ export default function Dashboard() {
|
||||
title: "Success",
|
||||
description: "Appointment created successfully.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
||||
// Invalidate both appointments and patients queries
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
@@ -185,11 +240,21 @@ export default function Dashboard() {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Update appointment mutation
|
||||
const updateAppointmentMutation = useMutation({
|
||||
mutationFn: async ({ id, appointment }: { id: number; appointment: UpdateAppointment }) => {
|
||||
const res = await apiRequest("PUT", `/api/appointments/${id}`, appointment);
|
||||
mutationFn: async ({
|
||||
id,
|
||||
appointment,
|
||||
}: {
|
||||
id: number;
|
||||
appointment: UpdateAppointment;
|
||||
}) => {
|
||||
const res = await apiRequest(
|
||||
"PUT",
|
||||
`/api/appointments/${id}`,
|
||||
appointment
|
||||
);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -198,7 +263,9 @@ export default function Dashboard() {
|
||||
title: "Success",
|
||||
description: "Appointment updated successfully.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments"] });
|
||||
// Invalidate both appointments and patients queries
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast({
|
||||
@@ -208,10 +275,12 @@ export default function Dashboard() {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Handle appointment submission (create or update)
|
||||
const handleAppointmentSubmit = (appointmentData: InsertAppointment | UpdateAppointment) => {
|
||||
if (selectedAppointment && typeof selectedAppointment.id === 'number') {
|
||||
const handleAppointmentSubmit = (
|
||||
appointmentData: InsertAppointment | UpdateAppointment
|
||||
) => {
|
||||
if (selectedAppointment && typeof selectedAppointment.id === "number") {
|
||||
updateAppointmentMutation.mutate({
|
||||
id: selectedAppointment.id,
|
||||
appointment: appointmentData as UpdateAppointment,
|
||||
@@ -219,7 +288,7 @@ export default function Dashboard() {
|
||||
} else {
|
||||
if (user) {
|
||||
createAppointmentMutation.mutate({
|
||||
...appointmentData as InsertAppointment,
|
||||
...(appointmentData as InsertAppointment),
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
@@ -228,27 +297,28 @@ export default function Dashboard() {
|
||||
|
||||
// Since we removed filters, just return all patients
|
||||
const filteredPatients = patients;
|
||||
|
||||
// Get today's date in YYYY-MM-DD format
|
||||
const today = format(new Date(), 'yyyy-MM-dd');
|
||||
|
||||
// Filter appointments for today
|
||||
const todaysAppointments = appointments.filter(
|
||||
(appointment) => appointment.date === today
|
||||
);
|
||||
|
||||
const today = format(new Date(), "yyyy-MM-dd");
|
||||
|
||||
const todaysAppointments = appointments.filter((appointment) => {
|
||||
const appointmentDate = format(new Date(appointment.date), "yyyy-MM-dd");
|
||||
return appointmentDate === today;
|
||||
});
|
||||
|
||||
// Count completed appointments today
|
||||
const completedTodayCount = todaysAppointments.filter(
|
||||
(appointment) => appointment.status === 'completed'
|
||||
).length;
|
||||
const completedTodayCount = todaysAppointments.filter((appointment) => {
|
||||
return appointment.status === "completed";
|
||||
}).length;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
||||
|
||||
<Sidebar
|
||||
isMobileOpen={isMobileMenuOpen}
|
||||
setIsMobileOpen={setIsMobileMenuOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4">
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
@@ -281,9 +351,11 @@ export default function Dashboard() {
|
||||
{/* Today's Appointments Section */}
|
||||
<div className="flex flex-col space-y-4 mb-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<h2 className="text-xl font-medium text-gray-800">Today's Appointments</h2>
|
||||
<Button
|
||||
className="mt-2 md:mt-0"
|
||||
<h2 className="text-xl font-medium text-gray-800">
|
||||
Today's Appointments
|
||||
</h2>
|
||||
<Button
|
||||
className="mt-2 md:mt-0"
|
||||
onClick={() => {
|
||||
setSelectedAppointment(undefined);
|
||||
setIsAddAppointmentOpen(true);
|
||||
@@ -293,39 +365,76 @@ export default function Dashboard() {
|
||||
New Appointment
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{todaysAppointments.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
{todaysAppointments.map((appointment) => {
|
||||
const patient = patients.find(p => p.id === appointment.patientId);
|
||||
const patient = patients.find(
|
||||
(p) => p.id === appointment.patientId
|
||||
);
|
||||
return (
|
||||
<div key={appointment.id} className="p-4 flex items-center justify-between">
|
||||
<div
|
||||
key={appointment.id}
|
||||
className="p-4 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-10 w-10 rounded-full bg-primary bg-opacity-10 text-primary flex items-center justify-center">
|
||||
<Clock className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
{patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown Patient'}
|
||||
{patient
|
||||
? `${patient.firstName} ${patient.lastName}`
|
||||
: "Unknown Patient"}
|
||||
</h3>
|
||||
<div className="text-sm text-gray-500 flex items-center space-x-2">
|
||||
<span>{new Date(appointment.startTime).toLocaleString()} - {new Date(appointment.endTime).toLocaleString()}</span>
|
||||
<span>
|
||||
{new Date(
|
||||
`${appointment.date.toString().slice(0, 10)}T${appointment.startTime}:00`
|
||||
).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}{" "}
|
||||
-{" "}
|
||||
{new Date(
|
||||
`${appointment.date.toString().slice(0, 10)}T${appointment.endTime}:00`
|
||||
).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{appointment.type.charAt(0).toUpperCase() + appointment.type.slice(1)}</span>
|
||||
<span>
|
||||
{appointment.type.charAt(0).toUpperCase() +
|
||||
appointment.type.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
${appointment.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||
appointment.status === 'cancelled' ? 'bg-red-100 text-red-800' :
|
||||
appointment.status === 'confirmed' ? 'bg-blue-100 text-blue-800' :
|
||||
'bg-yellow-100 text-yellow-800'}`}>
|
||||
{appointment.status ? appointment.status.charAt(0).toUpperCase() + appointment.status.slice(1) : 'Scheduled'}
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
${
|
||||
appointment.status === "completed"
|
||||
? "bg-green-100 text-green-800"
|
||||
: appointment.status === "cancelled"
|
||||
? "bg-red-100 text-red-800"
|
||||
: appointment.status === "confirmed"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
>
|
||||
{appointment.status
|
||||
? appointment.status.charAt(0).toUpperCase() +
|
||||
appointment.status.slice(1)
|
||||
: "Scheduled"}
|
||||
</span>
|
||||
<Link to="/appointments" className="text-primary hover:text-primary/80 text-sm">
|
||||
<Link
|
||||
to="/appointments"
|
||||
className="text-primary hover:text-primary/80 text-sm"
|
||||
>
|
||||
View All
|
||||
</Link>
|
||||
</div>
|
||||
@@ -336,7 +445,9 @@ export default function Dashboard() {
|
||||
) : (
|
||||
<div className="p-6 text-center">
|
||||
<Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" />
|
||||
<h3 className="text-lg font-medium text-gray-900">No appointments today</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
No appointments today
|
||||
</h3>
|
||||
<p className="mt-1 text-gray-500">
|
||||
You don't have any appointments scheduled for today.
|
||||
</p>
|
||||
@@ -355,14 +466,16 @@ export default function Dashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Patient Management Section */}
|
||||
<div className="flex flex-col space-y-4">
|
||||
{/* Patient Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<h2 className="text-xl font-medium text-gray-800">Patient Management</h2>
|
||||
<Button
|
||||
className="mt-2 md:mt-0"
|
||||
<h2 className="text-xl font-medium text-gray-800">
|
||||
Patient Management
|
||||
</h2>
|
||||
<Button
|
||||
className="mt-2 md:mt-0"
|
||||
onClick={() => {
|
||||
setCurrentPatient(undefined);
|
||||
setIsAddPatientOpen(true);
|
||||
@@ -373,13 +486,11 @@ export default function Dashboard() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search and filters removed */}
|
||||
|
||||
{/* Patient Table */}
|
||||
<PatientTable
|
||||
patients={filteredPatients}
|
||||
onEdit={handleEditPatient}
|
||||
onView={handleViewPatient}
|
||||
<PatientTable
|
||||
patients={filteredPatients}
|
||||
onEdit={handleEditPatient}
|
||||
onView={handleViewPatient}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
@@ -390,7 +501,9 @@ export default function Dashboard() {
|
||||
open={isAddPatientOpen}
|
||||
onOpenChange={setIsAddPatientOpen}
|
||||
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
|
||||
isLoading={addPatientMutation.isPending || updatePatientMutation.isPending}
|
||||
isLoading={
|
||||
addPatientMutation.isPending || updatePatientMutation.isPending
|
||||
}
|
||||
patient={currentPatient}
|
||||
/>
|
||||
|
||||
@@ -403,120 +516,138 @@ export default function Dashboard() {
|
||||
Complete information about the patient.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
{currentPatient && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-16 w-16 rounded-full bg-primary text-white flex items-center justify-center text-xl font-medium">
|
||||
{currentPatient.firstName.charAt(0)}{currentPatient.lastName.charAt(0)}
|
||||
{currentPatient.firstName.charAt(0)}
|
||||
{currentPatient.lastName.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">{currentPatient.firstName} {currentPatient.lastName}</h3>
|
||||
<p className="text-gray-500">Patient ID: {currentPatient.id.toString().padStart(4, '0')}</p>
|
||||
<h3 className="text-xl font-semibold">
|
||||
{currentPatient.firstName} {currentPatient.lastName}
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
Patient ID: {currentPatient.id.toString().padStart(4, "0")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">Personal Information</h4>
|
||||
<h4 className="font-medium text-gray-900">
|
||||
Personal Information
|
||||
</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Date of Birth:</span>{' '}
|
||||
{new Date(currentPatient.dateOfBirth).toLocaleDateString()}
|
||||
<span className="text-gray-500">Date of Birth:</span>{" "}
|
||||
{new Date(
|
||||
currentPatient.dateOfBirth
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Gender:</span>{' '}
|
||||
{currentPatient.gender.charAt(0).toUpperCase() + currentPatient.gender.slice(1)}
|
||||
<span className="text-gray-500">Gender:</span>{" "}
|
||||
{currentPatient.gender.charAt(0).toUpperCase() +
|
||||
currentPatient.gender.slice(1)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Status:</span>{' '}
|
||||
<span className={`${
|
||||
currentPatient.status === 'active'
|
||||
? 'text-green-600'
|
||||
: 'text-amber-600'
|
||||
} font-medium`}>
|
||||
{currentPatient.status.charAt(0).toUpperCase() + currentPatient.status.slice(1)}
|
||||
<span className="text-gray-500">Status:</span>{" "}
|
||||
<span
|
||||
className={`${
|
||||
currentPatient.status === "active"
|
||||
? "text-green-600"
|
||||
: "text-amber-600"
|
||||
} font-medium`}
|
||||
>
|
||||
{currentPatient.status.charAt(0).toUpperCase() +
|
||||
currentPatient.status.slice(1)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">Contact Information</h4>
|
||||
<h4 className="font-medium text-gray-900">
|
||||
Contact Information
|
||||
</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Phone:</span>{' '}
|
||||
<span className="text-gray-500">Phone:</span>{" "}
|
||||
{currentPatient.phone}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Email:</span>{' '}
|
||||
{currentPatient.email || 'N/A'}
|
||||
<span className="text-gray-500">Email:</span>{" "}
|
||||
{currentPatient.email || "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Address:</span>{' '}
|
||||
<span className="text-gray-500">Address:</span>{" "}
|
||||
{currentPatient.address ? (
|
||||
<>
|
||||
{currentPatient.address}
|
||||
{currentPatient.city && `, ${currentPatient.city}`}
|
||||
{currentPatient.zipCode && ` ${currentPatient.zipCode}`}
|
||||
{currentPatient.zipCode &&
|
||||
` ${currentPatient.zipCode}`}
|
||||
</>
|
||||
) : (
|
||||
'N/A'
|
||||
"N/A"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">Insurance</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Provider:</span>{' '}
|
||||
{currentPatient.insuranceProvider
|
||||
? currentPatient.insuranceProvider === 'delta'
|
||||
? 'Delta Dental'
|
||||
: currentPatient.insuranceProvider === 'metlife'
|
||||
? 'MetLife'
|
||||
: currentPatient.insuranceProvider === 'cigna'
|
||||
? 'Cigna'
|
||||
: currentPatient.insuranceProvider === 'aetna'
|
||||
? 'Aetna'
|
||||
: currentPatient.insuranceProvider
|
||||
: 'N/A'}
|
||||
<span className="text-gray-500">Provider:</span>{" "}
|
||||
{currentPatient.insuranceProvider
|
||||
? currentPatient.insuranceProvider === "delta"
|
||||
? "Delta Dental"
|
||||
: currentPatient.insuranceProvider === "metlife"
|
||||
? "MetLife"
|
||||
: currentPatient.insuranceProvider === "cigna"
|
||||
? "Cigna"
|
||||
: currentPatient.insuranceProvider === "aetna"
|
||||
? "Aetna"
|
||||
: currentPatient.insuranceProvider
|
||||
: "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">ID:</span>{' '}
|
||||
{currentPatient.insuranceId || 'N/A'}
|
||||
<span className="text-gray-500">ID:</span>{" "}
|
||||
{currentPatient.insuranceId || "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Group Number:</span>{' '}
|
||||
{currentPatient.groupNumber || 'N/A'}
|
||||
<span className="text-gray-500">Group Number:</span>{" "}
|
||||
{currentPatient.groupNumber || "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Policy Holder:</span>{' '}
|
||||
{currentPatient.policyHolder || 'Self'}
|
||||
<span className="text-gray-500">Policy Holder:</span>{" "}
|
||||
{currentPatient.policyHolder || "Self"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">Medical Information</h4>
|
||||
<h4 className="font-medium text-gray-900">
|
||||
Medical Information
|
||||
</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p>
|
||||
<span className="text-gray-500">Allergies:</span>{' '}
|
||||
{currentPatient.allergies || 'None reported'}
|
||||
<span className="text-gray-500">Allergies:</span>{" "}
|
||||
{currentPatient.allergies || "None reported"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-gray-500">Medical Conditions:</span>{' '}
|
||||
{currentPatient.medicalConditions || 'None reported'}
|
||||
<span className="text-gray-500">Medical Conditions:</span>{" "}
|
||||
{currentPatient.medicalConditions || "None reported"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsViewPatientOpen(false)}
|
||||
>
|
||||
Close
|
||||
@@ -534,13 +665,16 @@ export default function Dashboard() {
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
{/* Add/Edit Appointment Modal */}
|
||||
<AddAppointmentModal
|
||||
open={isAddAppointmentOpen}
|
||||
onOpenChange={setIsAddAppointmentOpen}
|
||||
onSubmit={handleAppointmentSubmit}
|
||||
isLoading={createAppointmentMutation.isPending || updateAppointmentMutation.isPending}
|
||||
isLoading={
|
||||
createAppointmentMutation.isPending ||
|
||||
updateAppointmentMutation.isPending
|
||||
}
|
||||
appointment={selectedAppointment}
|
||||
patients={patients}
|
||||
/>
|
||||
|
||||
@@ -4,39 +4,55 @@ import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||
import { PatientSearch, SearchCriteria } from "@/components/patients/patient-search";
|
||||
import {
|
||||
PatientSearch,
|
||||
SearchCriteria,
|
||||
} from "@/components/patients/patient-search";
|
||||
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
|
||||
// import { Patient, InsertPatient, UpdatePatient } from "@repo/db/shared/schemas";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {z} from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
const PatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const PatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
appointments: true,
|
||||
});
|
||||
type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
const insertPatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||
|
||||
const updatePatientSchema = (PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>).omit({
|
||||
const insertPatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
}).partial();
|
||||
});
|
||||
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
||||
|
||||
const updatePatientSchema = (
|
||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||
)
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial();
|
||||
|
||||
type UpdatePatient = z.infer<typeof updatePatientSchema>;
|
||||
|
||||
|
||||
// Type for the ref to access modal methods
|
||||
type AddPatientModalRef = {
|
||||
shouldSchedule: boolean;
|
||||
@@ -48,11 +64,15 @@ export default function PatientsPage() {
|
||||
const { user } = useAuth();
|
||||
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
||||
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(undefined);
|
||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(null);
|
||||
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(
|
||||
null
|
||||
);
|
||||
const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
|
||||
|
||||
|
||||
// File upload states
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
@@ -65,25 +85,29 @@ export default function PatientsPage() {
|
||||
isLoading: isLoadingPatients,
|
||||
refetch: refetchPatients,
|
||||
} = useQuery<Patient[]>({
|
||||
queryKey: ["/api/patients"],
|
||||
queryKey: ["/api/patients/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/patients/");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Add patient mutation
|
||||
const addPatientMutation = useMutation({
|
||||
mutationFn: async (patient: InsertPatient) => {
|
||||
const res = await apiRequest("POST", "/api/patients", patient);
|
||||
const res = await apiRequest("POST", "/api/patients/", patient);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: (newPatient) => {
|
||||
setIsAddPatientOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient added successfully!",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
|
||||
// If the add patient modal wants to proceed to scheduling, redirect to appointments page
|
||||
if (addPatientModalRef.current?.shouldSchedule) {
|
||||
addPatientModalRef.current.navigateToSchedule(newPatient.id);
|
||||
@@ -100,13 +124,19 @@ export default function PatientsPage() {
|
||||
|
||||
// Update patient mutation
|
||||
const updatePatientMutation = useMutation({
|
||||
mutationFn: async ({ id, patient }: { id: number; patient: UpdatePatient }) => {
|
||||
mutationFn: async ({
|
||||
id,
|
||||
patient,
|
||||
}: {
|
||||
id: number;
|
||||
patient: UpdatePatient;
|
||||
}) => {
|
||||
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsAddPatientOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient updated successfully!",
|
||||
@@ -131,14 +161,25 @@ export default function PatientsPage() {
|
||||
if (user) {
|
||||
addPatientMutation.mutate({
|
||||
...patient,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePatient = (patient: UpdatePatient) => {
|
||||
if (currentPatient) {
|
||||
updatePatientMutation.mutate({ id: currentPatient.id, patient });
|
||||
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
|
||||
if (currentPatient && user) {
|
||||
const { id, ...sanitizedPatient } = patient;
|
||||
updatePatientMutation.mutate({
|
||||
id: currentPatient.id,
|
||||
patient: sanitizedPatient,
|
||||
});
|
||||
} else {
|
||||
console.error("No current patient or user found for update");
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Cannot update patient: No patient or user found",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,29 +193,32 @@ export default function PatientsPage() {
|
||||
setIsViewPatientOpen(true);
|
||||
};
|
||||
|
||||
const isLoading = isLoadingPatients || addPatientMutation.isPending || updatePatientMutation.isPending;
|
||||
|
||||
const isLoading =
|
||||
isLoadingPatients ||
|
||||
addPatientMutation.isPending ||
|
||||
updatePatientMutation.isPending;
|
||||
|
||||
// Search handling
|
||||
const handleSearch = (criteria: SearchCriteria) => {
|
||||
setSearchCriteria(criteria);
|
||||
};
|
||||
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearchCriteria(null);
|
||||
};
|
||||
|
||||
|
||||
// File upload handling
|
||||
const handleFileUpload = (file: File) => {
|
||||
setUploadedFile(file);
|
||||
setIsUploading(false); // In a real implementation, this would be set to true during upload
|
||||
|
||||
|
||||
toast({
|
||||
title: "File Selected",
|
||||
description: `${file.name} is ready for processing.`,
|
||||
variant: "default",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Process file and extract patient information
|
||||
const handleExtractInfo = async () => {
|
||||
if (!uploadedFile) {
|
||||
@@ -185,79 +229,81 @@ export default function PatientsPage() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsExtracting(true);
|
||||
|
||||
|
||||
try {
|
||||
// Read the file as base64
|
||||
const reader = new FileReader();
|
||||
|
||||
|
||||
// Set up a Promise to handle file reading
|
||||
const fileReadPromise = new Promise<string>((resolve, reject) => {
|
||||
reader.onload = (event) => {
|
||||
if (event.target && typeof event.target.result === 'string') {
|
||||
if (event.target && typeof event.target.result === "string") {
|
||||
resolve(event.target.result);
|
||||
} else {
|
||||
reject(new Error('Failed to read file as base64'));
|
||||
reject(new Error("Failed to read file as base64"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Error reading file'));
|
||||
reject(new Error("Error reading file"));
|
||||
};
|
||||
|
||||
|
||||
// Read the file as a data URL (base64)
|
||||
reader.readAsDataURL(uploadedFile);
|
||||
});
|
||||
|
||||
|
||||
// Get the base64 data
|
||||
const base64Data = await fileReadPromise;
|
||||
|
||||
|
||||
// Send file to server as base64
|
||||
const response = await fetch('/api/upload-file', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/api/upload-file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pdfData: base64Data,
|
||||
filename: uploadedFile.name
|
||||
filename: uploadedFile.name,
|
||||
}),
|
||||
credentials: 'include'
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
||||
throw new Error(
|
||||
`Server returned ${response.status}: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
// Only keep firstName, lastName, dateOfBirth, and insuranceId from the extracted info
|
||||
const simplifiedInfo = {
|
||||
firstName: data.extractedInfo.firstName,
|
||||
lastName: data.extractedInfo.lastName,
|
||||
dateOfBirth: data.extractedInfo.dateOfBirth,
|
||||
insuranceId: data.extractedInfo.insuranceId
|
||||
insuranceId: data.extractedInfo.insuranceId,
|
||||
};
|
||||
|
||||
|
||||
setExtractedInfo(simplifiedInfo);
|
||||
|
||||
|
||||
// Show success message
|
||||
toast({
|
||||
title: "Information Extracted",
|
||||
description: "Basic patient information (name, DOB, ID) has been extracted successfully.",
|
||||
description:
|
||||
"Basic patient information (name, DOB, ID) has been extracted successfully.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
|
||||
// Open patient form pre-filled with extracted data
|
||||
setCurrentPatient(undefined);
|
||||
|
||||
|
||||
// Pre-fill the form by opening the modal with the extracted information
|
||||
setTimeout(() => {
|
||||
setIsAddPatientOpen(true);
|
||||
}, 500);
|
||||
|
||||
} else {
|
||||
throw new Error(data.message || "Failed to extract information");
|
||||
}
|
||||
@@ -265,35 +311,38 @@ export default function PatientsPage() {
|
||||
console.error("Error extracting information:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : "Failed to extract information from file",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to extract information from file",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsExtracting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Filter patients based on search criteria
|
||||
const filteredPatients = useMemo(() => {
|
||||
if (!searchCriteria || !searchCriteria.searchTerm) {
|
||||
return patients;
|
||||
}
|
||||
|
||||
|
||||
const term = searchCriteria.searchTerm.toLowerCase();
|
||||
return patients.filter((patient) => {
|
||||
switch (searchCriteria.searchBy) {
|
||||
case 'name':
|
||||
case "name":
|
||||
return (
|
||||
patient.firstName.toLowerCase().includes(term) ||
|
||||
patient.lastName.toLowerCase().includes(term)
|
||||
);
|
||||
case 'phone':
|
||||
case "phone":
|
||||
return patient.phone.toLowerCase().includes(term);
|
||||
case 'insuranceProvider':
|
||||
case "insuranceProvider":
|
||||
return patient.insuranceProvider?.toLowerCase().includes(term);
|
||||
case 'insuranceId':
|
||||
case "insuranceId":
|
||||
return patient.insuranceId?.toLowerCase().includes(term);
|
||||
case 'all':
|
||||
case "all":
|
||||
default:
|
||||
return (
|
||||
patient.firstName.toLowerCase().includes(term) ||
|
||||
@@ -311,11 +360,14 @@ export default function PatientsPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
||||
|
||||
<Sidebar
|
||||
isMobileOpen={isMobileMenuOpen}
|
||||
setIsMobileOpen={setIsMobileMenuOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4">
|
||||
<div className="container mx-auto space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -361,7 +413,7 @@ export default function PatientsPage() {
|
||||
<File className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FileUploadZone
|
||||
<FileUploadZone
|
||||
onFileUpload={handleFileUpload}
|
||||
isUploading={isUploading}
|
||||
acceptedFileTypes="application/pdf"
|
||||
@@ -370,9 +422,9 @@ export default function PatientsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
<div className="md:col-span-1 flex items-end">
|
||||
<Button
|
||||
className="w-full h-12 gap-2"
|
||||
onClick={handleExtractInfo}
|
||||
<Button
|
||||
className="w-full h-12 gap-2"
|
||||
onClick={handleExtractInfo}
|
||||
disabled={!uploadedFile || isExtracting}
|
||||
>
|
||||
{isExtracting ? (
|
||||
@@ -399,23 +451,25 @@ export default function PatientsPage() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientSearch
|
||||
<PatientSearch
|
||||
onSearch={handleSearch}
|
||||
onClearSearch={handleClearSearch}
|
||||
isSearchActive={!!searchCriteria}
|
||||
/>
|
||||
|
||||
|
||||
{searchCriteria && (
|
||||
<div className="flex items-center my-4 px-2 py-1 bg-muted rounded-md text-sm">
|
||||
<p>
|
||||
Found {filteredPatients.length}
|
||||
{filteredPatients.length === 1 ? ' patient' : ' patients'}
|
||||
{searchCriteria.searchBy !== 'all' ? ` with ${searchCriteria.searchBy}` : ''}
|
||||
Found {filteredPatients.length}
|
||||
{filteredPatients.length === 1 ? " patient" : " patients"}
|
||||
{searchCriteria.searchBy !== "all"
|
||||
? ` with ${searchCriteria.searchBy}`
|
||||
: ""}
|
||||
matching "{searchCriteria.searchTerm}"
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<PatientTable
|
||||
patients={filteredPatients}
|
||||
onEdit={handleEditPatient}
|
||||
@@ -433,16 +487,20 @@ export default function PatientsPage() {
|
||||
isLoading={isLoading}
|
||||
patient={currentPatient}
|
||||
// Pass extracted info as a separate prop to avoid triggering edit mode
|
||||
extractedInfo={!currentPatient && extractedInfo ? {
|
||||
firstName: extractedInfo.firstName || "",
|
||||
lastName: extractedInfo.lastName || "",
|
||||
dateOfBirth: extractedInfo.dateOfBirth || "",
|
||||
insuranceId: extractedInfo.insuranceId || ""
|
||||
} : undefined}
|
||||
extractedInfo={
|
||||
!currentPatient && extractedInfo
|
||||
? {
|
||||
firstName: extractedInfo.firstName || "",
|
||||
lastName: extractedInfo.lastName || "",
|
||||
dateOfBirth: extractedInfo.dateOfBirth || "",
|
||||
insuranceId: extractedInfo.insuranceId || "",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user