({
label: `${i + 7}:00`,
}));
-export const RegularStudentScheduleModal = ({
- isOpen,
+const ModalContent = ({
onClose,
onSave,
studentName,
initialSchedule = [],
occupiedSlots = [],
-}: RegularStudentScheduleModalProps) => {
+}: Omit) => {
+ const initialSavedSlots = useMemo(() => initialSchedule, [initialSchedule]);
const [savedSlots, setSavedSlots] =
- useState(initialSchedule);
+ useState(initialSavedSlots);
const [currentDay, setCurrentDay] = useState("Monday");
const [currentHour, setCurrentHour] = useState(7);
- if (!isOpen) return null;
-
const isDuplicate = savedSlots.some(
(slot) => slot.day === currentDay && slot.hour === currentHour,
);
@@ -59,8 +57,20 @@ export const RegularStudentScheduleModal = ({
const newSlot = { day: currentDay, hour: currentHour };
const updatedSlots = [...savedSlots, newSlot];
setSavedSlots(updatedSlots);
- setCurrentDay("Monday");
- setCurrentHour(7);
+
+ const nextHour = currentHour + 1;
+ if (nextHour <= 23) {
+ setCurrentHour(nextHour);
+ } else {
+ const currentDayIndex = DAYS.findIndex((d) => d.value === currentDay);
+ if (currentDayIndex < DAYS.length - 1) {
+ setCurrentDay(DAYS[currentDayIndex + 1].value);
+ setCurrentHour(7);
+ } else {
+ setCurrentDay("Monday");
+ setCurrentHour(7);
+ }
+ }
};
const handleRemoveSlot = (index: number) => {
@@ -73,9 +83,6 @@ export const RegularStudentScheduleModal = ({
};
const handleCancel = () => {
- setSavedSlots(initialSchedule);
- setCurrentDay("Monday");
- setCurrentHour(7);
onClose();
};
@@ -204,3 +211,12 @@ export const RegularStudentScheduleModal = ({
);
};
+
+export const RegularStudentScheduleModal = ({
+ isOpen,
+ ...props
+}: RegularStudentScheduleModalProps) => {
+ if (!isOpen) return null;
+
+ return
;
+};
diff --git a/client/src/components/sidebar/Sidebar.tsx b/client/src/components/sidebar/Sidebar.tsx
index 7f290858..67d41c4f 100644
--- a/client/src/components/sidebar/Sidebar.tsx
+++ b/client/src/components/sidebar/Sidebar.tsx
@@ -1,80 +1,6 @@
-import React from "react";
import { Link, useLocation } from "react-router-dom";
-import AppointmentsIcon from "../icons/Appointments";
-import DashboardIcon from "../icons/Dashboard";
-import Chat from "../icons/Chat";
-import UsersIcon from "../icons/UsersIcon";
-import {
- chatRoutes,
- studentBase,
- studentPrivatesRoutesVariables,
- teacherBase,
- teacherPrivatesRoutesVariables,
-} from "../../router/routesVariables/pathVariables.ts";
-import { joinPath } from "../../util/joinPath.util.ts";
import { Logo } from "../logo/Logo.tsx";
-
-export type MenuItem = {
- name: string;
- link: string;
- icon: React.ElementType;
-};
-
-export const defaultStudentMenuItems: MenuItem[] = [
- {
- name: "Dashboard",
- link: joinPath(studentBase, studentPrivatesRoutesVariables.dashboard),
- icon: DashboardIcon,
- },
- {
- name: "Appointments",
- link: joinPath(studentBase, studentPrivatesRoutesVariables.appointments),
- icon: AppointmentsIcon,
- },
- // {
- // name: "Video Call",
- // link: joinPath(studentBase, studentPrivatesRoutesVariables.videoCall),
- // icon: VideoCallIcon,
- // },
- {
- name: "My Profile",
- link: joinPath(studentBase, studentPrivatesRoutesVariables.profile),
- icon: UsersIcon,
- },
- {
- name: "Chat",
- link: joinPath(studentBase, chatRoutes.root),
- icon: Chat,
- },
-];
-
-export const defaultTeacherMenuItems: MenuItem[] = [
- {
- name: "Dashboard",
- link: joinPath(teacherBase, teacherPrivatesRoutesVariables.dashboard),
- icon: DashboardIcon,
- },
- {
- name: "Appointments",
- link: joinPath(teacherBase, teacherPrivatesRoutesVariables.appointments),
- icon: AppointmentsIcon,
- },
- // {
- // name: "Video Call",
- // link: joinPath(teacherBase, teacherPrivatesRoutesVariables.videoCall),
- // icon: VideoCallIcon,
- // },
- {
- name: "My Profile",
- link: joinPath(teacherBase, teacherPrivatesRoutesVariables.profile),
- icon: UsersIcon,
- },
- {
- name: "Chat",
- link: joinPath(teacherBase, chatRoutes.root),
- icon: Chat,
- },
-];
+import { type MenuItem, defaultStudentMenuItems } from "./sidebarMenuItems.ts";
type SidebarProps = {
items?: MenuItem[];
diff --git a/client/src/components/sidebar/sidebarMenuItems.ts b/client/src/components/sidebar/sidebarMenuItems.ts
index 49374416..007d5b44 100644
--- a/client/src/components/sidebar/sidebarMenuItems.ts
+++ b/client/src/components/sidebar/sidebarMenuItems.ts
@@ -3,7 +3,6 @@ import AppointmentsIcon from "../icons/Appointments";
import DashboardIcon from "../icons/Dashboard";
import Chat from "../icons/Chat";
import UsersIcon from "../icons/UsersIcon";
-// import VideoCallIcon from "../icons/VideoCallIcon";
import {
chatRoutes,
studentBase,
@@ -30,11 +29,6 @@ export const defaultStudentMenuItems: MenuItem[] = [
link: joinPath(studentBase, studentPrivatesRoutesVariables.appointments),
icon: AppointmentsIcon,
},
- // {
- // name: "Video Call",
- // link: joinPath(studentBase, studentPrivatesRoutesVariables.videoCall),
- // icon: VideoCallIcon,
- // },
{
name: "My Profile",
link: joinPath(studentBase, studentPrivatesRoutesVariables.profile),
@@ -58,11 +52,6 @@ export const defaultTeacherMenuItems: MenuItem[] = [
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.appointments),
icon: AppointmentsIcon,
},
- // {
- // name: "Video Call",
- // link: joinPath(teacherBase, teacherPrivatesRoutesVariables.videoCall),
- // icon: VideoCallIcon,
- // },
{
name: "My Profile",
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.profile),
diff --git a/client/src/components/teacherAppointmentCard/TeacherAppointmentActions.tsx b/client/src/components/teacherAppointmentCard/TeacherAppointmentActions.tsx
index 175b4f66..ebf03bd3 100644
--- a/client/src/components/teacherAppointmentCard/TeacherAppointmentActions.tsx
+++ b/client/src/components/teacherAppointmentCard/TeacherAppointmentActions.tsx
@@ -23,31 +23,37 @@ export const TeacherAppointmentActions = ({
onRemoveFromRegular,
isRegularTab = false,
}: TeacherAppointmentActionsProps) => {
+ const isInRegular = !!onRemoveFromRegular;
+
return (
- {!isPast && onStatusChange && !isRegularTab && (
+ {!isPast && !isRegularTab && onStatusChange && (
)}
- {status === "approved" && !isPast && onAddToRegular && (
-
- )}
+ {!isPast &&
+ !isRegularTab &&
+ status === "approved" &&
+ !isInRegular &&
+ onAddToRegular && (
+
+ )}
- {onRemoveFromRegular && (
+ {!isPast && onRemoveFromRegular && (
)}
diff --git a/client/src/components/teacherAppointmentCard/TeacherAppointmentCard.tsx b/client/src/components/teacherAppointmentCard/TeacherAppointmentCard.tsx
index b07616dc..db78e4c5 100644
--- a/client/src/components/teacherAppointmentCard/TeacherAppointmentCard.tsx
+++ b/client/src/components/teacherAppointmentCard/TeacherAppointmentCard.tsx
@@ -59,6 +59,8 @@ export const TeacherAppointmentCard = ({
lesson={appointment.lesson}
studentName={appointment.studentName}
price={appointment.price}
+ weeklySchedule={appointment.weeklySchedule}
+ isRegularTab={isRegularTab}
/>
{
+ const scheduleByDay: Record = {};
+ weeklySchedule.forEach((slot) => {
+ if (!scheduleByDay[slot.day]) {
+ scheduleByDay[slot.day] = [];
+ }
+ scheduleByDay[slot.day].push(slot.hour);
+ });
+
+ Object.keys(scheduleByDay).forEach((day) => {
+ scheduleByDay[day].sort((a, b) => a - b);
+ });
+
+ return Object.entries(scheduleByDay)
+ .map(([day, hours]) => {
+ const hoursStr = hours.map((h) => `${h}:00`).join(", ");
+ return `${day}: ${hoursStr}`;
+ })
+ .join(" • ");
};
export const TeacherAppointmentInfo = ({
lesson,
studentName,
price,
+ weeklySchedule,
+ isRegularTab = false,
}: TeacherAppointmentInfoProps) => {
return (
@@ -26,6 +53,12 @@ export const TeacherAppointmentInfo = ({
{price} euro /hour
+
+ {isRegularTab && weeklySchedule && weeklySchedule.length > 0 && (
+
+ {formatSchedule(weeklySchedule)}
+
+ )}
);
};
diff --git a/client/src/components/teacherAppointmentCard/TeacherAppointmentsList.tsx b/client/src/components/teacherAppointmentCard/TeacherAppointmentsList.tsx
index 90ff9336..651264d5 100644
--- a/client/src/components/teacherAppointmentCard/TeacherAppointmentsList.tsx
+++ b/client/src/components/teacherAppointmentCard/TeacherAppointmentsList.tsx
@@ -11,7 +11,7 @@ type TeacherAppointmentsListProps = {
onRemoveFromRegular?: (appointmentId: string) => void;
regularStudentIds?: string[];
isRegularTab?: boolean;
- onScheduleClick?: () => void;
+ onScheduleClick?: (appointment: Appointment) => void;
};
export const TeacherAppointmentsList = ({
@@ -45,7 +45,7 @@ export const TeacherAppointmentsList = ({
studentAvatar={appointment.studentProfileImageUrl}
isPast={isPast}
onStatusChange={
- !isPast && !isRegularTab
+ !isRegularTab && !isPast
? (newStatus: AppointmentStatus) =>
onStatusChange(appointment.id, newStatus)
: undefined
@@ -54,22 +54,30 @@ export const TeacherAppointmentsList = ({
onStartCall(appointment.studentId, appointment.id)
}
onDelete={
- isRegularTab || isPast
+ isPast || isRegularTab
? () => onDelete(appointment.id)
: undefined
}
onAddToRegular={
- !isRegularTab && !isInRegular && onAddToRegular
+ !isRegularTab &&
+ !isPast &&
+ !isInRegular &&
+ onAddToRegular &&
+ appointment.status === "approved"
? () => onAddToRegular(appointment)
: undefined
}
onRemoveFromRegular={
- isRegularTab && onRemoveFromRegular
+ !isPast && isInRegular && onRemoveFromRegular
? () => onRemoveFromRegular(appointment.id)
: undefined
}
isRegularTab={isRegularTab}
- onScheduleClick={onScheduleClick}
+ onScheduleClick={
+ isRegularTab && onScheduleClick
+ ? () => onScheduleClick(appointment)
+ : undefined
+ }
/>
);
})}
diff --git a/client/src/components/teacherProfileSection/LessonSchedule.tsx b/client/src/components/teacherProfileSection/LessonSchedule.tsx
index 9a467bc2..33c76e4d 100644
--- a/client/src/components/teacherProfileSection/LessonSchedule.tsx
+++ b/client/src/components/teacherProfileSection/LessonSchedule.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useMemo } from "react";
import { Button } from "../ui/button/Button.tsx";
import Cross from "../icons/Cross.tsx";
@@ -12,6 +12,7 @@ interface LessonScheduleProps {
onClose: () => void;
onSave: (slots: TimeSlot[]) => Promise | void;
initialSlots?: TimeSlot[];
+ bookedSlots?: TimeSlot[];
}
const DAYS = [
@@ -26,21 +27,32 @@ const DAYS = [
const HOURS = Array.from({ length: 17 }, (_, i) => i + 7);
-export const LessonSchedule = ({
- isOpen,
+const ModalContent = ({
onClose,
onSave,
initialSlots = [],
-}: LessonScheduleProps) => {
- const [selectedSlots, setSelectedSlots] = useState>(
+ bookedSlots = [],
+}: Omit) => {
+ const initialSelectedSlots = useMemo(
() => new Set(initialSlots.map((slot) => `${slot.day}-${slot.hour}`)),
+ [initialSlots],
);
- if (!isOpen) return null;
+ const [selectedSlots, setSelectedSlots] =
+ useState>(initialSelectedSlots);
+
+ const blockedSlots = useMemo(
+ () => new Set(bookedSlots.map((slot) => `${slot.day}-${slot.hour}`)),
+ [bookedSlots],
+ );
const toggleSlot = (day: string, hour: number) => {
const key = `${day}-${hour}`;
+ if (blockedSlots.has(key)) {
+ return;
+ }
+
setSelectedSlots((prev) => {
const newSet = new Set(prev);
if (newSet.has(key)) {
@@ -107,15 +119,18 @@ export const LessonSchedule = ({
{HOURS.map((hour) => {
const key = `${day}-${hour}`;
const isSelected = selectedSlots.has(key);
+ const isBlocked = blockedSlots.has(key);
return (
toggleSlot(day, hour)}
+ onClick={() => !isBlocked && toggleSlot(day, hour)}
className={`border border-gray-600 p-1 transition-colors min-w-[60px] min-h-[50px] ${
- isSelected
- ? "bg-purple-500 hover:bg-purple-600 cursor-pointer"
- : "bg-gray-700 hover:bg-gray-600 cursor-pointer"
+ isBlocked
+ ? "bg-red-900 cursor-not-allowed"
+ : isSelected
+ ? "bg-purple-500 hover:bg-purple-600 cursor-pointer"
+ : "bg-gray-700 hover:bg-gray-600 cursor-pointer"
}`}
> |
);
@@ -130,8 +145,14 @@ export const LessonSchedule = ({
-
free time lesson
+
Available time
+ {bookedSlots.length > 0 && (
+
+ )}
@@ -143,3 +164,9 @@ export const LessonSchedule = ({
);
};
+
+export const LessonSchedule = ({ isOpen, ...props }: LessonScheduleProps) => {
+ if (!isOpen) return null;
+
+ return ;
+};
diff --git a/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx b/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx
index 44fb1861..308c7761 100644
--- a/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx
+++ b/client/src/components/teacherSection/teacherSchedule/TeacherSchedule.tsx
@@ -31,11 +31,17 @@ export default function TeacherSchedule({ teacher }: TeacherScheduleProps) {
const { data } = useTeacherAppointmentsQuery(
isAuthenticated ? teacher?.id : undefined,
);
+
const appointments = useMemo(() => {
const allAppointments = data?.appointments || [];
return allAppointments.filter((apt) => apt.date && apt.time);
}, [data?.appointments]);
+ const regularStudents = useMemo(() => {
+ const allAppointments = data?.appointments || [];
+ return allAppointments.filter((apt) => apt.isRegularStudent === true);
+ }, [data?.appointments]);
+
const subjectOptions = useMemo(() => {
if (!teacher?.subjects) return [];
return teacher.subjects.map((subject) => ({
@@ -160,9 +166,13 @@ export default function TeacherSchedule({ teacher }: TeacherScheduleProps) {
}
const startHour = parseInt(startMatch[1], 10);
- const endHour = parseInt(endMatch[1], 10);
+ let endHour = parseInt(endMatch[1], 10);
- if (startHour < 0 || startHour > 23 || endHour < 0 || endHour > 23) {
+ if (slot.end === "23:59") {
+ endHour = 24;
+ }
+
+ if (startHour < 0 || startHour > 23) {
return;
}
@@ -184,6 +194,23 @@ export default function TeacherSchedule({ teacher }: TeacherScheduleProps) {
approvedAppointments.map((apt) => apt.time.substring(0, 5)),
);
+ const regularStudentSlots = new Set();
+ regularStudents.forEach(
+ (student: { weeklySchedule?: Array<{ day: string; hour: number }> }) => {
+ if (student.weeklySchedule && Array.isArray(student.weeklySchedule)) {
+ student.weeklySchedule.forEach(
+ (slot: { day: string; hour: number }) => {
+ const slotDayName = slot.day.toLowerCase();
+ if (slotDayName === dayName) {
+ const slotTime = `${slot.hour.toString().padStart(2, "0")}:00`;
+ regularStudentSlots.add(slotTime);
+ }
+ },
+ );
+ }
+ },
+ );
+
const now = new Date();
const today = new Date();
today.setHours(0, 0, 0, 0);
@@ -193,7 +220,7 @@ export default function TeacherSchedule({ teacher }: TeacherScheduleProps) {
const isToday = selectedDateOnly.getTime() === today.getTime();
return slots.filter((slot) => {
- if (bookedTimes.has(slot)) {
+ if (bookedTimes.has(slot) || regularStudentSlots.has(slot)) {
return false;
}
diff --git a/client/src/features/appointments/mutations/useRemoveRegularStudentMutation.ts b/client/src/features/appointments/mutations/useRemoveRegularStudentMutation.ts
index 42dfecd9..9718035e 100644
--- a/client/src/features/appointments/mutations/useRemoveRegularStudentMutation.ts
+++ b/client/src/features/appointments/mutations/useRemoveRegularStudentMutation.ts
@@ -12,9 +12,24 @@ export const useRemoveRegularStudentMutation = () => {
queryClient.invalidateQueries({
queryKey: queryKeys.regularStudents(),
});
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.regularTeachers(),
+ });
queryClient.invalidateQueries({
queryKey: queryKeys.appointments,
});
+ queryClient.invalidateQueries({
+ queryKey: ["appointments", "teacher"],
+ });
+ },
+ onError: (error: Error) => {
+ console.error("Failed to remove regular student:", error);
+ if (error && typeof error === "object" && "response" in error) {
+ console.error(
+ "Error response:",
+ (error as { response?: { data?: unknown } }).response?.data,
+ );
+ }
},
});
};
diff --git a/client/src/features/appointments/mutations/useSetRegularStudentMutation.ts b/client/src/features/appointments/mutations/useSetRegularStudentMutation.ts
index 3d7daccd..b27eb41e 100644
--- a/client/src/features/appointments/mutations/useSetRegularStudentMutation.ts
+++ b/client/src/features/appointments/mutations/useSetRegularStudentMutation.ts
@@ -11,9 +11,15 @@ export const useSetRegularStudentMutation = () => {
queryClient.invalidateQueries({
queryKey: queryKeys.regularStudents(),
});
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.regularTeachers(),
+ });
queryClient.invalidateQueries({
queryKey: queryKeys.appointments,
});
+ queryClient.invalidateQueries({
+ queryKey: ["appointments", "teacher"],
+ });
},
});
};
diff --git a/client/src/features/appointments/mutations/useUpdateWeeklyScheduleMutation.ts b/client/src/features/appointments/mutations/useUpdateWeeklyScheduleMutation.ts
index 20a0d1ab..7bd3b236 100644
--- a/client/src/features/appointments/mutations/useUpdateWeeklyScheduleMutation.ts
+++ b/client/src/features/appointments/mutations/useUpdateWeeklyScheduleMutation.ts
@@ -24,6 +24,9 @@ export const useUpdateWeeklyScheduleMutation = () => {
queryClient.invalidateQueries({
queryKey: queryKeys.appointments,
});
+ queryClient.invalidateQueries({
+ queryKey: ["appointments", "teacher"],
+ });
},
});
};
diff --git a/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts b/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts
index 724eea30..223081a2 100644
--- a/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts
+++ b/client/src/features/appointments/query/useTeacherAppointmentsQuery.ts
@@ -37,6 +37,6 @@ export const useTeacherAppointmentsQuery = (
return useQuery({
queryKey: queryKeys.teacherAppointments(resolvedTeacherId, page, limit),
queryFn: () => fetchTeacherAppointments(resolvedTeacherId, page, limit),
- enabled: !!resolvedTeacherId && !!user,
+ enabled: !!resolvedTeacherId,
});
};
diff --git a/client/src/layouts/PrivateLayout.tsx b/client/src/layouts/PrivateLayout.tsx
index 92f74bfb..9dae692a 100644
--- a/client/src/layouts/PrivateLayout.tsx
+++ b/client/src/layouts/PrivateLayout.tsx
@@ -1,9 +1,9 @@
import { Outlet } from "react-router-dom";
+import { Sidebar } from "../components/sidebar/Sidebar.tsx";
import {
defaultStudentMenuItems,
defaultTeacherMenuItems,
- Sidebar,
-} from "../components/sidebar/Sidebar.tsx";
+} from "../components/sidebar/sidebarMenuItems.ts";
import { TopBar } from "../components/headerPrivate/TopBar.tsx";
import { useAuthSessionStore } from "../store/authSession.store.ts";
import { useEffect } from "react";
diff --git a/client/src/pages/privateStudentsPages/clientsAppointments/ClientsAppointments.tsx b/client/src/pages/privateStudentsPages/clientsAppointments/ClientsAppointments.tsx
index e1c6bbe8..aeb661bf 100644
--- a/client/src/pages/privateStudentsPages/clientsAppointments/ClientsAppointments.tsx
+++ b/client/src/pages/privateStudentsPages/clientsAppointments/ClientsAppointments.tsx
@@ -9,16 +9,25 @@ import { getTeacherByIdApi } from "../../../api/teacher/teacher.api";
import { TeacherType } from "../../../api/teacher/teacher.type";
import { useAppointmentTime } from "../../../features/appointments/hooks/useAppointmentTime";
import { useModalStore } from "../../../store/modals.store";
+import { useRegularTeachersQuery } from "../../../features/appointments/query/useRegularTeachersQuery";
+import { ViewScheduleModal } from "../../../components/regularStudentScheduleModal/ViewScheduleModal";
+import { Appointment } from "../../../types/appointments.types";
export const ClientsAppointments = () => {
const [activeTab, setActiveTab] = useState<"requests" | "regular">(
"requests",
);
const [page, setPage] = useState(1);
+ const [regularPage, setRegularPage] = useState(1);
const [teachersData, setTeachersData] = useState>(
{},
);
+ const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
+ const [selectedTeacher, setSelectedTeacher] = useState(
+ null,
+ );
const limit = 5;
+ const regularLimit = 5;
const user = useAuthSessionStore((state) => state.user);
const { isPastAppointment } = useAppointmentTime();
const { open: openModal } = useModalStore();
@@ -29,10 +38,17 @@ export const ClientsAppointments = () => {
error,
} = useStudentAppointmentsQuery(user?.id || "", page, limit);
+ const { data: regularTeachersData } = useRegularTeachersQuery(
+ regularPage,
+ regularLimit,
+ );
+
const deleteAppointmentMutation = useDeleteAppointmentMutation();
const appointments = studentData?.appointments || [];
const totalPages = studentData?.totalPages || 1;
+ const regularTeachers = regularTeachersData?.appointments || [];
+ const regularTotalPages = regularTeachersData?.totalPages || 1;
useEffect(() => {
const fetchTeachersData = async () => {
@@ -72,6 +88,11 @@ export const ClientsAppointments = () => {
});
};
+ const handleShowSchedule = (appointment: Appointment) => {
+ setSelectedTeacher(appointment);
+ setIsScheduleModalOpen(true);
+ };
+
if (isLoading) {
return (
@@ -123,7 +144,7 @@ export const ClientsAppointments = () => {
: "text-gray-400 hover:text-gray-300"
}`}
>
- Regular Students
+ Regular Teachers
@@ -169,19 +190,57 @@ export const ClientsAppointments = () => {