Skip to content

Commit 766bf92

Browse files
committed
appointment cancelling & rescheduling
1 parent 0a6640d commit 766bf92

13 files changed

Lines changed: 221 additions & 46 deletions

File tree

frontend/src/features/appointments/api/appointments.api.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@ export async function getAvailableSlots(
1111

1212
return data;
1313
}
14-
1514
export async function bookAppointment(payload: {
1615
doctorId: string;
1716
appointmentTime: string;
18-
appointmentDate: string;
1917
durationMinutes?: number;
2018
}) {
2119
const { data } = await api.post('/appointments', payload);
2220
return data;
2321
}
24-
2522
export async function getDoctorsByDepartment(
2623
departmentId: string
2724
): Promise<Doctor[]> {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { api } from '@/src/lib/api';
2+
3+
export async function rescheduleAppointment(
4+
appointmentId: string,
5+
newTime: string
6+
) {
7+
const { data } = await api.post(`/appointments/reschedule/${appointmentId}`, {
8+
newTime,
9+
});
10+
11+
return data;
12+
}
Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
'use client';
22

3+
import { useState } from 'react';
34
import { useCancelAppointment } from '../hooks/useCancelAppointment';
5+
import RescheduleModal from './RescheduleModal';
46

57
interface Props {
68
appointment: any;
79
}
810

911
export default function AppointmentCard({ appointment }: Props) {
1012
const cancelMutation = useCancelAppointment();
13+
const [showReschedule, setShowReschedule] = useState(false);
1114

12-
const isPast =
13-
new Date(appointment.appointment_time) < new Date();
15+
const appointmentDate = new Date(appointment.appointment_time);
16+
const now = new Date();
17+
18+
const isPast = appointmentDate < now;
19+
20+
const diffMinutes =
21+
(appointmentDate.getTime() - now.getTime()) / 60000;
22+
23+
const canReschedule =
24+
appointment.status === 'SCHEDULED' &&
25+
diffMinutes >= 60;
1426

1527
return (
1628
<div className="bg-white p-4 rounded-lg shadow space-y-2">
@@ -20,36 +32,53 @@ export default function AppointmentCard({ appointment }: Props) {
2032

2133
<p>
2234
<strong>Date:</strong>{' '}
23-
{new Date(
24-
appointment.appointment_time
25-
).toLocaleString()}
35+
{appointmentDate.toLocaleString()}
2636
</p>
2737

2838
<p>
2939
<strong>Status:</strong> {appointment.status}
3040
</p>
3141

32-
{/* Cancel button */}
33-
{appointment.status !== 'CANCELLED' && !isPast && (
42+
{/* Cancel */}
43+
{appointment.status === 'SCHEDULED' && !isPast && (
3444
<button
3545
onClick={() =>
3646
cancelMutation.mutate(appointment.id)
3747
}
3848
disabled={cancelMutation.isPending}
39-
className="mt-2 bg-red-500 hover:bg-red-600 text-white px-3 py-1 cursor-pointer rounded text-sm transition disabled:opacity-50"
49+
className="mt-2 bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm transition disabled:opacity-50"
4050
>
4151
{cancelMutation.isPending
4252
? 'Cancelling...'
4353
: 'Cancel'}
4454
</button>
4555
)}
4656

47-
{/* Show cancelled label */}
57+
{/* Reschedule */}
58+
{canReschedule && (
59+
<button
60+
onClick={() => setShowReschedule(true)}
61+
className="mt-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-sm transition"
62+
>
63+
Reschedule
64+
</button>
65+
)}
66+
67+
{/* Cancelled label */}
4868
{appointment.status === 'CANCELLED' && (
4969
<span className="text-red-500 font-medium">
5070
Cancelled
5171
</span>
5272
)}
73+
74+
{/* Modal */}
75+
{showReschedule && (
76+
<RescheduleModal
77+
appointmentId={appointment.id}
78+
doctorId={appointment.doctor_id}
79+
onClose={() => setShowReschedule(false)}
80+
/>
81+
)}
5382
</div>
5483
);
5584
}

frontend/src/features/appointments/components/BookingForm.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ export default function BookingForm() {
2020

2121
const mutation = useBookAppointment();
2222

23-
function handleBook() {
24-
if (!selectedSlot || !doctorId || !date) return;
25-
26-
mutation.mutate({
27-
doctorId,
28-
appointmentDate: date,
29-
appointmentTime: selectedSlot,
30-
durationMinutes: 15,
31-
});
32-
}
23+
function handleBook() {
24+
if (!selectedSlot || !doctorId || !date) return;
25+
26+
// Combine date + time into ISO timestamp
27+
const fullDateTime = `${date}T${selectedSlot}:00`;
28+
29+
mutation.mutate({
30+
doctorId,
31+
appointmentTime: fullDateTime,
32+
durationMinutes: 15,
33+
});
34+
}
3335

3436
return (
3537
<div className="space-y-6 max-w-xl bg-white p-6 rounded-xl shadow-md">
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { useAvailableSlots } from '../hooks/useAvailableSlots';
5+
import { useRescheduleAppointment } from '../hooks/useRescheduleAppointment';
6+
import SlotSelector from './SlotSelector';
7+
8+
type Props = {
9+
appointmentId: string;
10+
doctorId: string;
11+
onClose: () => void;
12+
};
13+
14+
export default function RescheduleModal({
15+
appointmentId,
16+
doctorId,
17+
onClose,
18+
}: Props) {
19+
const [date, setDate] = useState('');
20+
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
21+
22+
const { data: slots = [] } = useAvailableSlots(doctorId, date);
23+
const mutation = useRescheduleAppointment();
24+
25+
function handleReschedule() {
26+
if (!selectedSlot || !date) return;
27+
28+
const fullDateTime = `${date}T${selectedSlot}:00`;
29+
30+
mutation.mutate(
31+
{
32+
appointmentId,
33+
newTime: fullDateTime,
34+
},
35+
{
36+
onSuccess: () => {
37+
onClose();
38+
},
39+
}
40+
);
41+
}
42+
43+
return (
44+
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
45+
<div className="bg-white p-6 rounded-xl w-96 space-y-4">
46+
<h2 className="text-lg font-semibold">Reschedule Appointment</h2>
47+
48+
<input
49+
type="date"
50+
className="border p-2 w-full rounded-lg"
51+
value={date}
52+
onChange={(e) => {
53+
setDate(e.target.value);
54+
setSelectedSlot(null);
55+
}}
56+
min={new Date().toISOString().split('T')[0]}
57+
/>
58+
59+
{slots.length > 0 && (
60+
<SlotSelector
61+
slots={slots}
62+
selected={selectedSlot}
63+
onSelect={setSelectedSlot}
64+
/>
65+
)}
66+
67+
<button
68+
onClick={handleReschedule}
69+
disabled={!selectedSlot || mutation.isPending}
70+
className="bg-blue-600 text-white w-full p-2 rounded-lg"
71+
>
72+
{mutation.isPending ? 'Rescheduling...' : 'Confirm Reschedule'}
73+
</button>
74+
75+
{mutation.isError && (
76+
<p className="text-red-500 text-sm">
77+
{(mutation.error as any)?.response?.data?.error}
78+
</p>
79+
)}
80+
81+
<button
82+
onClick={onClose}
83+
className="text-gray-500 text-sm w-full"
84+
>
85+
Cancel
86+
</button>
87+
</div>
88+
</div>
89+
);
90+
}

frontend/src/features/appointments/hooks/useAvailableSlots.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ export function useAvailableSlots(doctorId: string, date: string) {
88
return useQuery({
99
queryKey: ['available-slots', doctorId, date],
1010
queryFn: () => getAvailableSlots(doctorId, date),
11-
enabled:
12-
!!doctorId &&
13-
!!date &&
14-
!!auth.accessToken && // ✅ force boolean
15-
!auth.isRestoring,
11+
enabled: !!doctorId && !!date && !!auth.accessToken && !auth.isRestoring,
12+
refetchOnWindowFocus: true,
13+
refetchInterval: 15000, // every 15 seconds (optional)
1614
});
1715
}

frontend/src/features/appointments/hooks/useCancelAppointment.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ export function useCancelAppointment() {
66

77
return useMutation({
88
mutationFn: cancelAppointment,
9-
onSuccess: () => {
9+
onSuccess: (data) => {
10+
// Assuming backend returns doctor_id + appointment_time
11+
const doctorId = data.doctor_id;
12+
const date = data.appointment_time.split('T')[0];
13+
14+
queryClient.invalidateQueries({
15+
queryKey: ['available-slots', doctorId, date],
16+
});
17+
1018
queryClient.invalidateQueries({
1119
queryKey: ['history'],
1220
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
import { rescheduleAppointment } from '../api/rescheduleAppointment';
3+
4+
export function useRescheduleAppointment() {
5+
const queryClient = useQueryClient();
6+
7+
return useMutation({
8+
mutationFn: ({
9+
appointmentId,
10+
newTime,
11+
}: {
12+
appointmentId: string;
13+
newTime: string;
14+
}) => rescheduleAppointment(appointmentId, newTime),
15+
16+
onSuccess: () => {
17+
queryClient.invalidateQueries({ queryKey: ['history'] });
18+
queryClient.invalidateQueries({ queryKey: ['available-slots'] });
19+
queryClient.invalidateQueries({ queryKey: ['my-status'] });
20+
},
21+
});
22+
}

services/patient-service/src/app.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import patientRoutes from './modules/patients/patient.routes';
55
import healthRoutes from './routes/health';
66
import authRoutes from './modules/auth/auth.routes';
77
import { errorMiddleware } from './middlewares/error.middleware';
8-
import appointmentRoutes from '../src/modules/appointments/appointment.routes';
9-
8+
import appointmentRoutes from './modules/appointments/appointment.routes';
109
const app = express();
1110

1211
// Middleware
@@ -22,6 +21,10 @@ app.use((req, res, next) => {
2221
next();
2322
});
2423

24+
app.use((req, res, next) => {
25+
console.log('ROUTE HIT:', req.method, req.originalUrl);
26+
next();
27+
});
2528
// 🔓 Public routes (auth)
2629
app.use('/auth', authRoutes);
2730

services/patient-service/src/modules/appointments/appointment.repository.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ export class AppointmentRepository {
99
durationMinutes: number;
1010
priority: 'NORMAL' | 'HIGH';
1111
}) {
12-
const result = await pool.query(
13-
`
12+
try {
13+
const result = await pool.query(
14+
`
1415
INSERT INTO appointments (
1516
id,
1617
doctor_id,
@@ -21,21 +22,27 @@ export class AppointmentRepository {
2122
status
2223
)
2324
VALUES ($1,$2,$3,$4,$5,$6,'SCHEDULED')
24-
ON CONFLICT (doctor_id, appointment_time)
25-
DO NOTHING
2625
RETURNING *;
2726
`,
28-
[
29-
randomUUID(),
30-
data.doctorId,
31-
data.patientId,
32-
data.appointmentTime,
33-
data.durationMinutes,
34-
data.priority,
35-
]
36-
);
27+
[
28+
randomUUID(),
29+
data.doctorId,
30+
data.patientId,
31+
data.appointmentTime,
32+
data.durationMinutes,
33+
data.priority,
34+
]
35+
);
3736

38-
return result.rows[0] || null;
37+
return result.rows[0];
38+
} catch (err: any) {
39+
if (err.code === '23505') {
40+
// unique violation
41+
return null;
42+
}
43+
44+
throw err;
45+
}
3946
}
4047

4148
async getDoctorAppointmentsForDay(doctorId: string, date: string) {
@@ -132,7 +139,12 @@ export class AppointmentRepository {
132139
[doctorId, date]
133140
);
134141

135-
return result.rows.map((r) => new Date(r.appointment_time).toISOString());
142+
return result.rows.map((r) => {
143+
const d = new Date(r.appointment_time);
144+
const hours = d.getHours().toString().padStart(2, '0');
145+
const minutes = d.getMinutes().toString().padStart(2, '0');
146+
return `${hours}:${minutes}`;
147+
});
136148
}
137149
}
138150

0 commit comments

Comments
 (0)