From 2496756b8206370feeb9224acd234496dfa9604c Mon Sep 17 00:00:00 2001 From: Dasha Date: Sun, 8 Feb 2026 18:51:04 +0100 Subject: [PATCH] add appointment system --- client/src/components/table/LessonRow.tsx | 16 +- client/src/components/table/LessonsTable.tsx | 9 +- .../teacherSchedule/TeacherSchedule.tsx | 75 ++++++++- .../teacherSchedule/Time/Time.tsx | 7 +- client/src/components/ui/modal/Modal.tsx | 43 +++++ .../mutations/useCreateAppointmentMutation.ts | 43 +++++ .../mutations/useUpdateAppointmentMutation.ts | 45 ++++++ .../query/useTeacherAppointmentsQuery.ts | 82 ++++++++++ client/src/features/queryKeys.ts | 2 + .../teachers/query/useTeacherQuery.ts | 77 +++++++++ .../TeacherAppointments.tsx | 149 ++++++++---------- .../src/pages/teacherDetail/teacherDetail.tsx | 51 ++++-- client/src/types/appointments.types.ts | 13 ++ client/src/types/teacher.types.ts | 4 + 14 files changed, 505 insertions(+), 111 deletions(-) create mode 100644 client/src/components/ui/modal/Modal.tsx create mode 100644 client/src/features/appointments/mutations/useCreateAppointmentMutation.ts create mode 100644 client/src/features/appointments/mutations/useUpdateAppointmentMutation.ts create mode 100644 client/src/features/appointments/query/useTeacherAppointmentsQuery.ts create mode 100644 client/src/features/teachers/query/useTeacherQuery.ts create mode 100644 client/src/types/appointments.types.ts diff --git a/client/src/components/table/LessonRow.tsx b/client/src/components/table/LessonRow.tsx index 4200c3e..0af4d8c 100644 --- a/client/src/components/table/LessonRow.tsx +++ b/client/src/components/table/LessonRow.tsx @@ -4,12 +4,21 @@ import KebabVerticalIcon from "../icons/KebabVertical"; import UncheckedIcon from "../icons/Uncheked"; import Check from "../icons/Check"; import { StatusButtons } from "../ui/statusButtons/StatusButtons"; +import { AppointmentStatus } from "../../types/appointments.types"; export type LessonRowData = { id?: string | number; - checked: boolean; // checkbox state + checked: boolean; linkText?: string; - [key: string]: ReactNode; + lesson?: string; + student?: string; + price?: string; + date?: string; + time?: string; + status?: AppointmentStatus; + onStatusChange?: (status: AppointmentStatus) => void; +} & { + [key: string]: ReactNode | string | number | boolean | undefined; }; type LessonRowProps = { @@ -74,9 +83,10 @@ const LessonRow = ({ | "approved" | "rejected" } + onStatusChange={data.onStatusChange} /> ) : ( - data[column.key] + (data[column.key] as ReactNode) )} ))} diff --git a/client/src/components/table/LessonsTable.tsx b/client/src/components/table/LessonsTable.tsx index 3d0039e..2f13729 100644 --- a/client/src/components/table/LessonsTable.tsx +++ b/client/src/components/table/LessonsTable.tsx @@ -39,10 +39,11 @@ const LessonsTable = ({ const [rows, setRows] = useState(sourceRows); const handleToggleRow = (rowIndex: number) => { - setRows((prev) => - prev.map((row, index) => - index === rowIndex ? { ...row, checked: !row.checked } : row, - ), + setRows( + (prev) => + prev.map((row, index) => + index === rowIndex ? { ...row, checked: !row.checked } : row, + ) as LessonRowData[], ); }; diff --git a/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx b/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx index bec54ad..1b3dbb0 100644 --- a/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx +++ b/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx @@ -2,16 +2,48 @@ import { useState } from "react"; import { Calendar } from "./Calendar/Calendar"; import { Time } from "./Time/Time"; import { Button } from "../../ui/button/Button"; +import { Modal } from "../../ui/modal/Modal"; +import { TeacherType } from "../../../types/teacher.types"; -export default function TeacherSchedule() { +interface TeacherScheduleProps { + teacher?: TeacherType; +} + +export default function TeacherSchedule({ teacher }: TeacherScheduleProps) { const [selectedDate, setSelectedDate] = useState(null); + const [selectedTime, setSelectedTime] = useState(null); const [showTimeAndBook, setShowTimeAndBook] = useState(false); + const [showConfirmModal, setShowConfirmModal] = useState(false); const handleDateSelection = (date: Date): void => { setSelectedDate(date); setShowTimeAndBook(true); }; + const handleTimeSelection = (time: string): void => { + setSelectedTime(time); + }; + + const handleBook = (): void => { + if (selectedDate && selectedTime && teacher) { + setShowConfirmModal(true); + } + }; + + const handleConfirmBooking = (): void => { + setShowConfirmModal(false); + setSelectedDate(null); + setSelectedTime(null); + setShowTimeAndBook(false); + }; + + const getAvailableTimeSlots = (): string[] => { + if (!teacher || !selectedDate) return []; + + const dateStr = selectedDate.toISOString().split("T")[0]; + return teacher.schedule?.[dateStr] || teacher.availableTimeSlots || []; + }; + return (
@@ -28,18 +60,53 @@ export default function TeacherSchedule() { {showTimeAndBook && (
-
)} - {showTimeAndBook && ( + {showTimeAndBook && selectedTime && (
- +
)}
+ + setShowConfirmModal(false)} + title="Confirm Booking" + onConfirm={handleConfirmBooking} + confirmText="Book Now" + cancelText="Cancel" + > +
+

+ Teacher: {teacher?.name} +

+

+ Subject: {teacher?.subject} +

+

+ Date: {selectedDate?.toLocaleDateString()} +

+

+ Time: {selectedTime} +

+

+ Price: ${teacher?.price} +

+

+ The lesson request will be sent to the teacher. +

+
+
); } diff --git a/client/src/components/teacherSection/teacherSchedule/Time/Time.tsx b/client/src/components/teacherSection/teacherSchedule/Time/Time.tsx index 9330525..7132866 100644 --- a/client/src/components/teacherSection/teacherSchedule/Time/Time.tsx +++ b/client/src/components/teacherSection/teacherSchedule/Time/Time.tsx @@ -2,12 +2,13 @@ import { useState } from "react"; interface TimeProps { onTimeSelect: (time: string) => void; + availableSlots?: string[]; } -export function Time({ onTimeSelect }: TimeProps) { +export function Time({ onTimeSelect, availableSlots }: TimeProps) { const [selectedTime, setSelectedTime] = useState(null); - const timeSlots = [ + const defaultTimeSlots = [ "7:00", "8:00", "9:00", @@ -25,6 +26,8 @@ export function Time({ onTimeSelect }: TimeProps) { "21:00", ]; + const timeSlots = availableSlots || defaultTimeSlots; + const handleTimeClick = (time: string): void => { setSelectedTime(time); onTimeSelect(time); diff --git a/client/src/components/ui/modal/Modal.tsx b/client/src/components/ui/modal/Modal.tsx new file mode 100644 index 0000000..6399659 --- /dev/null +++ b/client/src/components/ui/modal/Modal.tsx @@ -0,0 +1,43 @@ +import { ReactNode } from "react"; +import { Button } from "../button/Button"; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: ReactNode; + onConfirm?: () => void; + confirmText?: string; + cancelText?: string; +} + +export const Modal = ({ + isOpen, + onClose, + title, + children, + onConfirm, + confirmText = "Confirm", + cancelText = "Cancel", +}: ModalProps) => { + if (!isOpen) return null; + + return ( +
+
+ +
+

{title}

+ +
{children}
+ +
+ + {onConfirm && } +
+
+
+ ); +}; diff --git a/client/src/features/appointments/mutations/useCreateAppointmentMutation.ts b/client/src/features/appointments/mutations/useCreateAppointmentMutation.ts new file mode 100644 index 0000000..b616fec --- /dev/null +++ b/client/src/features/appointments/mutations/useCreateAppointmentMutation.ts @@ -0,0 +1,43 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "../../queryKeys"; +import { Appointment } from "../../../types/appointments.types"; + +interface CreateAppointmentRequest { + teacherId: string; + studentId: string; + date: string; + time: string; + lesson: string; + price: string; +} + +const createAppointment = async ( + data: CreateAppointmentRequest, +): Promise => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + id: Date.now().toString(), + lesson: data.lesson, + teacher: data.teacherId, + student: data.studentId, + price: data.price, + date: data.date, + time: data.time, + status: "pending", + videoCall: `https://meet.google.com/${data.teacherId}-${data.studentId}-${Date.now()}`, + }; +}; + +export const useCreateAppointmentMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createAppointment, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.appointments, + }); + }, + }); +}; diff --git a/client/src/features/appointments/mutations/useUpdateAppointmentMutation.ts b/client/src/features/appointments/mutations/useUpdateAppointmentMutation.ts new file mode 100644 index 0000000..a16747a --- /dev/null +++ b/client/src/features/appointments/mutations/useUpdateAppointmentMutation.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "../../queryKeys"; +import { + Appointment, + AppointmentStatus, +} from "../../../types/appointments.types"; + +interface UpdateAppointmentRequest { + appointmentId: string; + status: AppointmentStatus; +} + +const updateAppointmentStatus = async ( + data: UpdateAppointmentRequest, +): Promise => { + await new Promise((resolve) => setTimeout(resolve, 500)); + + return { + id: data.appointmentId, + lesson: "English", + teacher: "1", + student: "Anna Tkachuk", + price: "25 euro", + date: "5/27/15", + time: "2:00 PM", + status: data.status, + videoCall: `https://meet.google.com/1-anna-${data.appointmentId}`, + }; +}; + +export const useUpdateAppointmentMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: updateAppointmentStatus, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.appointments, + }); + }, + onError: (error) => { + console.error("Failed to update appointment status:", error); + }, + }); +}; diff --git a/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts b/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts new file mode 100644 index 0000000..61c80b2 --- /dev/null +++ b/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts @@ -0,0 +1,82 @@ +import { useQuery } from "@tanstack/react-query"; +import { queryKeys } from "../../queryKeys"; +import { Appointment } from "../../../types/appointments.types"; +import { useAuthSessionStore } from "../../../store/authSession.store"; + +const mockTeacherAppointments: Appointment[] = [ + { + id: "1", + lesson: "English", + teacher: "1", + student: "Anna Tkachuk", + price: "25 euro", + date: "5/27/15", + time: "2:00 PM", + status: "pending", + videoCall: "https://meet.google.com", + }, + { + id: "2", + lesson: "Business English", + teacher: "1", + student: "Ola Tkachuk", + price: "30 euro", + date: "5/28/15", + time: "10:00 AM", + status: "pending", + videoCall: "https://meet.google.com", + }, + { + id: "3", + lesson: "English", + teacher: "1", + student: "Alaa Tkachuk", + price: "25 euro", + date: "5/29/15", + time: "3:00 PM", + status: "pending", + videoCall: "https://meet.google.com", + }, + { + id: "4", + lesson: "English", + teacher: "1", + student: "Daria Tkachuk", + price: "25 euro", + date: "5/30/15", + time: "4:00 PM", + status: "pending", + videoCall: "https://meet.google.com", + }, + { + id: "5", + lesson: "English", + teacher: "1", + student: "Daryna Tkachuk", + price: "25 euro", + date: "5/31/15", + time: "11:00 AM", + status: "pending", + videoCall: "https://meet.google.com", + }, +]; + +const fetchTeacherAppointments = async ( + teacherId: string, +): Promise => { + await new Promise((resolve) => setTimeout(resolve, 500)); + + return mockTeacherAppointments.filter( + (appointment) => appointment.teacher === teacherId, + ); +}; + +export const useTeacherAppointmentsQuery = () => { + const user = useAuthSessionStore((state) => state.user); + + return useQuery({ + queryKey: queryKeys.appointments, + queryFn: () => fetchTeacherAppointments(user?.id || ""), + enabled: !!user?.id, + }); +}; diff --git a/client/src/features/queryKeys.ts b/client/src/features/queryKeys.ts index fd46435..6acd689 100644 --- a/client/src/features/queryKeys.ts +++ b/client/src/features/queryKeys.ts @@ -3,5 +3,7 @@ export const queryKeys = { students: ["students"] as const, teachers: ["teachers"] as const, + teacher: (id: string) => ["teachers", id] as const, + appointments: ["appointments"] as const, // teachersList: (params) => ['teachers', params] as const, }; diff --git a/client/src/features/teachers/query/useTeacherQuery.ts b/client/src/features/teachers/query/useTeacherQuery.ts new file mode 100644 index 0000000..c4deb35 --- /dev/null +++ b/client/src/features/teachers/query/useTeacherQuery.ts @@ -0,0 +1,77 @@ +import { useQuery } from "@tanstack/react-query"; +import { queryKeys } from "../../queryKeys"; +import { TeacherType } from "../../../types/teacher.types"; + +const mockTeachers: TeacherType[] = [ + { + id: "1", + name: "Els Menson", + subject: "English", + image: "/src/assets/images/person.jpg", + experience: "12 years", + education: "University of Amsterdam", + price: 35, + approaching: + "Communicative, student-centered methodology focused on real-life English. Lessons emphasize speaking practice, practical vocabulary, and personalized learning goals.", + availableTimeSlots: ["9:00", "10:00", "11:00", "14:00", "15:00", "16:00"], + schedule: { + "2024-01-15": ["9:00", "10:00", "11:00", "14:00", "15:00", "16:00"], + "2024-01-16": ["10:00", "11:00", "14:00", "15:00"], + "2024-01-17": ["9:00", "10:00", "14:00", "15:00", "16:00"], + }, + }, + { + id: "2", + name: "John Smith", + subject: "Business English", + image: "/src/assets/images/person.jpg", + experience: "8 years", + education: "Harvard Business School", + price: 45, + approaching: + "Business-focused English training with emphasis on professional communication, presentations, and corporate vocabulary.", + availableTimeSlots: ["8:00", "9:00", "13:00", "14:00", "17:00", "18:00"], + schedule: { + "2024-01-15": ["8:00", "9:00", "13:00", "14:00"], + "2024-01-16": ["9:00", "13:00", "14:00", "17:00", "18:00"], + "2024-01-17": ["8:00", "9:00", "13:00", "14:00", "17:00"], + }, + }, + { + id: "3", + name: "Anna Tkachuk", + subject: "English", + image: "/src/assets/images/person.jpg", + experience: "10 years", + education: "Kyiv National University", + price: 30, + approaching: + "Interactive and engaging teaching style with focus on grammar, pronunciation, and conversational skills.", + availableTimeSlots: ["9:00", "10:00", "11:00", "14:00", "15:00", "16:00"], + schedule: { + "2024-01-15": ["9:00", "10:00", "11:00", "14:00", "15:00", "16:00"], + "2024-01-16": ["10:00", "11:00", "14:00", "15:00"], + "2024-01-17": ["9:00", "10:00", "14:00", "15:00", "16:00"], + }, + }, +]; + +const fetchTeacher = async (teacherId: string): Promise => { + await new Promise((resolve) => setTimeout(resolve, 500)); + + const teacher = mockTeachers.find((teacher) => teacher.id === teacherId); + + if (!teacher) { + throw new Error(`Teacher with id ${teacherId} not found`); + } + + return teacher; +}; + +export const useTeacherQuery = (teacherId: string) => { + return useQuery({ + queryKey: queryKeys.teacher(teacherId), + queryFn: () => fetchTeacher(teacherId), + enabled: !!teacherId, + }); +}; diff --git a/client/src/pages/teacherAppointments/TeacherAppointments.tsx b/client/src/pages/teacherAppointments/TeacherAppointments.tsx index 6016fe6..4dbcc7a 100644 --- a/client/src/pages/teacherAppointments/TeacherAppointments.tsx +++ b/client/src/pages/teacherAppointments/TeacherAppointments.tsx @@ -7,10 +7,21 @@ import { TopBar } from "../../components/headerPrivate/TopBar"; import { PageTitle } from "../../components/pageTitle/PageTitle"; import LessonsTable from "../../components/table/LessonsTable"; import { Pagination } from "../../components/ui/pagination/Pagination"; +import { useTeacherAppointmentsQuery } from "../../features/appointments/query/useTeacherAppointmentsQuery"; +import { useUpdateAppointmentMutation } from "../../features/appointments/mutations/useUpdateAppointmentMutation"; +import { AppointmentStatus } from "../../types/appointments.types"; +import { LessonRowData } from "../../components/table/LessonRow"; export const TeacherAppointments = () => { const [page, setPage] = useState(1); + const { + data: appointments = [], + isLoading, + error, + } = useTeacherAppointmentsQuery(); + const updateAppointmentMutation = useUpdateAppointmentMutation(); + const columns = [ { key: "lesson", label: "Lessons", width: "130px" }, { key: "student", label: "Students", width: "184px" }, @@ -20,6 +31,61 @@ export const TeacherAppointments = () => { { key: "status", label: "Status", width: "200px" }, ]; + const handleStatusChange = ( + appointmentId: string, + newStatus: AppointmentStatus, + ) => { + updateAppointmentMutation.mutate({ + appointmentId, + status: newStatus, + }); + }; + + const tableRows = appointments.map((appointment) => ({ + id: appointment.id, + checked: false, + lesson: appointment.lesson, + student: appointment.student, + price: appointment.price, + date: appointment.date, + time: appointment.time, + status: appointment.status, + onStatusChange: (newStatus: AppointmentStatus) => + handleStatusChange(appointment.id, newStatus), + })) as LessonRowData[]; + + if (isLoading) { + return ( +
+ +
+ +
+
+ Loading appointments... +
+
+
+
+ ); + } + + if (error) { + return ( +
+ +
+ +
+
+ Error loading appointments +
+
+
+
+ ); + } + return (
@@ -38,88 +104,7 @@ export const TeacherAppointments = () => { rowHeight={66} columns={columns} useStatusButtons={true} - rows={[ - { - id: 1, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 2, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 3, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 4, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 5, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 6, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 7, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - { - id: 8, - checked: false, - lesson: "English", - student: "Anna Tkachuk", - price: "25 euro", - date: "5/27/15", - time: "2:00 PM", - status: "pending", - }, - ]} + rows={tableRows} />
diff --git a/client/src/pages/teacherDetail/teacherDetail.tsx b/client/src/pages/teacherDetail/teacherDetail.tsx index a8eeac9..da9ccd4 100644 --- a/client/src/pages/teacherDetail/teacherDetail.tsx +++ b/client/src/pages/teacherDetail/teacherDetail.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { Button } from "../../components/ui/button/Button"; import { TeacherCard } from "../../components/teacherCard/teacherCard"; import { TeacherNavigation } from "../../components/teacherSection/teacherNavigation/TeacherNavigation"; @@ -6,28 +6,18 @@ import TeacherSubjects from "../../components/teacherSection/teacherSubjects/tea import TeacherAbout from "../../components/teacherSection/teacherAbout/teacherAbout"; import TeacherSchedule from "../../components/teacherSection/teacherSchedule/TeacherSchedule"; import { useState } from "react"; -import Person from "../../assets/images/person.jpg"; -import { TeacherType } from "../../types/teacher.types"; import { ReviewsTeacher } from "../../components/teacherSection/Reviews/ReviewsTeacher"; - -const teacher: TeacherType = { - id: "1", - name: "Els Menson", - subject: "English", - price: 35, - image: Person, - experience: "12 years", - education: "University of Amsterdam", - approaching: - "Communicative, student-centered methodology focused on real-life English. Lessons emphasize speaking practice, practical vocabulary, and personalized learning goals.", -}; +import { useTeacherQuery } from "../../features/teachers/query/useTeacherQuery"; type TabType = "about" | "subjects" | "schedule"; export const TeacherDetail = () => { const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); const [activeTab, setActiveTab] = useState("subjects"); + const { data: teacher, isLoading, error } = useTeacherQuery(id || ""); + const handleBack = () => { navigate(-1); }; @@ -39,12 +29,41 @@ export const TeacherDetail = () => { case "about": return ; case "schedule": - return ; + return ; default: return null; } }; + if (isLoading) { + return ( +
+
+
+
Loading teacher...
+
+
+
+ ); + } + + if (error || !teacher) { + return ( +
+
+
+ +
+
+
Teacher not found
+
+
+
+ ); + } + return (
diff --git a/client/src/types/appointments.types.ts b/client/src/types/appointments.types.ts new file mode 100644 index 0000000..819db6e --- /dev/null +++ b/client/src/types/appointments.types.ts @@ -0,0 +1,13 @@ +export type AppointmentStatus = "pending" | "approved" | "rejected"; + +export interface Appointment { + id: string; + lesson: string; + teacher?: string; + student?: string; + price: string; + date: string; + time: string; + status: AppointmentStatus; + videoCall?: string; +} diff --git a/client/src/types/teacher.types.ts b/client/src/types/teacher.types.ts index 9b21fbe..a9c08d1 100644 --- a/client/src/types/teacher.types.ts +++ b/client/src/types/teacher.types.ts @@ -7,4 +7,8 @@ export type TeacherType = { education: string; price: number; approaching: string; + availableTimeSlots?: string[]; + schedule?: { + [date: string]: string[]; + }; };