Skip to content

Commit 2e9023f

Browse files
authored
Merge pull request #157 from HackYourFutureProjects/Fix/reject
Fix/reject
2 parents 01a4937 + 385212f commit 2e9023f

13 files changed

Lines changed: 243 additions & 2 deletions

File tree

client/src/components/appointmentCard/AppointmentCard.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AppointmentStatusBar } from "./AppointmentStatusBar";
33
import { AppointmentAvatar } from "./AppointmentAvatar";
44
import { AppointmentInfo } from "./AppointmentInfo";
55
import { AppointmentJoinButton } from "./AppointmentJoinButton";
6+
import { RejectionReasonDisplay } from "./RejectionReasonDisplay";
67
import {
78
getStatusStyles,
89
isInternalVideoCallLink,
@@ -69,6 +70,8 @@ export const AppointmentCard = ({
6970
onDelete={isPast ? onDelete : undefined}
7071
/>
7172
</div>
73+
74+
<RejectionReasonDisplay appointment={appointment} />
7275
</div>
7376
);
7477
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React, { useState } from "react";
2+
import { Appointment } from "../../types/appointments.types";
3+
import { useUpdateAppointmentMutation } from "../../features/appointments/mutations/useUpdateAppointmentMutation";
4+
5+
interface RejectAppointmentModalProps {
6+
appointment: Appointment;
7+
isOpen: boolean;
8+
onClose: () => void;
9+
}
10+
11+
export const RejectAppointmentModal = ({
12+
appointment,
13+
isOpen,
14+
onClose,
15+
}: RejectAppointmentModalProps) => {
16+
const [rejectionReason, setRejectionReason] = useState("");
17+
const [error, setError] = useState("");
18+
const updateAppointmentMutation = useUpdateAppointmentMutation();
19+
20+
const handleSubmit = async (e: React.FormEvent) => {
21+
e.preventDefault();
22+
23+
if (rejectionReason.trim().length < 100) {
24+
setError("Rejection reason must be at least 100 characters");
25+
return;
26+
}
27+
28+
if (rejectionReason.trim().length > 500) {
29+
setError("Rejection reason must not exceed 500 characters");
30+
return;
31+
}
32+
33+
try {
34+
await updateAppointmentMutation.mutateAsync({
35+
appointmentId: appointment.id,
36+
status: "rejected",
37+
rejectionReason: rejectionReason.trim(),
38+
});
39+
onClose();
40+
setRejectionReason("");
41+
setError("");
42+
} catch {
43+
setError("Failed to reject appointment. Please try again.");
44+
}
45+
};
46+
47+
const handleClose = () => {
48+
onClose();
49+
setRejectionReason("");
50+
setError("");
51+
};
52+
53+
if (!isOpen) return null;
54+
55+
return (
56+
<div className="fixed inset-0 backdrop-blur-sm bg-white/5 flex items-center justify-center z-50">
57+
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
58+
<h2 className="text-xl font-semibold mb-4">Reject Appointment</h2>
59+
60+
<div className="mb-4">
61+
<p className="text-sm text-gray-600 mb-2">
62+
<strong>Student:</strong> {appointment.studentName}
63+
</p>
64+
<p className="text-sm text-gray-600 mb-2">
65+
<strong>Lesson:</strong> {appointment.lesson}
66+
</p>
67+
<p className="text-sm text-gray-600 mb-4">
68+
<strong>Date & Time:</strong> {appointment.date} at{" "}
69+
{appointment.time}
70+
</p>
71+
</div>
72+
73+
<form onSubmit={handleSubmit}>
74+
<div className="mb-4">
75+
<label
76+
htmlFor="rejectionReason"
77+
className="block text-sm font-medium text-gray-700 mb-2"
78+
>
79+
Reason for Rejection *
80+
</label>
81+
<textarea
82+
id="rejectionReason"
83+
value={rejectionReason}
84+
onChange={(e) => setRejectionReason(e.target.value)}
85+
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
86+
rows={6}
87+
placeholder="Please explain why you are rejecting this appointment (100-500 characters)..."
88+
required
89+
/>
90+
<div className="flex justify-between text-xs text-gray-500 mt-1">
91+
<span>Minimum 100 characters</span>
92+
<span
93+
className={rejectionReason.length > 500 ? "text-red-500" : ""}
94+
>
95+
{rejectionReason.length}/500
96+
</span>
97+
</div>
98+
</div>
99+
100+
{error && (
101+
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
102+
<p className="text-sm text-red-700">{error}</p>
103+
</div>
104+
)}
105+
106+
<div className="flex gap-3">
107+
<button
108+
type="button"
109+
onClick={handleClose}
110+
className="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
111+
disabled={updateAppointmentMutation.isPending}
112+
>
113+
Cancel
114+
</button>
115+
<button
116+
type="submit"
117+
className="flex-1 px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
118+
disabled={
119+
updateAppointmentMutation.isPending ||
120+
rejectionReason.trim().length < 100
121+
}
122+
>
123+
{updateAppointmentMutation.isPending
124+
? "Rejecting..."
125+
: "Reject Appointment"}
126+
</button>
127+
</div>
128+
</form>
129+
</div>
130+
</div>
131+
);
132+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Appointment } from "../../types/appointments.types";
2+
3+
interface RejectionReasonDisplayProps {
4+
appointment: Appointment;
5+
}
6+
7+
export const RejectionReasonDisplay = ({
8+
appointment,
9+
}: RejectionReasonDisplayProps) => {
10+
if (appointment.status !== "rejected" || !appointment.rejectionReason) {
11+
return null;
12+
}
13+
14+
return (
15+
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
16+
<h4 className="text-sm font-medium text-red-800 mb-2">
17+
Reason for Rejection:
18+
</h4>
19+
<p className="text-sm text-red-700 leading-relaxed">
20+
{appointment.rejectionReason}
21+
</p>
22+
</div>
23+
);
24+
};

client/src/components/ui/statusButtons/StatusButtons.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export const StatusButtons = ({
2626
if (disabled) return;
2727
event.preventDefault();
2828
event.stopPropagation();
29-
setStatus(newStatus);
29+
30+
if (newStatus !== "rejected") {
31+
setStatus(newStatus);
32+
}
33+
3034
onStatusChange?.(newStatus);
3135
};
3236

client/src/features/appointments/mutations/useUpdateAppointmentMutation.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,23 @@ import { getErrorMessage } from "../../../util/ErrorUtil";
1111
interface UpdateAppointmentRequest {
1212
appointmentId: string;
1313
status: AppointmentStatus;
14+
rejectionReason?: string;
1415
}
1516

1617
const updateAppointmentStatus = async (
1718
data: UpdateAppointmentRequest,
1819
): Promise<Appointment> => {
20+
const requestBody: { status: AppointmentStatus; rejectionReason?: string } = {
21+
status: data.status,
22+
};
23+
24+
if (data.rejectionReason) {
25+
requestBody.rejectionReason = data.rejectionReason;
26+
}
27+
1928
const response = await apiProtected.put<Appointment>(
2029
`/api/appointments/${data.appointmentId}/status`,
21-
{ status: data.status },
30+
requestBody,
2231
);
2332
return response.data;
2433
};

client/src/pages/privetTeachersPages/teacherAppointments/TeacherAppointments.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useVideoCall } from "../../../features/appointments/hooks/useVideoCall"
1414
import { useAppointmentTime } from "../../../features/appointments/hooks/useAppointmentTime";
1515
import { TeacherAppointmentsList } from "../../../components/teacherAppointmentCard/TeacherAppointmentsList";
1616
import { RegularStudentScheduleModal } from "../../../components/regularStudentScheduleModal/RegularStudentScheduleModal";
17+
import { RejectAppointmentModal } from "../../../components/appointmentCard/RejectAppointmentModal";
1718
import { useSetRegularStudentMutation } from "../../../features/appointments/mutations/useSetRegularStudentMutation";
1819
import { useUpdateWeeklyScheduleMutation } from "../../../features/appointments/mutations/useUpdateWeeklyScheduleMutation";
1920
import { useRemoveRegularStudentMutation } from "../../../features/appointments/mutations/useRemoveRegularStudentMutation";
@@ -30,6 +31,9 @@ export const TeacherAppointments = () => {
3031
const [selectedStudent, setSelectedStudent] = useState<Appointment | null>(
3132
null,
3233
);
34+
const [isRejectModalOpen, setIsRejectModalOpen] = useState(false);
35+
const [appointmentToReject, setAppointmentToReject] =
36+
useState<Appointment | null>(null);
3337

3438
const { open: openModal } = useModalStore();
3539
const { confirmStartCall } = useVideoCall();
@@ -74,6 +78,15 @@ export const TeacherAppointments = () => {
7478
appointmentId: string,
7579
newStatus: AppointmentStatus,
7680
) => {
81+
if (newStatus === "rejected") {
82+
const appointment = appointments.find((apt) => apt.id === appointmentId);
83+
if (appointment) {
84+
setAppointmentToReject(appointment);
85+
setIsRejectModalOpen(true);
86+
}
87+
return;
88+
}
89+
7790
updateAppointmentMutation.mutate({
7891
appointmentId,
7992
status: newStatus,
@@ -241,6 +254,17 @@ export const TeacherAppointments = () => {
241254
.flatMap((apt) => apt.weeklySchedule || []) || []
242255
}
243256
/>
257+
258+
{appointmentToReject && (
259+
<RejectAppointmentModal
260+
appointment={appointmentToReject}
261+
isOpen={isRejectModalOpen}
262+
onClose={() => {
263+
setIsRejectModalOpen(false);
264+
setAppointmentToReject(null);
265+
}}
266+
/>
267+
)}
244268
</div>
245269
);
246270
};

client/src/types/appointments.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface Appointment {
1414
time: string;
1515
description?: string;
1616
status: AppointmentStatus;
17+
rejectionReason?: string;
1718
videoCall?: string;
1819
isRegularStudent?: boolean;
1920
addedToRegularAt?: string;

server/src/db/schemes/appointmentSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const AppointmentSchema = new mongoose.Schema<AppointmentTypeDB>(
2929
enum: ["pending", "approved", "rejected"],
3030
default: "pending",
3131
},
32+
rejectionReason: { type: String, default: null },
3233
videoCall: { type: String, default: null },
3334
isRegularStudent: { type: Boolean, default: false },
3435
weeklySchedule: [weeklyScheduleSlotSchema],

server/src/db/schemes/types/appointment.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface AppointmentTypeDB {
1919
time: string;
2020
description?: string;
2121
status: "pending" | "approved" | "rejected";
22+
rejectionReason?: string;
2223
videoCall?: string;
2324
isRegularStudent?: boolean;
2425
weeklySchedule?: WeeklyScheduleSlot[];

server/src/services/appointment/appointment.service.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ export class AppointmentService {
111111
}
112112

113113
async updateAppointmentStatus(id: string, data: UpdateAppointmentStatusType) {
114+
if (data.status === "rejected") {
115+
if (!data.rejectionReason) {
116+
throw new Error(
117+
"Rejection reason is required when rejecting an appointment",
118+
);
119+
}
120+
if (
121+
data.rejectionReason.length < 100 ||
122+
data.rejectionReason.length > 500
123+
) {
124+
throw new Error(
125+
"Rejection reason must be between 100 and 500 characters",
126+
);
127+
}
128+
}
129+
114130
const updateData = {
115131
...data,
116132
updatedAt: new Date(),
@@ -258,6 +274,7 @@ export class AppointmentService {
258274
time: string;
259275
description?: string;
260276
status: string;
277+
rejectionReason?: string;
261278
videoCall?: string;
262279
isRegularStudent?: boolean;
263280
weeklySchedule?: { day: string; hour: number }[];
@@ -281,6 +298,7 @@ export class AppointmentService {
281298
time: apt.time,
282299
description: apt.description,
283300
status: apt.status,
301+
rejectionReason: apt.rejectionReason,
284302
videoCall: apt.videoCall,
285303
isRegularStudent: apt.isRegularStudent,
286304
weeklySchedule: apt.weeklySchedule,

0 commit comments

Comments
 (0)