recent patients done, actions are done, now have to add search criteria
This commit is contained in:
@@ -4,7 +4,7 @@ import { storage } from "../storage";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import { forwardToSeleniumAgent } from "../services/seleniumClient";
|
import { forwardToSeleniumAgent } from "../services/seleniumClaimClient";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|||||||
@@ -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
|
// Get a single patient by ID
|
||||||
router.get(
|
router.get(
|
||||||
"/:id",
|
"/: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
|
// Create a new patient
|
||||||
router.post("/", async (req: Request, res: Response): Promise<any> => {
|
router.post("/", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export interface IStorage {
|
|||||||
getPatient(id: number): Promise<Patient | undefined>;
|
getPatient(id: number): Promise<Patient | undefined>;
|
||||||
getPatientsByUserId(userId: number): Promise<Patient[]>;
|
getPatientsByUserId(userId: number): Promise<Patient[]>;
|
||||||
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
|
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
|
||||||
|
getTotalPatientCount(): Promise<number>;
|
||||||
createPatient(patient: InsertPatient): Promise<Patient>;
|
createPatient(patient: InsertPatient): Promise<Patient>;
|
||||||
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
|
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
|
||||||
deletePatient(id: number): Promise<void>;
|
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> {
|
async createPatient(patient: InsertPatient): Promise<Patient> {
|
||||||
return await db.patient.create({ data: patient as Patient });
|
return await db.patient.create({ data: patient as Patient });
|
||||||
},
|
},
|
||||||
@@ -371,7 +376,7 @@ export const storage: IStorage = {
|
|||||||
return db.appointment.findMany({
|
return db.appointment.findMany({
|
||||||
skip: offset,
|
skip: offset,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { date: "desc" }
|
orderBy: { date: "desc" },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -589,7 +594,7 @@ export const storage: IStorage = {
|
|||||||
id: true,
|
id: true,
|
||||||
filename: true,
|
filename: true,
|
||||||
uploadedAt: true,
|
uploadedAt: true,
|
||||||
patient:true,
|
patient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export const AddPatientModal = forwardRef<
|
|||||||
>(function AddPatientModal(props, ref) {
|
>(function AddPatientModal(props, ref) {
|
||||||
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } =
|
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } =
|
||||||
props;
|
props;
|
||||||
const { toast } = useToast();
|
|
||||||
const [formData, setFormData] = useState<
|
const [formData, setFormData] = useState<
|
||||||
InsertPatient | UpdatePatient | null
|
InsertPatient | UpdatePatient | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|||||||
@@ -338,11 +338,12 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
|||||||
<SelectItem value="placeholder">
|
<SelectItem value="placeholder">
|
||||||
Select provider
|
Select provider
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="delta">Delta Dental</SelectItem>
|
<SelectItem value="Mass Health">Mass Health</SelectItem>
|
||||||
<SelectItem value="metlife">MetLife</SelectItem>
|
<SelectItem value="Delta MA">Delta MA</SelectItem>
|
||||||
<SelectItem value="cigna">Cigna</SelectItem>
|
<SelectItem value="Metlife">MetLife</SelectItem>
|
||||||
<SelectItem value="aetna">Aetna</SelectItem>
|
<SelectItem value="Cigna">Cigna</SelectItem>
|
||||||
<SelectItem value="other">Other</SelectItem>
|
<SelectItem value="Aetna">Aetna</SelectItem>
|
||||||
|
<SelectItem value="Other">Other</SelectItem>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -7,14 +7,8 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -26,56 +20,224 @@ import {
|
|||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
import {z} from "zod";
|
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,
|
appointments: true,
|
||||||
});
|
});
|
||||||
type Patient = z.infer<typeof PatientSchema>;
|
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[];
|
patients: Patient[];
|
||||||
onEdit: (patient: Patient) => void;
|
totalCount: number;
|
||||||
onView: (patient: Patient) => void;
|
|
||||||
onDelete: (patient: Patient) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [currentPage, setCurrentPage] = useState(1);
|
||||||
const patientsPerPage = 5;
|
const patientsPerPage = 5;
|
||||||
|
const offset = (currentPage - 1) * patientsPerPage;
|
||||||
|
|
||||||
// Get current patients
|
const {
|
||||||
const indexOfLastPatient = currentPage * patientsPerPage;
|
data: patientsData,
|
||||||
const indexOfFirstPatient = indexOfLastPatient - patientsPerPage;
|
isLoading,
|
||||||
const currentPatients = patients.slice(indexOfFirstPatient, indexOfLastPatient);
|
isError,
|
||||||
const totalPages = Math.ceil(patients.length / patientsPerPage);
|
} = 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) => {
|
const getInitials = (firstName: string, lastName: string) => {
|
||||||
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
|
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvatarColor = (id: number) => {
|
const getAvatarColor = (id: number) => {
|
||||||
const colorClasses = [
|
const colorClasses = [
|
||||||
"bg-blue-500",
|
"bg-blue-500",
|
||||||
"bg-teal-500",
|
"bg-teal-500",
|
||||||
"bg-amber-500",
|
"bg-amber-500",
|
||||||
"bg-rose-500",
|
"bg-rose-500",
|
||||||
"bg-indigo-500",
|
"bg-indigo-500",
|
||||||
"bg-green-500",
|
"bg-green-500",
|
||||||
"bg-purple-500",
|
"bg-purple-500",
|
||||||
];
|
];
|
||||||
|
return colorClasses[id % colorClasses.length];
|
||||||
// This returns a literal string from above — not a generated string
|
};
|
||||||
return colorClasses[id % colorClasses.length];
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string | Date) => {
|
const formatDate = (dateString: string | Date) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return new Intl.DateTimeFormat('en-US', {
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
day: '2-digit',
|
day: "2-digit",
|
||||||
month: 'short',
|
month: "short",
|
||||||
year: 'numeric'
|
year: "numeric",
|
||||||
}).format(date);
|
}).format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,18 +256,41 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{currentPatients.length === 0 ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
<TableCell
|
||||||
No patients found. Add your first patient to get started.
|
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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
currentPatients.map((patient) => (
|
patientsData?.patients.map((patient) => (
|
||||||
<TableRow key={patient.id} className="hover:bg-gray-50">
|
<TableRow key={patient.id} className="hover:bg-gray-50">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center">
|
<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">
|
<AvatarFallback className="text-white">
|
||||||
{getInitials(patient.firstName, patient.lastName)}
|
{getInitials(patient.firstName, patient.lastName)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@@ -116,7 +301,7 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
{patient.firstName} {patient.lastName}
|
{patient.firstName} {patient.lastName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
PID-{patient.id.toString().padStart(4, '0')}
|
PID-{patient.id.toString().padStart(4, "0")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,21 +320,7 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
{patient.insuranceProvider ? (
|
{patient.insuranceProvider ?? "Not specified"}
|
||||||
patient.insuranceProvider === 'delta'
|
|
||||||
? 'Delta Dental'
|
|
||||||
: patient.insuranceProvider === 'metlife'
|
|
||||||
? 'MetLife'
|
|
||||||
: patient.insuranceProvider === 'cigna'
|
|
||||||
? 'Cigna'
|
|
||||||
: patient.insuranceProvider === 'aetna'
|
|
||||||
? 'Aetna'
|
|
||||||
: patient.insuranceProvider === 'none'
|
|
||||||
? 'No Insurance'
|
|
||||||
: patient.insuranceProvider
|
|
||||||
) : (
|
|
||||||
'Not specified'
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{patient.insuranceId && (
|
{patient.insuranceId && (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
@@ -159,7 +330,9 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={patient.status === 'active' ? 'success' : 'warning'}
|
variant={
|
||||||
|
patient.status === "active" ? "success" : "warning"
|
||||||
|
}
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
>
|
>
|
||||||
{patient.status}
|
{patient.status}
|
||||||
@@ -167,33 +340,43 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<Button
|
{allowDelete && (
|
||||||
onClick={() =>
|
<Button
|
||||||
onDelete(patient)
|
onClick={() => {
|
||||||
}
|
handleDeletePatient(patient);
|
||||||
className="text-red-600 hover:text-red-900"
|
}}
|
||||||
aria-label="Delete Staff"
|
className="text-red-600 hover:text-red-900"
|
||||||
variant="ghost"
|
aria-label="Delete Staff"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
>
|
size="icon"
|
||||||
<Delete/>
|
>
|
||||||
</Button>
|
<Delete />
|
||||||
<Button
|
</Button>
|
||||||
variant="ghost"
|
)}
|
||||||
size="icon"
|
{allowEdit && (
|
||||||
onClick={() => onEdit(patient)}
|
<Button
|
||||||
className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
variant="ghost"
|
||||||
>
|
size="icon"
|
||||||
<Edit className="h-4 w-4" />
|
onClick={() => {
|
||||||
</Button>
|
handleEditPatient(patient);
|
||||||
<Button
|
}}
|
||||||
variant="ghost"
|
className="text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
||||||
size="icon"
|
>
|
||||||
onClick={() => onView(patient)}
|
<Edit className="h-4 w-4" />
|
||||||
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
|
</Button>
|
||||||
>
|
)}
|
||||||
<Eye className="h-4 w-4" />
|
{allowView && (
|
||||||
</Button>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
handleViewPatient(patient);
|
||||||
|
}}
|
||||||
|
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -203,19 +386,189 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{patients.length > patientsPerPage && (
|
{/* View Patient Modal */}
|
||||||
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200">
|
<Dialog open={isViewPatientOpen} onOpenChange={setIsViewPatientOpen}>
|
||||||
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
<div>
|
<DialogHeader>
|
||||||
<p className="text-sm text-gray-700">
|
<DialogTitle>Patient Details</DialogTitle>
|
||||||
Showing <span className="font-medium">{indexOfFirstPatient + 1}</span> to{" "}
|
<DialogDescription>
|
||||||
<span className="font-medium">
|
Complete information about the patient.
|
||||||
{Math.min(indexOfLastPatient, patients.length)}
|
</DialogDescription>
|
||||||
</span>{" "}
|
</DialogHeader>
|
||||||
of <span className="font-medium">{patients.length}</span> results
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{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
|
||||||
|
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>
|
<Pagination>
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
@@ -225,7 +578,9 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (currentPage > 1) setCurrentPage(currentPage - 1);
|
if (currentPage > 1) setCurrentPage(currentPage - 1);
|
||||||
}}
|
}}
|
||||||
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
className={
|
||||||
|
currentPage === 1 ? "pointer-events-none opacity-50" : ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
|
|
||||||
@@ -243,15 +598,19 @@ export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTabl
|
|||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationNext
|
<PaginationNext
|
||||||
href="#"
|
href="#"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
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>
|
</PaginationItem>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { AppointmentsByDay } from "@/components/analytics/appointments-by-day";
|
|||||||
import { NewPatients } from "@/components/analytics/new-patients";
|
import { NewPatients } from "@/components/analytics/new-patients";
|
||||||
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -26,15 +25,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
|
||||||
|
|
||||||
//creating types out of schema auto generated.
|
//creating types out of schema auto generated.
|
||||||
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
|
||||||
@@ -72,17 +63,6 @@ const insertPatientSchema = (
|
|||||||
});
|
});
|
||||||
type InsertPatient = z.infer<typeof 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 for the ref to access modal methods
|
||||||
type AddPatientModalRef = {
|
type AddPatientModalRef = {
|
||||||
@@ -93,8 +73,6 @@ type AddPatientModalRef = {
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
||||||
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
|
||||||
const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false);
|
|
||||||
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
|
const [isAddAppointmentOpen, setIsAddAppointmentOpen] = useState(false);
|
||||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
||||||
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 = () => {
|
const toggleMobileMenu = () => {
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
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 =
|
const isLoading =
|
||||||
isLoadingPatients ||
|
isLoadingPatients ||
|
||||||
addPatientMutation.isPending ||
|
addPatientMutation.isPending;
|
||||||
updatePatientMutation.isPending;
|
|
||||||
|
|
||||||
// Create appointment mutation
|
// Create appointment mutation
|
||||||
const createAppointmentMutation = useMutation({
|
const createAppointmentMutation = useMutation({
|
||||||
@@ -570,17 +449,9 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Patient Table */}
|
{/* Patient Table */}
|
||||||
<PatientTable
|
<PatientTable
|
||||||
patients={filteredPatients}
|
allowDelete={true}
|
||||||
onEdit={handleEditPatient}
|
allowEdit={true}
|
||||||
onView={handleViewPatient}
|
allowView={true}
|
||||||
onDelete={handleDeletePatient}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
isOpen={isDeletePatientOpen}
|
|
||||||
onConfirm={handleConfirmDeletePatient}
|
|
||||||
onCancel={() => setIsDeletePatientOpen(false)}
|
|
||||||
entityName={currentPatient?.name}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -591,187 +462,11 @@ export default function Dashboard() {
|
|||||||
ref={addPatientModalRef}
|
ref={addPatientModalRef}
|
||||||
open={isAddPatientOpen}
|
open={isAddPatientOpen}
|
||||||
onOpenChange={setIsAddPatientOpen}
|
onOpenChange={setIsAddPatientOpen}
|
||||||
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
|
onSubmit={handleAddPatient}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
patient={currentPatient}
|
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 */}
|
{/* Add/Edit Appointment Modal */}
|
||||||
<AddAppointmentModal
|
<AddAppointmentModal
|
||||||
open={isAddAppointmentOpen}
|
open={isAddAppointmentOpen}
|
||||||
|
|||||||
@@ -300,98 +300,6 @@ export default function InsuranceEligibilityPage() {
|
|||||||
{checkInsuranceMutation.isPending && selectedProvider === 'MH' ? 'Checking...' : 'MH'}
|
{checkInsuranceMutation.isPending && selectedProvider === 'MH' ? 'Checking...' : 'MH'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -23,14 +23,6 @@ import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { z } from "zod";
|
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 useExtractPdfData from "@/hooks/use-extractPdfData";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
@@ -50,18 +42,6 @@ const insertPatientSchema = (
|
|||||||
});
|
});
|
||||||
type InsertPatient = z.infer<typeof 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 for the ref to access modal methods
|
||||||
type AddPatientModalRef = {
|
type AddPatientModalRef = {
|
||||||
shouldSchedule: boolean;
|
shouldSchedule: boolean;
|
||||||
@@ -72,8 +52,6 @@ export default function PatientsPage() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
||||||
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
|
|
||||||
const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false);
|
|
||||||
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
@@ -90,20 +68,6 @@ export default function PatientsPage() {
|
|||||||
const { mutate: extractPdf } = useExtractPdfData();
|
const { mutate: extractPdf } = useExtractPdfData();
|
||||||
const [location, navigate] = useLocation();
|
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
|
// Add patient mutation
|
||||||
const addPatientMutation = useMutation({
|
const addPatientMutation = useMutation({
|
||||||
mutationFn: async (patient: InsertPatient) => {
|
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 = () => {
|
const toggleMobileMenu = () => {
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
};
|
};
|
||||||
@@ -200,54 +110,7 @@ export default function PatientsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
|
const isLoading = addPatientMutation.isPending;
|
||||||
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;
|
|
||||||
|
|
||||||
// Search handling
|
// Search handling
|
||||||
const handleSearch = (criteria: SearchCriteria) => {
|
const handleSearch = (criteria: SearchCriteria) => {
|
||||||
@@ -258,42 +121,6 @@ export default function PatientsPage() {
|
|||||||
setSearchCriteria(null);
|
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
|
// File upload handling
|
||||||
const handleFileUpload = (file: File) => {
|
const handleFileUpload = (file: File) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
@@ -373,16 +200,6 @@ export default function PatientsPage() {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
New Patient
|
New Patient
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -441,213 +258,20 @@ export default function PatientsPage() {
|
|||||||
isSearchActive={!!searchCriteria}
|
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
|
<PatientTable
|
||||||
patients={filteredPatients}
|
allowDelete={true}
|
||||||
onEdit={handleEditPatient}
|
allowEdit={true}
|
||||||
onView={handleViewPatient}
|
allowView={true}
|
||||||
onDelete={handleDeletePatient}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
isOpen={isDeletePatientOpen}
|
|
||||||
onConfirm={handleConfirmDeletePatient}
|
|
||||||
onCancel={() => setIsDeletePatientOpen(false)}
|
|
||||||
entityName={currentPatient?.name}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 */}
|
{/* Add/Edit Patient Modal */}
|
||||||
<AddPatientModal
|
<AddPatientModal
|
||||||
ref={addPatientModalRef}
|
ref={addPatientModalRef}
|
||||||
open={isAddPatientOpen}
|
open={isAddPatientOpen}
|
||||||
onOpenChange={setIsAddPatientOpen}
|
onOpenChange={setIsAddPatientOpen}
|
||||||
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
|
onSubmit={handleAddPatient}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
patient={currentPatient}
|
patient={currentPatient}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user