savenschedule working and delete issue in pateint page, add patint issue on dashboard and patient page

This commit is contained in:
2025-05-16 14:28:17 +05:30
parent 7727ad862c
commit fb6b28d3fb
7 changed files with 386 additions and 35 deletions

View File

@@ -200,6 +200,7 @@ router.delete(
await storage.deletePatient(patientId);
res.status(204).send();
} catch (error) {
console.error("Delete patient error:", error);
res.status(500).json({ message: "Failed to delete patient" });
}
}

View File

@@ -14,7 +14,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Edit, Eye, MoreVertical } from "lucide-react";
import { Delete, Edit, Eye, MoreVertical } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
@@ -39,9 +39,10 @@ interface PatientTableProps {
patients: Patient[];
onEdit: (patient: Patient) => void;
onView: (patient: Patient) => void;
onDelete: (patient: Patient) => void;
}
export function PatientTable({ patients, onEdit, onView }: PatientTableProps) {
export function PatientTable({ patients, onEdit, onView, onDelete }: PatientTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const patientsPerPage = 5;
@@ -167,6 +168,17 @@ export function PatientTable({ patients, onEdit, onView }: PatientTableProps) {
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button
onClick={() =>
onDelete(patient)
}
className="text-red-600 hover:text-red-900"
aria-label="Delete Staff"
variant="ghost"
size="icon"
>
<Delete/>
</Button>
<Button
variant="ghost"
size="icon"

View File

@@ -1,6 +1,8 @@
import React, { useState } from "react";
import { z } from "zod";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import { Button } from "../ui/button";
import { Delete, Edit } from "lucide-react";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
@@ -15,7 +17,7 @@ interface StaffTableProps {
isError?: boolean;
onAdd: () => void;
onEdit: (staff: Staff) => void;
onDelete: (id: number) => void;
onDelete: (staff: Staff) => void;
onView: (staff: Staff) => void;
}
@@ -148,22 +150,27 @@ export function StaffTable({
{formattedDate}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<button
onClick={() => staff.id !== undefined && onEdit(staff)}
className="text-blue-600 hover:text-blue-900"
aria-label="Edit Staff"
>
Edit
</button>
<button
<Button
onClick={() =>
staff.id !== undefined && onDelete(staff.id)
staff !== undefined && onDelete(staff)
}
className="text-red-600 hover:text-red-900"
aria-label="Delete Staff"
variant="ghost"
size="icon"
>
Delete
</button>
<Delete/>
</Button>
<Button
onClick={() => staff.id !== undefined && onEdit(staff)}
className="text-blue-600 hover:text-blue-900"
aria-label="Edit Staff"
variant="ghost"
size="icon"
>
<Edit className="h-4 w-4" />
</Button>
</td>
</tr>
);

View File

@@ -0,0 +1,36 @@
export const DeleteConfirmationDialog = ({
isOpen,
onConfirm,
onCancel,
patientName,
}: {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
patientName?: string;
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
<div className="bg-white p-6 rounded-md shadow-md w-[90%] max-w-md">
<h2 className="text-xl font-semibold mb-4">Confirm Deletion</h2>
<p>Are you sure you want to delete <strong>{patientName}</strong>?</p>
<div className="mt-6 flex justify-end space-x-4">
<button
className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
onClick={onCancel}
>
Cancel
</button>
<button
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700"
onClick={onConfirm}
>
Delete
</button>
</div>
</div>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useRef} from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { format } from "date-fns";
import { TopAppBar } from "@/components/layout/top-app-bar";
@@ -33,6 +33,7 @@ import {
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>;
@@ -82,10 +83,17 @@ const updatePatientSchema = (
type UpdatePatient = z.infer<typeof updatePatientSchema>;
// Type for the ref to access modal methods
type AddPatientModalRef = {
shouldSchedule: boolean;
navigateToSchedule: (patientId: number) => void;
};
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
@@ -96,6 +104,9 @@ export default function Dashboard() {
const { toast } = useToast();
const { user } = useAuth();
const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
// Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
@@ -176,6 +187,31 @@ export default function Dashboard() {
},
});
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);
};
@@ -216,6 +252,28 @@ export default function Dashboard() {
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;
// Create appointment mutation
const createAppointmentMutation = useMutation({
mutationFn: async (appointment: InsertAppointment) => {
@@ -491,19 +549,27 @@ export default function Dashboard() {
patients={filteredPatients}
onEdit={handleEditPatient}
onView={handleViewPatient}
onDelete={handleDeletePatient}
/>
<DeleteConfirmationDialog
isOpen={isDeletePatientOpen}
onConfirm={handleConfirmDeletePatient}
onCancel={() => setIsDeletePatientOpen(false)}
patientName={currentPatient?.name}
/>
</div>
</main>
</div>
{/* Add/Edit Patient Modal */}
<AddPatientModal
ref={addPatientModalRef}
open={isAddPatientOpen}
onOpenChange={setIsAddPatientOpen}
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
isLoading={
addPatientMutation.isPending || updatePatientMutation.isPending
}
isLoading={isLoading}
patient={currentPatient}
/>

View File

@@ -24,6 +24,14 @@ import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas
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";
const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -64,6 +72,7 @@ export default function PatientsPage() {
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
);
@@ -152,12 +161,35 @@ export default function PatientsPage() {
},
});
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);
};
const handleAddPatient = (patient: InsertPatient) => {
// Add userId to the patient data
if (user) {
addPatientMutation.mutate({
...patient,
@@ -193,6 +225,23 @@ export default function PatientsPage() {
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 ||
@@ -370,10 +419,178 @@ export default function PatientsPage() {
patients={filteredPatients}
onEdit={handleEditPatient}
onView={handleViewPatient}
onDelete={handleDeletePatient}
/>
<DeleteConfirmationDialog
isOpen={isDeletePatientOpen}
onConfirm={handleConfirmDeletePatient}
onCancel={() => setIsDeletePatientOpen(false)}
patientName={currentPatient?.name}
/>
</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}
@@ -382,17 +599,6 @@ export default function PatientsPage() {
onSubmit={currentPatient ? handleUpdatePatient : handleAddPatient}
isLoading={isLoading}
patient={currentPatient}
// Pass extracted info as a separate prop to avoid triggering edit mode
extractedInfo={
!currentPatient && extractedInfo
? {
firstName: extractedInfo.firstName || "",
lastName: extractedInfo.lastName || "",
dateOfBirth: extractedInfo.dateOfBirth || "",
insuranceId: extractedInfo.insuranceId || "",
}
: undefined
}
/>
</div>
</main>

View File

@@ -9,6 +9,7 @@ import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import { z } from "zod";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { StaffForm } from "@/components/staffs/staff-form";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
// Correctly infer Staff type from zod schema
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
@@ -120,6 +121,7 @@ export default function SettingsPage() {
return id;
},
onSuccess: () => {
setIsDeleteStaffOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/staffs/"] });
toast({
title: "Staff Removed",
@@ -188,14 +190,28 @@ export default function SettingsPage() {
}
}, [isAddSuccess, isUpdateSuccess]);
// Delete staff
const handleDeleteStaff = (id: number) => {
if (confirm("Are you sure you want to delete this staff member?")) {
deleteStaffMutation.mutate(id);
const [isDeleteStaffOpen, setIsDeleteStaffOpen] = useState(false);
const [currentStaff, setCurrentStaff] = useState<Staff | undefined>(
undefined
);
const handleDeleteStaff = (staff: Staff) => {
setCurrentStaff(staff);
setIsDeleteStaffOpen(true);
};
const handleConfirmDeleteStaff = async () => {
if (currentStaff?.id) {
deleteStaffMutation.mutate(currentStaff.id);
} else {
toast({
title: "Error",
description: "No Staff selected for deletion.",
variant: "destructive",
});
}
};
// View staff handler (just an alert for now)
const handleViewStaff = (staff: Staff) =>
alert(
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`
@@ -227,6 +243,13 @@ export default function SettingsPage() {
{(error as Error)?.message || "Failed to load staff data."}
</p>
)}
<DeleteConfirmationDialog
isOpen={isDeleteStaffOpen}
onConfirm={handleConfirmDeleteStaff}
onCancel={() => setIsDeleteStaffOpen(false)}
patientName={currentStaff?.name}
/>
</div>
</CardContent>
</Card>