recent patients done, actions are done, now have to add search criteria

This commit is contained in:
2025-07-05 22:03:39 +05:30
parent e620e9db1c
commit 015d677c7e
9 changed files with 510 additions and 914 deletions

View File

@@ -4,7 +4,7 @@ import { storage } from "../storage";
import { z } from "zod";
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import multer from "multer";
import { forwardToSeleniumAgent } from "../services/seleniumClient";
import { forwardToSeleniumAgent } from "../services/seleniumClaimClient";
import path from "path";
import axios from "axios";
import fs from "fs";

View File

@@ -68,6 +68,24 @@ router.get("/", async (req, res) => {
}
});
// Get recent patients (paginated)
router.get("/recent", async (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 10;
const offset = parseInt(req.query.offset as string) || 0;
const [patients, totalCount] = await Promise.all([
storage.getRecentPatients(limit, offset),
storage.getTotalPatientCount(),
]);
res.json({ patients, totalCount });
} catch (error) {
console.error("Failed to retrieve recent patients:", error);
res.status(500).json({ message: "Failed to retrieve recent patients" });
}
});
// Get a single patient by ID
router.get(
"/:id",
@@ -101,19 +119,6 @@ router.get(
}
);
// Get recent patients (paginated)
router.get("/recent", async (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 10;
const offset = parseInt(req.query.offset as string) || 0;
const recentPatients = await storage.getRecentPatients(limit, offset);
res.json(recentPatients);
} catch (error) {
console.error("Failed to retrieve recent patients:", error);
res.status(500).json({ message: "Failed to retrieve recent patients" });
}
});
// Create a new patient
router.post("/", async (req: Request, res: Response): Promise<any> => {

View File

@@ -167,6 +167,7 @@ export interface IStorage {
getPatient(id: number): Promise<Patient | undefined>;
getPatientsByUserId(userId: number): Promise<Patient[]>;
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
getTotalPatientCount(): Promise<number>;
createPatient(patient: InsertPatient): Promise<Patient>;
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
deletePatient(id: number): Promise<void>;
@@ -304,6 +305,10 @@ export const storage: IStorage = {
});
},
async getTotalPatientCount(): Promise<number> {
return db.patient.count();
},
async createPatient(patient: InsertPatient): Promise<Patient> {
return await db.patient.create({ data: patient as Patient });
},
@@ -371,7 +376,7 @@ export const storage: IStorage = {
return db.appointment.findMany({
skip: offset,
take: limit,
orderBy: { date: "desc" }
orderBy: { date: "desc" },
});
},

View File

@@ -74,7 +74,6 @@ export const AddPatientModal = forwardRef<
>(function AddPatientModal(props, ref) {
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } =
props;
const { toast } = useToast();
const [formData, setFormData] = useState<
InsertPatient | UpdatePatient | null
>(null);

View File

@@ -338,11 +338,12 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
<SelectItem value="placeholder">
Select provider
</SelectItem>
<SelectItem value="delta">Delta Dental</SelectItem>
<SelectItem value="metlife">MetLife</SelectItem>
<SelectItem value="cigna">Cigna</SelectItem>
<SelectItem value="aetna">Aetna</SelectItem>
<SelectItem value="other">Other</SelectItem>
<SelectItem value="Mass Health">Mass Health</SelectItem>
<SelectItem value="Delta MA">Delta MA</SelectItem>
<SelectItem value="Metlife">MetLife</SelectItem>
<SelectItem value="Cigna">Cigna</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
<SelectItem value="Other">Other</SelectItem>
<SelectItem value="none">None</SelectItem>
</SelectContent>
</Select>

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import {
Table,
TableBody,
@@ -7,14 +7,8 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Delete, Edit, Eye, MoreVertical } from "lucide-react";
import { Delete, Edit, Eye } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
@@ -27,29 +21,199 @@ import {
} from "@/components/ui/pagination";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { z } from "zod";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useMutation, useQuery } from "@tanstack/react-query";
import LoadingScreen from "../ui/LoadingScreen";
import { useToast } from "@/hooks/use-toast";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { AddPatientModal } from "./add-patient-modal";
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import { useAuth } from "@/hooks/use-auth";
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>;
interface PatientTableProps {
const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
userId: true,
})
.partial();
type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface PatientApiResponse {
patients: Patient[];
onEdit: (patient: Patient) => void;
onView: (patient: Patient) => void;
onDelete: (patient: Patient) => void;
totalCount: number;
}
export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTableProps) {
interface PatientTableProps {
allowEdit?: boolean;
allowView?: boolean;
allowDelete?: boolean;
}
export function PatientTable({
allowEdit,
allowView,
allowDelete,
}: PatientTableProps) {
const { toast } = useToast();
const { user } = useAuth();
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false);
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
undefined
);
const [currentPage, setCurrentPage] = useState(1);
const patientsPerPage = 5;
const offset = (currentPage - 1) * patientsPerPage;
// Get current patients
const indexOfLastPatient = currentPage * patientsPerPage;
const indexOfFirstPatient = indexOfLastPatient - patientsPerPage;
const currentPatients = patients.slice(indexOfFirstPatient, indexOfLastPatient);
const totalPages = Math.ceil(patients.length / patientsPerPage);
const {
data: patientsData,
isLoading,
isError,
} = useQuery<PatientApiResponse>({
queryKey: ["patients", currentPage],
queryFn: async () => {
const res = await apiRequest(
"GET",
`/api/patients/recent?limit=${patientsPerPage}&offset=${offset}`
);
return res.json();
},
placeholderData: {
patients: [],
totalCount: 0,
},
});
// Update patient mutation
const updatePatientMutation = useMutation({
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: ["patients", currentPage] });
toast({
title: "Success",
description: "Patient updated successfully!",
variant: "default",
});
},
onError: (error) => {
toast({
title: "Error",
description: `Failed to update patient: ${error.message}`,
variant: "destructive",
});
},
});
const deletePatientMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/patients/${id}`);
return;
},
onSuccess: () => {
setIsDeletePatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["patients", currentPage] });
toast({
title: "Success",
description: "Patient deleted successfully!",
variant: "default",
});
},
onError: (error) => {
console.log(error);
toast({
title: "Error",
description: `Failed to delete patient: ${error.message}`,
variant: "destructive",
});
},
});
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",
});
}
};
const handleEditPatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsAddPatientOpen(true);
};
const handleViewPatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsViewPatientOpen(true);
};
const handleDeletePatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsDeletePatientOpen(true);
};
const handleConfirmDeletePatient = async () => {
if (currentPatient) {
deletePatientMutation.mutate(currentPatient.id);
} else {
toast({
title: "Error",
description: "No patient selected for deletion.",
variant: "destructive",
});
}
};
const totalPages = useMemo(
() => Math.ceil((patientsData?.totalCount || 0) / patientsPerPage),
[patientsData]
);
const startItem = offset + 1;
const endItem = Math.min(
offset + patientsPerPage,
patientsData?.totalCount || 0
);
const getInitials = (firstName: string, lastName: string) => {
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
@@ -65,17 +229,15 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
"bg-green-500",
"bg-purple-500",
];
// This returns a literal string from above — not a generated string
return colorClasses[id % colorClasses.length];
};
const formatDate = (dateString: string | Date) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
day: '2-digit',
month: 'short',
year: 'numeric'
return new Intl.DateTimeFormat("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
}).format(date);
};
@@ -94,18 +256,41 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
</TableRow>
</TableHeader>
<TableBody>
{currentPatients.length === 0 ? (
{isLoading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
No patients found. Add your first patient to get started.
<TableCell
colSpan={6}
className="text-center py-8 text-muted-foreground"
>
<LoadingScreen />
</TableCell>
</TableRow>
) : isError ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-red-500"
>
Error loading patients.
</TableCell>
</TableRow>
) : patientsData?.patients.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-muted-foreground"
>
No patients found.
</TableCell>
</TableRow>
) : (
currentPatients.map((patient) => (
patientsData?.patients.map((patient) => (
<TableRow key={patient.id} className="hover:bg-gray-50">
<TableCell>
<div className="flex items-center">
<Avatar className={`h-10 w-10 ${getAvatarColor(patient.id)}`}>
<Avatar
className={`h-10 w-10 ${getAvatarColor(patient.id)}`}
>
<AvatarFallback className="text-white">
{getInitials(patient.firstName, patient.lastName)}
</AvatarFallback>
@@ -116,7 +301,7 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
{patient.firstName} {patient.lastName}
</div>
<div className="text-sm text-gray-500">
PID-{patient.id.toString().padStart(4, '0')}
PID-{patient.id.toString().padStart(4, "0")}
</div>
</div>
</div>
@@ -135,21 +320,7 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{patient.insuranceProvider ? (
patient.insuranceProvider === 'delta'
? 'Delta Dental'
: patient.insuranceProvider === 'metlife'
? 'MetLife'
: patient.insuranceProvider === 'cigna'
? 'Cigna'
: patient.insuranceProvider === 'aetna'
? 'Aetna'
: patient.insuranceProvider === 'none'
? 'No Insurance'
: patient.insuranceProvider
) : (
'Not specified'
)}
{patient.insuranceProvider ?? "Not specified"}
</div>
{patient.insuranceId && (
<div className="text-sm text-gray-500">
@@ -159,7 +330,9 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
</TableCell>
<TableCell>
<Badge
variant={patient.status === 'active' ? 'success' : 'warning'}
variant={
patient.status === "active" ? "success" : "warning"
}
className="capitalize"
>
{patient.status}
@@ -167,10 +340,11 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
{allowDelete && (
<Button
onClick={() =>
onDelete(patient)
}
onClick={() => {
handleDeletePatient(patient);
}}
className="text-red-600 hover:text-red-900"
aria-label="Delete Staff"
variant="ghost"
@@ -178,22 +352,31 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
>
<Delete />
</Button>
)}
{allowEdit && (
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(patient)}
onClick={() => {
handleEditPatient(patient);
}}
className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
>
<Edit className="h-4 w-4" />
</Button>
)}
{allowView && (
<Button
variant="ghost"
size="icon"
onClick={() => onView(patient)}
onClick={() => {
handleViewPatient(patient);
}}
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
>
<Eye className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
@@ -203,19 +386,189 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
</Table>
</div>
{patients.length > patientsPerPage && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200">
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
{/* View Patient Modal */}
<Dialog open={isViewPatientOpen} onOpenChange={setIsViewPatientOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Patient Details</DialogTitle>
<DialogDescription>
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)}
</div>
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{indexOfFirstPatient + 1}</span> to{" "}
<span className="font-medium">
{Math.min(indexOfLastPatient, patients.length)}
</span>{" "}
of <span className="font-medium">{patients.length}</span> results
<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>
<div className="mt-2 space-y-2">
<p>
<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)}
</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>
</p>
</div>
</div>
<div>
<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>{" "}
{currentPatient.phone}
</p>
<p>
<span className="text-gray-500">Email:</span>{" "}
{currentPatient.email || "N/A"}
</p>
<p>
<span className="text-gray-500">Address:</span>{" "}
{currentPatient.address ? (
<>
{currentPatient.address}
{currentPatient.city && `, ${currentPatient.city}`}
{currentPatient.zipCode &&
` ${currentPatient.zipCode}`}
</>
) : (
"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"}
</p>
<p>
<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"}
</p>
<p>
<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>
<div className="mt-2 space-y-2">
<p>
<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"}
</p>
</div>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
variant="outline"
onClick={() => setIsViewPatientOpen(false)}
>
Close
</Button>
<Button
onClick={() => {
setIsViewPatientOpen(false);
handleEditPatient(currentPatient);
}}
>
Edit Patient
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* Add/Edit Patient Modal */}
<AddPatientModal
open={isAddPatientOpen}
onOpenChange={setIsAddPatientOpen}
onSubmit={handleUpdatePatient}
isLoading={isLoading}
patient={currentPatient}
/>
<DeleteConfirmationDialog
isOpen={isDeletePatientOpen}
onConfirm={handleConfirmDeletePatient}
onCancel={() => setIsDeletePatientOpen(false)}
entityName={currentPatient?.name}
/>
{/* Pagination */}
{totalPages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-muted-foreground mb-2 sm:mb-0 whitespace-nowrap">
Showing {startItem}{endItem} of {patientsData?.totalCount || 0}{" "}
results
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
@@ -225,7 +578,9 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
className={
currentPage === 1 ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
@@ -243,15 +598,19 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
if (currentPage < totalPages)
setCurrentPage(currentPage + 1);
}}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
</PaginationContent>

View File

@@ -16,7 +16,6 @@ import { AppointmentsByDay } from "@/components/analytics/appointments-by-day";
import { NewPatients } from "@/components/analytics/new-patients";
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {
Users,
Calendar,
@@ -26,15 +25,7 @@ import {
Clock,
} from "lucide-react";
import { Link } from "wouter";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { z } from "zod";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
//creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
@@ -72,17 +63,6 @@ const insertPatientSchema = (
});
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 = {
@@ -93,8 +73,6 @@ type AddPatientModalRef = {
export default function Dashboard() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false);
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
undefined
@@ -160,60 +138,6 @@ export default function Dashboard() {
},
});
// Update patient mutation
const updatePatientMutation = useMutation({
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/"] });
toast({
title: "Success",
description: "Patient updated successfully!",
variant: "default",
});
},
onError: (error) => {
toast({
title: "Error",
description: `Failed to update patient: ${error.message}`,
variant: "destructive",
});
},
});
const deletePatientMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/patients/${id}`);
return;
},
onSuccess: () => {
setIsDeletePatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({
title: "Success",
description: "Patient deleted successfully!",
variant: "default",
});
},
onError: (error) => {
console.log(error);
toast({
title: "Error",
description: `Failed to delete patient: ${error.message}`,
variant: "destructive",
});
},
});
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
@@ -227,54 +151,9 @@ export default function Dashboard() {
}
};
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",
});
}
};
const handleEditPatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsAddPatientOpen(true);
};
const handleViewPatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsViewPatientOpen(true);
};
const handleDeletePatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsDeletePatientOpen(true);
};
const handleConfirmDeletePatient = async () => {
if (currentPatient) {
deletePatientMutation.mutate(currentPatient.id);
} else {
toast({
title: "Error",
description: "No patient selected for deletion.",
variant: "destructive",
});
}
};
const isLoading =
isLoadingPatients ||
addPatientMutation.isPending ||
updatePatientMutation.isPending;
addPatientMutation.isPending;
// Create appointment mutation
const createAppointmentMutation = useMutation({
@@ -570,17 +449,9 @@ export default function Dashboard() {
{/* Patient Table */}
<PatientTable
patients={filteredPatients}
onEdit={handleEditPatient}
onView={handleViewPatient}
onDelete={handleDeletePatient}
/>
<DeleteConfirmationDialog
isOpen={isDeletePatientOpen}
onConfirm={handleConfirmDeletePatient}
onCancel={() => setIsDeletePatientOpen(false)}
entityName={currentPatient?.name}
allowDelete={true}
allowEdit={true}
allowView={true}
/>
</div>
</main>
@@ -591,187 +462,11 @@ export default function Dashboard() {
ref={addPatientModalRef}
open={isAddPatientOpen}
onOpenChange={setIsAddPatientOpen}
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
onSubmit={handleAddPatient}
isLoading={isLoading}
patient={currentPatient}
/>
{/* View Patient Modal */}
<Dialog open={isViewPatientOpen} onOpenChange={setIsViewPatientOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Patient Details</DialogTitle>
<DialogDescription>
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)}
</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>
</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>
<div className="mt-2 space-y-2">
<p>
{currentPatient.dateOfBirth ? (
(() => {
const dobDate = parseISO(currentPatient.dateOfBirth);
return isValid(dobDate) ? (
<span>
<span className="text-gray-500">
Date of Birth:
</span>{" "}
{format(dobDate, "PPP")}
</span>
) : (
<span className="text-gray-500">
Date of Birth: N/A
</span>
);
})()
) : (
<span className="text-gray-500">
Date of Birth: N/A
</span>
)}
</p>
<p>
<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>
</p>
</div>
</div>
<div>
<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>{" "}
{currentPatient.phone}
</p>
<p>
<span className="text-gray-500">Email:</span>{" "}
{currentPatient.email || "N/A"}
</p>
<p>
<span className="text-gray-500">Address:</span>{" "}
{currentPatient.address ? (
<>
{currentPatient.address}
{currentPatient.city && `, ${currentPatient.city}`}
{currentPatient.zipCode &&
` ${currentPatient.zipCode}`}
</>
) : (
"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"}
</p>
<p>
<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"}
</p>
<p>
<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>
<div className="mt-2 space-y-2">
<p>
<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"}
</p>
</div>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
variant="outline"
onClick={() => setIsViewPatientOpen(false)}
>
Close
</Button>
<Button
onClick={() => {
setIsViewPatientOpen(false);
handleEditPatient(currentPatient);
}}
>
Edit Patient
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* Add/Edit Appointment Modal */}
<AddAppointmentModal
open={isAddAppointmentOpen}

View File

@@ -300,98 +300,6 @@ export default function InsuranceEligibilityPage() {
{checkInsuranceMutation.isPending && selectedProvider === 'MH' ? 'Checking...' : 'MH'}
</Button>
</div>
<div>
<Button
onClick={handleDeltaMACheck}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
Delta MA
</Button>
</div>
<div>
<Button
onClick={handleMetlifeDentalCheck}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
Metlife Dental
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Button
onClick={() => console.log("Checking Tufts eligibility:", { memberId, dateOfBirth, firstName, lastName })}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
Tufts SCO/SWH/Navi/Mass Gen
</Button>
</div>
<div>
<Button
onClick={() => console.log("Checking United SCO eligibility:", { memberId, dateOfBirth, firstName, lastName })}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
United SCO
</Button>
</div>
<div>
<Button
onClick={() => console.log("Checking United AAPR eligibility:", { memberId, dateOfBirth, firstName, lastName })}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
United AAPR
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Button
onClick={() => console.log("Checking CCA eligibility:", { memberId, dateOfBirth, firstName, lastName })}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
CCA
</Button>
</div>
<div>
<Button
onClick={() => console.log("Checking Aetna eligibility:", { memberId, dateOfBirth, firstName, lastName })}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
Aetna
</Button>
</div>
<div>
<Button
onClick={() => console.log("Checking Altus eligibility:", { memberId, dateOfBirth, firstName, lastName })}
className="w-full"
variant="outline"
disabled={!memberId || !dateOfBirth || !firstName || !lastName}
>
<CheckCircle className="h-4 w-4 mr-2" />
Altus
</Button>
</div>
</div>
</div>
</CardContent>

View File

@@ -23,14 +23,6 @@ import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useAuth } from "@/hooks/use-auth";
import { z } from "zod";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import useExtractPdfData from "@/hooks/use-extractPdfData";
import { useLocation } from "wouter";
@@ -50,18 +42,6 @@ const insertPatientSchema = (
});
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;
@@ -72,8 +52,6 @@ export default function PatientsPage() {
const { toast } = useToast();
const { user } = useAuth();
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false);
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
undefined
);
@@ -90,20 +68,6 @@ export default function PatientsPage() {
const { mutate: extractPdf } = useExtractPdfData();
const [location, navigate] = useLocation();
// Fetch patients
const {
data: patients = [],
isLoading: isLoadingPatients,
refetch: refetchPatients,
} = useQuery<Patient[]>({
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) => {
@@ -133,60 +97,6 @@ export default function PatientsPage() {
},
});
// Update patient mutation
const updatePatientMutation = useMutation({
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/"] });
toast({
title: "Success",
description: "Patient updated successfully!",
variant: "default",
});
},
onError: (error) => {
toast({
title: "Error",
description: `Failed to update patient: ${error.message}`,
variant: "destructive",
});
},
});
const deletePatientMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/patients/${id}`);
return;
},
onSuccess: () => {
setIsDeletePatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({
title: "Success",
description: "Patient deleted successfully!",
variant: "default",
});
},
onError: (error) => {
console.log(error);
toast({
title: "Error",
description: `Failed to delete patient: ${error.message}`,
variant: "destructive",
});
},
});
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
@@ -200,54 +110,7 @@ export default function PatientsPage() {
}
};
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",
});
}
};
const handleEditPatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsAddPatientOpen(true);
};
const handleViewPatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsViewPatientOpen(true);
};
const handleDeletePatient = (patient: Patient) => {
setCurrentPatient(patient);
setIsDeletePatientOpen(true);
};
const handleConfirmDeletePatient = async () => {
if (currentPatient) {
deletePatientMutation.mutate(currentPatient.id);
} else {
toast({
title: "Error",
description: "No patient selected for deletion.",
variant: "destructive",
});
}
};
const isLoading =
isLoadingPatients ||
addPatientMutation.isPending ||
updatePatientMutation.isPending;
const isLoading = addPatientMutation.isPending;
// Search handling
const handleSearch = (criteria: SearchCriteria) => {
@@ -258,42 +121,6 @@ export default function PatientsPage() {
setSearchCriteria(null);
};
// 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":
return (
patient.firstName.toLowerCase().includes(term) ||
patient.lastName.toLowerCase().includes(term)
);
case "phone":
return patient.phone.toLowerCase().includes(term);
case "insuranceProvider":
return patient.insuranceProvider?.toLowerCase().includes(term);
case "insuranceId":
return patient.insuranceId?.toLowerCase().includes(term);
case "all":
default:
return (
patient.firstName.toLowerCase().includes(term) ||
patient.lastName.toLowerCase().includes(term) ||
patient.phone.toLowerCase().includes(term) ||
patient.email?.toLowerCase().includes(term) ||
patient.address?.toLowerCase().includes(term) ||
patient.city?.toLowerCase().includes(term) ||
patient.insuranceProvider?.toLowerCase().includes(term) ||
patient.insuranceId?.toLowerCase().includes(term)
);
}
});
}, [patients, searchCriteria]);
// File upload handling
const handleFileUpload = (file: File) => {
setIsUploading(true);
@@ -373,16 +200,6 @@ export default function PatientsPage() {
<Plus className="h-4 w-4" />
New Patient
</Button>
<Button
variant="outline"
size="sm"
className="h-9 gap-1"
onClick={() => refetchPatients()}
disabled={isLoading}
>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
</div>
@@ -441,213 +258,20 @@ export default function PatientsPage() {
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}`
: ""}
matching "{searchCriteria.searchTerm}"
</p>
</div>
)}
<PatientTable
patients={filteredPatients}
onEdit={handleEditPatient}
onView={handleViewPatient}
onDelete={handleDeletePatient}
/>
<DeleteConfirmationDialog
isOpen={isDeletePatientOpen}
onConfirm={handleConfirmDeletePatient}
onCancel={() => setIsDeletePatientOpen(false)}
entityName={currentPatient?.name}
allowDelete={true}
allowEdit={true}
allowView={true}
/>
</CardContent>
</Card>
{/* View Patient Modal */}
<Dialog
open={isViewPatientOpen}
onOpenChange={setIsViewPatientOpen}
>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Patient Details</DialogTitle>
<DialogDescription>
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)}
</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>
</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>
<div className="mt-2 space-y-2">
<p>
<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)}
</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>
</p>
</div>
</div>
<div>
<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>{" "}
{currentPatient.phone}
</p>
<p>
<span className="text-gray-500">Email:</span>{" "}
{currentPatient.email || "N/A"}
</p>
<p>
<span className="text-gray-500">Address:</span>{" "}
{currentPatient.address ? (
<>
{currentPatient.address}
{currentPatient.city &&
`, ${currentPatient.city}`}
{currentPatient.zipCode &&
` ${currentPatient.zipCode}`}
</>
) : (
"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"}
</p>
<p>
<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"}
</p>
<p>
<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>
<div className="mt-2 space-y-2">
<p>
<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"}
</p>
</div>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
variant="outline"
onClick={() => setIsViewPatientOpen(false)}
>
Close
</Button>
<Button
onClick={() => {
setIsViewPatientOpen(false);
handleEditPatient(currentPatient);
}}
>
Edit Patient
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* Add/Edit Patient Modal */}
<AddPatientModal
ref={addPatientModalRef}
open={isAddPatientOpen}
onOpenChange={setIsAddPatientOpen}
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
onSubmit={handleAddPatient}
isLoading={isLoading}
patient={currentPatient}
/>