done
This commit is contained in:
@@ -28,6 +28,7 @@ interface AddAppointmentModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSubmit: (data: InsertAppointment | UpdateAppointment) => void;
|
onSubmit: (data: InsertAppointment | UpdateAppointment) => void;
|
||||||
|
onDelete?: (id: number) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
appointment?: Appointment;
|
appointment?: Appointment;
|
||||||
patients: Patient[];
|
patients: Patient[];
|
||||||
@@ -37,10 +38,12 @@ export function AddAppointmentModal({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onDelete,
|
||||||
isLoading,
|
isLoading,
|
||||||
appointment,
|
appointment,
|
||||||
patients,
|
patients,
|
||||||
}: AddAppointmentModalProps) {
|
}: AddAppointmentModalProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
@@ -58,6 +61,8 @@ export function AddAppointmentModal({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ interface AppointmentFormProps {
|
|||||||
appointment?: Appointment;
|
appointment?: Appointment;
|
||||||
patients: Patient[];
|
patients: Patient[];
|
||||||
onSubmit: (data: InsertAppointment | UpdateAppointment) => void;
|
onSubmit: (data: InsertAppointment | UpdateAppointment) => void;
|
||||||
|
onDelete?: (id: number) => void;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,18 +82,19 @@ export function AppointmentForm({
|
|||||||
appointment,
|
appointment,
|
||||||
patients,
|
patients,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onDelete,
|
||||||
|
onOpenChange,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: AppointmentFormProps) {
|
}: AppointmentFormProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}, 50); // small delay ensures content is mounted
|
}, 50); // small delay ensures content is mounted
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
|
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
|
||||||
useQuery<Staff[]>({
|
useQuery<Staff[]>({
|
||||||
@@ -183,12 +186,13 @@ useEffect(() => {
|
|||||||
const term = debouncedSearchTerm.toLowerCase();
|
const term = debouncedSearchTerm.toLowerCase();
|
||||||
setFilteredPatients(
|
setFilteredPatients(
|
||||||
patients.filter((p) =>
|
patients.filter((p) =>
|
||||||
`${p.firstName} ${p.lastName} ${p.phone} ${p.dob}`.toLowerCase().includes(term)
|
`${p.firstName} ${p.lastName} ${p.phone} ${p.dob}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(term)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [debouncedSearchTerm, patients]);
|
}, [debouncedSearchTerm, patients]);
|
||||||
|
|
||||||
|
|
||||||
// Force form field values to update and clean up storage
|
// Force form field values to update and clean up storage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -305,7 +309,6 @@ useEffect(() => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<div className="p-2" onKeyDown={(e) => e.stopPropagation()}>
|
<div className="p-2" onKeyDown={(e) => e.stopPropagation()}>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder="Search patients..."
|
placeholder="Search patients..."
|
||||||
@@ -313,7 +316,7 @@ useEffect(() => {
|
|||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
const navKeys = ['ArrowDown', 'ArrowUp', 'Enter'];
|
const navKeys = ["ArrowDown", "ArrowUp", "Enter"];
|
||||||
if (!navKeys.includes(e.key)) {
|
if (!navKeys.includes(e.key)) {
|
||||||
e.stopPropagation(); // Only stop keys that affect select state
|
e.stopPropagation(); // Only stop keys that affect select state
|
||||||
}
|
}
|
||||||
@@ -321,18 +324,22 @@ useEffect(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-60 overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/30">
|
<div className="max-h-60 overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/30">
|
||||||
|
|
||||||
{filteredPatients.length > 0 ? (
|
{filteredPatients.length > 0 ? (
|
||||||
filteredPatients.map((patient) => (
|
filteredPatients.map((patient) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={patient.id}
|
key={patient.id}
|
||||||
value={patient.id.toString()}
|
value={patient.id.toString()}
|
||||||
><div className="flex flex-col">
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{patient.firstName} {patient.lastName}
|
{patient.firstName} {patient.lastName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
DOB: {new Date(patient.dateOfBirth).toLocaleDateString()} • {patient.phone}
|
DOB:{" "}
|
||||||
|
{new Date(
|
||||||
|
patient.dateOfBirth
|
||||||
|
).toLocaleDateString()}{" "}
|
||||||
|
• {patient.phone}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -589,6 +596,22 @@ useEffect(() => {
|
|||||||
<Button type="submit" disabled={isLoading} className="w-full">
|
<Button type="submit" disabled={isLoading} className="w-full">
|
||||||
{appointment ? "Update Appointment" : "Create Appointment"}
|
{appointment ? "Update Appointment" : "Create Appointment"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{appointment?.id && onDelete && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange?.(false); // 👈 Close the modal first
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
onDelete?.(appointment.id!);
|
||||||
|
}, 300); // 300ms is safe for most animations
|
||||||
|
}}
|
||||||
|
className="bg-red-600 text-white w-full rounded hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Delete Appointment
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ export const DeleteConfirmationDialog = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
patientName,
|
entityName,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
patientName?: string;
|
entityName?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ export const DeleteConfirmationDialog = ({
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<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">
|
<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>
|
<h2 className="text-xl font-semibold mb-4">Confirm Deletion</h2>
|
||||||
<p>Are you sure you want to delete <strong>{patientName}</strong>?</p>
|
<p>
|
||||||
|
Are you sure you want to delete <strong>{entityName}</strong>?
|
||||||
|
</p>
|
||||||
<div className="mt-6 flex justify-end space-x-4">
|
<div className="mt-6 flex justify-end space-x-4">
|
||||||
<button
|
<button
|
||||||
className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
|
className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { HTML5Backend } from "react-dnd-html5-backend";
|
|||||||
import { Menu, Item, useContextMenu } from "react-contexify";
|
import { Menu, Item, useContextMenu } from "react-contexify";
|
||||||
import "react-contexify/ReactContexify.css";
|
import "react-contexify/ReactContexify.css";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
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>;
|
||||||
@@ -110,6 +111,11 @@ export default function AppointmentsPage() {
|
|||||||
const [selectedDate, setSelectedDate] = useState<Date>(startOfToday());
|
const [selectedDate, setSelectedDate] = useState<Date>(startOfToday());
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [location] = useLocation();
|
const [location] = useLocation();
|
||||||
|
const [confirmDeleteState, setConfirmDeleteState] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
appointmentId?: number;
|
||||||
|
appointmentTitle?: string;
|
||||||
|
}>({ open: false });
|
||||||
|
|
||||||
// Create context menu hook
|
// Create context menu hook
|
||||||
const { show } = useContextMenu({
|
const { show } = useContextMenu({
|
||||||
@@ -350,6 +356,7 @@ export default function AppointmentsPage() {
|
|||||||
// Invalidate both appointments and patients queries
|
// Invalidate both appointments and patients queries
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
||||||
|
setConfirmDeleteState({ open: false });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -397,11 +404,25 @@ export default function AppointmentsPage() {
|
|||||||
setIsAddModalOpen(true);
|
setIsAddModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete appointment
|
// When user confirms delete in dialog
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (!confirmDeleteState.appointmentId) return;
|
||||||
|
deleteAppointmentMutation.mutate(confirmDeleteState.appointmentId);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteAppointment = (id: number) => {
|
const handleDeleteAppointment = (id: number) => {
|
||||||
if (confirm("Are you sure you want to delete this appointment?")) {
|
const appointment = appointments.find((a) => a.id === id);
|
||||||
deleteAppointmentMutation.mutate(id);
|
if (!appointment) return;
|
||||||
}
|
|
||||||
|
// Find patient by patientId
|
||||||
|
const patient = patients.find((p) => p.id === appointment.patientId);
|
||||||
|
|
||||||
|
|
||||||
|
setConfirmDeleteState({
|
||||||
|
open: true,
|
||||||
|
appointmentId: id,
|
||||||
|
appointmentTitle: `${patient?.firstName ?? "Appointment"}`,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
@@ -911,6 +932,14 @@ export default function AppointmentsPage() {
|
|||||||
}
|
}
|
||||||
appointment={editingAppointment}
|
appointment={editingAppointment}
|
||||||
patients={patients}
|
patients={patients}
|
||||||
|
onDelete={handleDeleteAppointment}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
isOpen={confirmDeleteState.open}
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
onCancel={() => setConfirmDeleteState({ open: false })}
|
||||||
|
entityName={confirmDeleteState.appointmentTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Claim Services Modal */}
|
{/* Claim Services Modal */}
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export default function SettingsPage() {
|
|||||||
isOpen={isDeleteStaffOpen}
|
isOpen={isDeleteStaffOpen}
|
||||||
onConfirm={handleConfirmDeleteStaff}
|
onConfirm={handleConfirmDeleteStaff}
|
||||||
onCancel={() => setIsDeleteStaffOpen(false)}
|
onCancel={() => setIsDeleteStaffOpen(false)}
|
||||||
patientName={currentStaff?.name}
|
entityName={currentStaff?.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user