From efcaffd953e86425b9640663a249ddde2bad2fca Mon Sep 17 00:00:00 2001 From: Phuc Date: Wed, 28 May 2025 11:34:10 -0500 Subject: [PATCH 01/32] Meeting Code --- backend/toast_tutor/controller/meeting.py | 53 +++ backend/toast_tutor/models.py | 19 + backend/toast_tutor/serializers.py | 23 +- backend/toast_tutor/urls.py | 10 +- frontend/src/App.jsx | 32 ++ frontend/src/assets/icon.jsx | 337 ++++++++++++------ .../components/booking_steps/MeetingUtils.jsx | 84 +++++ frontend/src/constant/APIRoutes.js | 7 + frontend/src/pages/meeting/BookingPage.jsx | 123 +++++++ .../src/pages/meeting/CreateMeetingPage.jsx | 224 ++++++++++++ .../src/pages/meeting/MeetingsListPage.jsx | 147 ++++++++ .../src/pages/meeting/TutorMeetingPage.jsx | 138 +++++++ 12 files changed, 1083 insertions(+), 114 deletions(-) create mode 100644 backend/toast_tutor/controller/meeting.py create mode 100644 frontend/src/components/booking_steps/MeetingUtils.jsx create mode 100644 frontend/src/pages/meeting/BookingPage.jsx create mode 100644 frontend/src/pages/meeting/CreateMeetingPage.jsx create mode 100644 frontend/src/pages/meeting/MeetingsListPage.jsx create mode 100644 frontend/src/pages/meeting/TutorMeetingPage.jsx diff --git a/backend/toast_tutor/controller/meeting.py b/backend/toast_tutor/controller/meeting.py new file mode 100644 index 00000000..8f7933c7 --- /dev/null +++ b/backend/toast_tutor/controller/meeting.py @@ -0,0 +1,53 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response + +from ..models import Meeting, User +from ..serializers import MeetingSerializer + +@api_view(["GET", "PATCH"]) +def meeting_detail_or_update(request, pk): + meeting = get_object_or_404(Meeting, pk=pk) + if request.method == "GET": + return Response(MeetingSerializer(meeting).data) + + serializer = MeetingSerializer(meeting, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + +@api_view(["POST"]) +def create_meeting(request): + serializer = MeetingSerializer(data=request.data) + if serializer.is_valid(): + meeting = serializer.save() + return Response(MeetingSerializer(meeting).data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +@api_view(["GET"]) +def get_meetings(request): + qs = Meeting.objects.all().order_by("-start_time") + return Response(MeetingSerializer(qs, many=True).data, status=200)\ + +@api_view(["GET"]) +def get_meetings_by_tutor(request, tutor_id: int): + qs = (Meeting.objects + .filter(organizer_id=tutor_id, status="scheduled") # optional filter + .order_by("-start_time")) + return Response(MeetingSerializer(qs, many=True).data, status=200) + +@api_view(["POST"]) +def book_meeting(request, pk: int): + mtg = get_object_or_404(Meeting, pk=pk, status="scheduled") + + student_id = request.data.get("student") + if not student_id: + return Response({"detail": "student id required"}, 400) + if mtg.organizer_id == int(student_id): + return Response({"detail": "organizer cannot book own meeting"}, 400) + + mtg.student_id = student_id + mtg.status = "booked" + mtg.save(update_fields=["student", "status"]) + return Response(MeetingSerializer(mtg).data, 200) \ No newline at end of file diff --git a/backend/toast_tutor/models.py b/backend/toast_tutor/models.py index 815d169a..153b0eea 100644 --- a/backend/toast_tutor/models.py +++ b/backend/toast_tutor/models.py @@ -82,6 +82,25 @@ class Award(models.Model): def __str__(self): return f"{self.name} ({self.year})" +class Meeting(models.Model): + status = models.CharField( + max_length=20, + choices=[ + ("scheduled", "Scheduled"), + ("booked", "Booked"), + ("completed", "Completed"), + ], + default="scheduled", + ) + organizer = models.ForeignKey(User, on_delete=models.CASCADE, + related_name="organized_meetings") + student = models.ForeignKey(User,on_delete=models.CASCADE, related_name="student_meetings", + null=True,blank=True) + start_time = models.DateTimeField() + end_time = models.DateTimeField() + google_event_id = models.CharField(max_length=255, blank=True, null=True) + google_meet_link = models.URLField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) class ResetToken(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="reset_tokens") diff --git a/backend/toast_tutor/serializers.py b/backend/toast_tutor/serializers.py index 4b550a87..3445a02d 100644 --- a/backend/toast_tutor/serializers.py +++ b/backend/toast_tutor/serializers.py @@ -121,7 +121,28 @@ class Meta: "award", ] - +class MeetingSerializer(serializers.ModelSerializer): + organizer_id = serializers.IntegerField(source="organizer.id", read_only=True) + organizer_name = serializers.CharField(source="organizer.username", read_only=True) + class Meta: + model = Meeting + fields = [ + "id", + "organizer_id", + "organizer_name", + "organizer", + "student", + "start_time", + "end_time", + "status", + "google_event_id", + "google_meet_link", + "created_at", + ] + extra_kwargs = { + "student": {"required": False, "allow_null": True}, + } + class ResetTokenSerializer(serializers.ModelSerializer): class Meta: model = ResetToken diff --git a/backend/toast_tutor/urls.py b/backend/toast_tutor/urls.py index e409c434..974ecb71 100644 --- a/backend/toast_tutor/urls.py +++ b/backend/toast_tutor/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .controller import matching, profile, auth +from .controller import matching, profile, auth, meeting # from .views import register_user, login_user, logout_user from .controller.userauth import login_user, logout_user, register_user @@ -12,6 +12,14 @@ path("auth/register/", register_user, name="register"), path("auth/login/", login_user, name="login"), path("auth/logout/", logout_user, name="logout"), + # MEETINGS + path("meetings/", meeting.get_meetings, name="meetings_list"), + path("meetings/create/", meeting.create_meeting, name="meeting_create"), + path("meetings//", meeting.meeting_detail_or_update, name="meeting_detail"), + path("meetings//book/", meeting.book_meeting, name="meeting_book"), + path("meetings/tutor//", # NEW + meeting.get_meetings_by_tutor, + name="meetings_by_tutor"), # AWARD SECTION path( "tutor/profile/addaward//", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e8f098cf..7968ca91 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,10 @@ import LandingPage from "./pages/LandingPage"; import TutorProfile from "./pages/TutorProfile"; import Booking from "./pages/features/booking_tutor/CombineBookingSteps"; import WaitingMatched from "./pages/features/booking_tutor/WaitingMatched"; +import MeetingsListPage from "./pages/meeting/MeetingsListPage"; +import CreateMeetingPage from "./pages/meeting/CreateMeetingPage"; +import BookingMeetingPage from "./pages/meeting/BookingPage"; +import TutorMeetingPage from "./pages/meeting/TutorMeetingPage"; import MatchedTutors from "./pages/features/booking_tutor/MatchedTutors"; import SignUp from "./pages/auth/SignUp"; import EnterEmail from "./pages/auth/password/EnterEmail"; @@ -54,6 +58,34 @@ function App() { } /> + + + + } + /> + + + + } + /> + + + + } + /> + } + />