Repository: KivSystemAdmin/aaasobo-management-system
Pull request #90: 61 add deadlines for rebooking classes
Author: login: Shingo023
Created: 2025-05-07T04:48:23Z | Merged: 2025-05-07T22:36:34Z

### Overview
This PR introduces the `rebookableUntil` dateTime field to each class, based on a request from the client (https://pivfk.slack.com/archives/C07PSSYJ1GA/p1745422931550039)."
The updated flow for booking, canceling, rebooking, and completing is shown in the image below.
![スクリーンショット 2025-05-06 午後8 47 43](https://github.com/user-attachments/assets/a4c76250-d864-4073-8e28-8a96b9d383a3)

### Review Preparations

1. Run `npm run prisma:init` in the backend directory.
2. Go to the **Instructor's Availability** tab in the admin dashboard and confirm that Helen is unavailable on **Sat 5/31**.
3. In the **Instructor's Schedule** tab, update Helen's schedule by making Saturday 09:00 and 10:00 available.

### Review Points
1. Using Prisma Studio, confirm that there is a dummy class with the `dateTime` set to `2025-05-03T09:00:00.000Z` and the `rebookableUntil` set to `2025-10-30T09:00:00.000Z`.
Then, complete the class from the admin dashboard and confirm in Prisma Studio that the `isRebookable` field has been updated to `false` and the `rebookableUntil` field to `null`.
![スクリーンショット 2025-05-06 午後6 16 44](https://github.com/user-attachments/assets/491b9ce7-2895-4183-9efe-bcb320d2652c)
![スクリーンショット 2025-05-06 午後6 08 19](https://github.com/user-attachments/assets/eae67189-4575-40f3-bc83-c7366c4ccdae)
![スクリーンショット 2025-05-06 午後6 10 26](https://github.com/user-attachments/assets/da4dd1de-3cb4-4730-be8c-c399bf4313bd)

2. Book regular classes for Saturdays from the customer dashboard, and confirm in Prisma Studio that the class status for Sat, 5/31 is set to `pending`, since Helen is unavailable that day.

![スクリーンショット 2025-05-06 午後3 32 44](https://github.com/user-attachments/assets/6878e64d-c111-4062-92ec-1b138a0e4d7a)

3. Rebook a class for June 7 by clicking the "Book Class" button on the customer dashboard, and confirm in Prisma Studio that the `isRebookable` and `rebookableUntil` fields have been updated, and that a new class has been created, as shown in the image below.
![スクリーンショット 2025-05-06 午後5 45 11](https://github.com/user-attachments/assets/79c9962f-af78-48ee-80ed-6f7ec35ffb6c)

4. Update the class status from `booked` to `canceledByInstructor` from the admin dashboard, and confirm in Prisma Studio that the `rebookableUntil` field has been updated, as shown in the image below.
![スクリーンショット 2025-05-06 午後5 51 48](https://github.com/user-attachments/assets/1ccb6fce-f5b6-4956-9971-cb09aaf780f4)
![スクリーンショット 2025-05-06 午後5 55 21](https://github.com/user-attachments/assets/3cf8aacf-3d8e-4196-bb1e-8a07ca71773f)

### Note:
There’s still more code that needs to be updated regarding the booking and canceling logic. I plan to address it in future PRs.



Diff:
diff --git a/backend/prisma/migrations/20250502183838_add_rebookable_until/migration.sql b/backend/prisma/migrations/20250502183838_add_rebookable_until/migration.sql
new file mode 100644
index 00000000..d4fb491a
--- /dev/null
+++ b/backend/prisma/migrations/20250502183838_add_rebookable_until/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Class" ADD COLUMN     "rebookableUntil" TIMESTAMP(3);
diff --git a/backend/prisma/migrations/20250506175923_add_pending_to_status/migration.sql b/backend/prisma/migrations/20250506175923_add_pending_to_status/migration.sql
new file mode 100644
index 00000000..c596478b
--- /dev/null
+++ b/backend/prisma/migrations/20250506175923_add_pending_to_status/migration.sql
@@ -0,0 +1,2 @@
+-- AlterEnum
+ALTER TYPE "Status" ADD VALUE 'pending';
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index f616aa0f..896b8a52 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -71,7 +71,7 @@ model Customer {
   children      Children[]
   subscription  Subscription[]
   prefecture    String
-  createdAt    DateTime       @default(now())
+  createdAt     DateTime       @default(now())
 }
 
 model Class {
@@ -88,6 +88,7 @@ model Class {
   subscriptionId   Int
   subscription     Subscription      @relation(fields: [subscriptionId], references: [id])
   isRebookable     Boolean           @default(true)
+  rebookableUntil  DateTime?
 }
 
 enum Status {
@@ -95,6 +96,7 @@ enum Status {
   completed
   canceledByCustomer
   canceledByInstructor
+  pending
 }
 
 model Admins {
diff --git a/backend/src/controllers/classesController.ts b/backend/src/controllers/classesController.ts
index 345341b7..20704ecb 100644
--- a/backend/src/controllers/classesController.ts
+++ b/backend/src/controllers/classesController.ts
@@ -91,6 +91,7 @@ export const getClassesByCustomerIdController = async (
         classAttendance,
         isRebookable,
         recurringClassId,
+        rebookableUntil,
       } = eachClass;
 
       return {
@@ -119,6 +120,7 @@ export const getClassesByCustomerIdController = async (
         status,
         isRebookable,
         recurringClassId,
+        rebookableUntil,
       };
     });
 
@@ -139,6 +141,7 @@ export const createClassController = async (req: Request, res: Response) => {
     childrenIds,
     status,
     recurringClassId,
+    rebookableUntil,
   } = req.body;
 
   // Check for missing fields
@@ -150,6 +153,7 @@ export const createClassController = async (req: Request, res: Response) => {
   if (!childrenIds) missingFields.push("childrenIds");
   if (!status) missingFields.push("status");
   if (!recurringClassId) missingFields.push("recurringClassId");
+  if (!rebookableUntil) missingFields.push("rebookableUntil");
 
   if (missingFields.length > 0) {
     return res.status(400).json({
@@ -195,7 +199,6 @@ export const createClassController = async (req: Request, res: Response) => {
       });
     }
 
-    const isRebookable = false;
     const [newClass, updatedClass] = await Promise.all([
       // Create a new Booked Class
       createClass(
@@ -206,18 +209,12 @@ export const createClassController = async (req: Request, res: Response) => {
           status,
           subscriptionId: subscription.id,
           recurringClassId,
+          rebookableUntil,
         },
         childrenIds,
       ),
-      // Upgrade the CanceledByCustomer class that was rebooked => isRebookable: false
-      updateClass(
-        classId,
-        undefined, // dateTime
-        undefined, // instructorId
-        undefined, // childrenIds
-        undefined, // status
-        isRebookable,
-      ),
+      // Update the rebooked class's isRebookable and rebookableUntil fields to prevent further rebooking
+      updateClass(classId, { isRebookable: false, rebookableUntil: null }),
     ]);
 
     res.status(201).json({ newClass, updatedClass });
@@ -304,18 +301,10 @@ export const getClassByIdController = async (req: Request, res: Response) => {
 // Update[Edit] a class
 export const updateClassController = async (req: Request, res: Response) => {
   const classId = parseInt(req.params.id);
-  const { dateTime, instructorId, childrenIds, status, isRebookable } =
-    req.body;
+  const classData = req.body;
 
   try {
-    const updatedClass = await updateClass(
-      classId,
-      dateTime,
-      instructorId,
-      childrenIds,
-      status,
-      isRebookable,
-    );
+    const updatedClass = await updateClass(classId, classData);
 
     res.status(200).json({
       message: "Class is updated successfully",
diff --git a/backend/src/helper/dateUtils.ts b/backend/src/helper/dateUtils.ts
index 861dc8d1..a2378a1e 100644
--- a/backend/src/helper/dateUtils.ts
+++ b/backend/src/helper/dateUtils.ts
@@ -78,22 +78,6 @@ export const getMonthNumber = (month: Month): number => {
   return months.indexOf(month);
 };
 
-export function getFirstDayOfFiveMonthsAgo(): Date {
-  // Step 1: Get the current date in Japan time
-  const nowUTC = new Date();
-  const nowJST = new Date(nowUTC.getTime() + 9 * 60 * 60 * 1000);
-
-  // Step 2: Calculate the first day of the month, 5 months ago, at 00:00:00 JST
-  const firstDayJST = new Date(
-    Date.UTC(nowJST.getFullYear(), nowJST.getMonth() - 5, 1, 0, 0, 0, 0),
-  );
-
-  // Step 3: Convert JST to UTC (subtract 9 hours)
-  const firstDayUTC = new Date(firstDayJST.getTime() - 9 * 60 * 60 * 1000);
-
-  return firstDayUTC;
-}
-
 // Function to format time (e.g., 19:00)
 export const formatTime = (date: Date) => {
   const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -143,6 +127,6 @@ export function convertDayTimeToUTC(day: Day, time: string) {
   };
 }
 
-export const nHoursLater = (n: number) => {
-  return new Date(new Date().getTime() + n * 60 * 60 * 1000);
+export const nHoursLater = (n: number, dateTime: Date = new Date()) => {
+  return new Date(dateTime.getTime() + n * 60 * 60 * 1000);
 };
diff --git a/backend/src/seed/dummy.ts b/backend/src/seed/dummy.ts
index fff0b228..5f164c98 100644
--- a/backend/src/seed/dummy.ts
+++ b/backend/src/seed/dummy.ts
@@ -369,6 +369,15 @@ async function insertClasses() {
         subscriptionId: alice.subscription[0].id,
         recurringClassId: 1,
       },
+      {
+        instructorId: helen.id,
+        customerId: alice.id,
+        dateTime: "2025-05-03T09:00:00Z",
+        status: "booked",
+        subscriptionId: alice.subscription[0].id,
+        recurringClassId: 1,
+        rebookableUntil: "2025-10-30T09:00:00Z",
+      },
       // {
       //   instructorId: helen.id,
       //   customerId: bob.id,
@@ -857,6 +866,31 @@ async function insertInstructorUnavailabilities() {
         instructorId: helen.id,
         dateTime: new Date("2025-03-17T07:30:00Z"),
       },
+      // Helen is unavailable on Saturday, May 31.
+      {
+        instructorId: helen.id,
+        dateTime: new Date("2025-05-31T09:00:00Z"),
+      },
+      {
+        instructorId: helen.id,
+        dateTime: new Date("2025-05-31T09:30:00Z"),
+      },
+      {
+        instructorId: helen.id,
+        dateTime: new Date("2025-05-31T10:00:00Z"),
+      },
+      {
+        instructorId: helen.id,
+        dateTime: new Date("2025-05-31T10:30:00Z"),
+      },
+      {
+        instructorId: helen.id,
+        dateTime: new Date("2025-05-31T11:00:00Z"),
+      },
+      {
+        instructorId: helen.id,
+        dateTime: new Date("2025-05-31T11:30:00Z"),
+      },
     ],
   });
 }
@@ -929,7 +963,7 @@ async function main() {
     await insertAdmins();
 
     // Dependant on the above
-    await insertInstructorAvailabilities();
+    // await insertInstructorAvailabilities();
     await insertSubscriptions();
     await insertChildren();
 
diff --git a/backend/src/services/classesService.ts b/backend/src/services/classesService.ts
index dd44db37..42098c1d 100644
--- a/backend/src/services/classesService.ts
+++ b/backend/src/services/classesService.ts
@@ -1,6 +1,6 @@
 import { Prisma, Status } from "@prisma/client";
 import { prisma } from "../../prisma/prismaClient";
-import { getFirstDayOfFiveMonthsAgo } from "../helper/dateUtils";
+import { nHoursLater } from "../helper/dateUtils";
 
 // Fetch all the classes with related instructors and customers data
 export const getAllClasses = async () => {
@@ -70,6 +70,7 @@ export const createClass = async (
     status: Status;
     subscriptionId: number;
     recurringClassId: number;
+    rebookableUntil: string;
   },
   childrenIds: number[],
 ) => {
@@ -168,40 +169,34 @@ export const getClassById = async (classId: number) => {
 
 // Update/Edit a class
 export const updateClass = async (
-  classId: number,
-  dateTime?: string,
-  instructorId?: number,
-  childrenIds?: number[],
-  status?:
-    | "booked"
-    | "completed"
-    | "canceledByCustomer"
-    | "canceledByInstructor",
-  isRebookable?: boolean,
+  id: number,
+  classData: {
+    dateTime?: string;
+    instructorId?: number;
+    childrenIds?: number[];
+    status?: Status;
+    isRebookable?: boolean;
+    rebookableUntil?: string | null;
+  },
 ) => {
+  const { childrenIds, ...fieldsToUpdate } = classData;
   try {
     const updatedClass = await prisma.$transaction(async (prisma) => {
-      // Update the Class data
       const updatedClass = await prisma.class.update({
-        where: { id: classId },
-        data: {
-          dateTime,
-          instructorId,
-          status,
-          isRebookable,
-        },
+        where: { id },
+        data: fieldsToUpdate,
       });
 
       // Delete existing classAttendance records
       await prisma.classAttendance.deleteMany({
-        where: { classId },
+        where: { classId: id },
       });
 
       // Add new classAttendance records if childrenIds is provided
       if (childrenIds) {
         await prisma.classAttendance.createMany({
           data: childrenIds.map((childId) => ({
-            classId,
+            classId: id,
             childrenId: childId,
           })),
         });
@@ -446,7 +441,8 @@ export const checkDoubleBooking = async (
 };
 
 export const getBookableClasses = async (customerId: number) => {
-  const fiveMonthsAgoFirstDay = getFirstDayOfFiveMonthsAgo();
+  // A class can only be rebooked if its "rebookableUntil" time is more than three hours from now
+  const rebookableFrom = nHoursLater(3);
 
   const classes = await prisma.class.findMany({
     where: {
@@ -455,14 +451,20 @@ export const getBookableClasses = async (customerId: number) => {
       OR: [
         {
           status: "canceledByCustomer",
-          dateTime: {
-            gte: fiveMonthsAgoFirstDay,
+          rebookableUntil: {
+            gte: rebookableFrom,
           },
         },
         {
           status: "canceledByInstructor",
-          dateTime: {
-            gte: fiveMonthsAgoFirstDay,
+          rebookableUntil: {
+            gte: rebookableFrom,
+          },
+        },
+        {
+          status: "pending",
+          rebookableUntil: {
+            gte: rebookableFrom,
           },
         },
       ],
@@ -472,7 +474,7 @@ export const getBookableClasses = async (customerId: number) => {
     },
   });
 
-  // Return only the dateTime property from the class data
+  // TODO: Also return "id" and "rebookableUntil" for each class
   return classes.map((classItem) => classItem.dateTime);
 };
 
@@ -580,7 +582,8 @@ export const createCanceledClasses = async ({
         recurringClassId,
         subscriptionId,
         dateTime,
-        status: "canceledByInstructor",
+        status: "pending", // NOTE: the status has been changed from "canceledByInstructor" to "pending"
+        rebookableUntil: nHoursLater(180 * 24, dateTime), // 180 days (* 24 hours) after the class dateTime,
       })),
     });
     // Add the Class Attendance to the ClassAttendance Table based on the Class ID.
@@ -606,6 +609,9 @@ export const getCustomerClasses = async (customerId: number) => {
   const classes = await prisma.class.findMany({
     where: {
       customerId: customerId,
+      NOT: {
+        status: "pending",
+      },
     },
     orderBy: {
       dateTime: "desc",
diff --git a/backend/src/services/passwordResetTokensService.ts b/backend/src/services/passwordResetTokensService.ts
index 261d5e77..65670b46 100644
--- a/backend/src/services/passwordResetTokensService.ts
+++ b/backend/src/services/passwordResetTokensService.ts
@@ -1,9 +1,10 @@
 import { v4 as uuidv4 } from "uuid";
 import { prisma } from "../../prisma/prismaClient";
+import { nHoursLater } from "../helper/dateUtils";
 
 export const generatePasswordResetToken = async (email: string) => {
   const token = uuidv4();
-  const expires = new Date(new Date().getTime() + 60 * 60 * 1000);
+  const expires = nHoursLater(1);
 
   // Delete an existing token to make only the latest one valid
   const existingToken = await prisma.passwordResetToken.findFirst({
diff --git a/backend/src/services/recurringClassesService.ts b/backend/src/services/recurringClassesService.ts
index ed10efd3..7062c2d0 100644
--- a/backend/src/services/recurringClassesService.ts
+++ b/backend/src/services/recurringClassesService.ts
@@ -1,5 +1,6 @@
 import { Prisma } from "@prisma/client";
 import { prisma } from "../../prisma/prismaClient";
+import { nHoursLater } from "../helper/dateUtils";
 
 // Create a new recurring class in the DB
 export const addRecurringClass = async (
@@ -31,6 +32,7 @@ export const addRecurringClass = async (
         subscriptionId,
         dateTime,
         status: "booked",
+        rebookableUntil: nHoursLater(180 * 24, dateTime), // 180 days (* 24 hours) after the class dateTime,
       })),
     });
     // Add the Class Attendance to the ClassAttendance Table based on the Class ID.
diff --git a/backend/src/services/verificationTokensService.ts b/backend/src/services/verificationTokensService.ts
index f5726acc..27f75d3b 100644
--- a/backend/src/services/verificationTokensService.ts
+++ b/backend/src/services/verificationTokensService.ts
@@ -1,6 +1,7 @@
 import { v4 as uuidv4 } from "uuid";
 import { prisma } from "../../prisma/prismaClient";
 import { Prisma } from "@prisma/client";
+import { nHoursLater } from "../helper/dateUtils";
 
 export const generateVerificationToken = async (
   email: string,
@@ -8,7 +9,7 @@ export const generateVerificationToken = async (
 ) => {
   const db = tx ?? prisma;
   const token = uuidv4();
-  const expires = new Date(new Date().getTime() + 60 * 60 * 1000);
+  const expires = nHoursLater(1);
 
   // Delete an existing token to make only the latest one valid
   const existingToken = await db.verificationToken.findFirst({
diff --git a/frontend/src/app/components/customers-dashboard/classes/BookClass.tsx b/frontend/src/app/components/customers-dashboard/classes/BookClass.tsx
index d55f139d..87b580cb 100644
--- a/frontend/src/app/components/customers-dashboard/classes/BookClass.tsx
+++ b/frontend/src/app/components/customers-dashboard/classes/BookClass.tsx
@@ -66,7 +66,8 @@ function BookClass({
               eachClass.isRebookable) ||
             (eachClass.status === "canceledByInstructor" &&
               eachClass.isRebookable &&
-              now <= fiveMonthsLaterEndOfMonth)
+              now <= fiveMonthsLaterEndOfMonth) ||
+            (eachClass.status === "pending" && eachClass.isRebookable)
           );
         });
 
diff --git a/frontend/src/app/components/customers-dashboard/classes/BookClassForm.tsx b/frontend/src/app/components/customers-dashboard/classes/BookClassForm.tsx
index 810fe432..e6e3896f 100644
--- a/frontend/src/app/components/customers-dashboard/classes/BookClassForm.tsx
+++ b/frontend/src/app/components/customers-dashboard/classes/BookClassForm.tsx
@@ -140,6 +140,7 @@ function BookClassForm({
         status: "booked",
         childrenIds: selectedChildrenIdsArray,
         recurringClassId: classToRebook.recurringClassId,
+        rebookableUntil: classToRebook.rebookableUntil,
       });
 
       toast.success("The class has been successfully booked!");
diff --git a/frontend/src/app/components/customers-dashboard/classes/EditClassForm.tsx b/frontend/src/app/components/customers-dashboard/classes/EditClassForm.tsx
index 9895582c..5cf97678 100644
--- a/frontend/src/app/components/customers-dashboard/classes/EditClassForm.tsx
+++ b/frontend/src/app/components/customers-dashboard/classes/EditClassForm.tsx
@@ -72,8 +72,7 @@ function EditClassForm({
     const selectedChildrenIdsArray = Array.from(selectedChildrenIds);
 
     try {
-      await editClass({
-        classId: editedClass.id,
+      await editClass(editedClass.id, {
         dateTime: selectedDateTime,
         instructorId: selectedInstructorId,
         childrenIds: selectedChildrenIdsArray,
diff --git a/frontend/src/app/components/instructors-dashboard/class-schedule/ClassDetails.tsx b/frontend/src/app/components/instructors-dashboard/class-schedule/ClassDetails.tsx
index a00bf5b7..7a629691 100644
--- a/frontend/src/app/components/instructors-dashboard/class-schedule/ClassDetails.tsx
+++ b/frontend/src/app/components/instructors-dashboard/class-schedule/ClassDetails.tsx
@@ -7,12 +7,6 @@ import styles from "./ClassDetails.module.scss";
 import Link from "next/link";
 import Loading from "../../elements/loading/Loading";
 
-type StatusType =
-  | "booked"
-  | "completed"
-  | "canceledByCustomer"
-  | "canceledByInstructor";
-
 function ClassDetails({
   instructorId,
   classId,
@@ -67,7 +61,7 @@ function ClassDetails({
   const handleUpdateClassDetail = (
     completedClassId: number,
     attendedChildren: Child[],
-    updatedStatus: StatusType,
+    updatedStatus: ClassStatus,
   ) => {
     if (completedClassId !== classId) return;
 
diff --git a/frontend/src/app/components/instructors-dashboard/class-schedule/InstructorClassesTable.tsx b/frontend/src/app/components/instructors-dashboard/class-schedule/InstructorClassesTable.tsx
index 2deebe16..c5af8c22 100644
--- a/frontend/src/app/components/instructors-dashboard/class-schedule/InstructorClassesTable.tsx
+++ b/frontend/src/app/components/instructors-dashboard/class-schedule/InstructorClassesTable.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
 import {
   formatTime24Hour,
   isPastClassEndTime,
+  nHoursLater,
 } from "@/app/helper/utils/dateUtils";
 import styles from "./InstructorClassesTable.module.scss";
 import Link from "next/link";
@@ -15,12 +16,6 @@ import {
   UsersIcon,
 } from "@heroicons/react/24/solid";
 
-type StatusType =
-  | "booked"
-  | "completed"
-  | "canceledByCustomer"
-  | "canceledByInstructor";
-
 const InstructorClassesTable = ({
   instructorId,
   selectedDateClasses,
@@ -36,7 +31,7 @@ const InstructorClassesTable = ({
   handleUpdateClassDetail: (
     completedClassId: number,
     attendedChildren: Child[],
-    updatedStatus: StatusType,
+    updatedStatus: ClassStatus,
   ) => void;
   isAdminAuthenticate?: boolean;
   classDate: string;
@@ -47,7 +42,7 @@ const InstructorClassesTable = ({
   const [selectedChildrenIds, setSelectedChildrenIds] = useState<Set<number>>(
     new Set(),
   );
-  const [selectedStatus, setSelectedStatus] = useState<StatusType>("booked");
+  const [selectedStatus, setSelectedStatus] = useState<ClassStatus>("booked");
 
   useEffect(() => {
     setClasses(selectedDateClasses);
@@ -61,7 +56,7 @@ const InstructorClassesTable = ({
     classId: number,
     children: Child[],
     classStart: string,
-    status: StatusType,
+    status: ClassStatus,
   ) => {
     if (!isPastClassEndTime(classStart, timeZone) && !isAdminAuthenticate) {
       return alert(
@@ -88,7 +83,7 @@ const InstructorClassesTable = ({
     setSelectedChildrenIds(new Set(selectedChildrenIds));
   };
 
-  const handleStatusChange = (changedStatus: StatusType) => {
+  const handleStatusChange = (changedStatus: ClassStatus) => {
     setSelectedStatus(changedStatus);
   };
 
@@ -102,7 +97,7 @@ const InstructorClassesTable = ({
     classToCompleteId: number,
     registeredChildren: Child[], // All of the initially registered children(all the children of the customer)
     classStart: string,
-    updatedStatus: StatusType,
+    updatedStatus: ClassStatus,
     childrenWithoutEditingAttendance?: Child[],
   ) => {
     if (
@@ -116,16 +111,24 @@ const InstructorClassesTable = ({
 
     const attendedChildrenIds = childrenWithoutEditingAttendance
       ? childrenWithoutEditingAttendance.map((child) => child.id)
-      : Array.from(selectedChildrenIds);
+      : updatedStatus === "canceledByInstructor"
+        ? undefined
+        : Array.from(selectedChildrenIds);
 
     const isRebookable = updatedStatus !== "completed";
+    const rebookableUntil =
+      updatedStatus === "canceledByInstructor"
+        ? nHoursLater(180 * 24, new Date(classStart)).toISOString() // If the class is canceled by the instructor, set rebookableUntil to 180 days (* 24 * 60 minutes) after the class dateTime
+        : updatedStatus === "completed"
+          ? null
+          : undefined;
 
     try {
-      await editClass({
-        classId: classToCompleteId,
+      await editClass(classToCompleteId, {
         childrenIds: attendedChildrenIds,
         status: updatedStatus,
-        isRebookable: isRebookable,
+        isRebookable,
+        rebookableUntil,
       });
 
       setClasses((prev) => {
@@ -138,7 +141,7 @@ const InstructorClassesTable = ({
                 attendingChildren:
                   childrenWithoutEditingAttendance ||
                   registeredChildren.filter((child) =>
-                    attendedChildrenIds.includes(child.id),
+                    attendedChildrenIds?.includes(child.id),
                   ),
                 status: updatedStatus,
               }
@@ -150,7 +153,7 @@ const InstructorClassesTable = ({
         classToCompleteId,
         childrenWithoutEditingAttendance ||
           registeredChildren.filter((child) =>
-            attendedChildrenIds.includes(child.id),
+            attendedChildrenIds?.includes(child.id),
           ),
         updatedStatus,
       );
@@ -164,7 +167,7 @@ const InstructorClassesTable = ({
     setSelectedStatus("booked");
   };
 
-  const statusToString = (status: StatusType): string => {
+  const statusToString = (status: ClassStatus): string => {
     switch (status) {
       case "booked":
         return "Booked";
diff --git a/frontend/src/app/helper/api/classesApi.ts b/frontend/src/app/helper/api/classesApi.ts
index 20d83c23..7da28127 100644
--- a/frontend/src/app/helper/api/classesApi.ts
+++ b/frontend/src/app/helper/api/classesApi.ts
@@ -46,6 +46,7 @@ export const bookClass = async (classData: {
   childrenIds: number[];
   status: string;
   recurringClassId: number;
+  rebookableUntil: string;
 }) => {
   try {
     const response = await fetch(`${BACKEND_ORIGIN}/classes`, {
@@ -90,22 +91,21 @@ export const getClassById = async (classId: number) => {
 };
 
 // PATCH a class date
-export const editClass = async (editedClass: {
-  classId: number;
-  childrenIds: number[];
-  dateTime?: string;
-  status?:
-    | "booked"
-    | "completed"
-    | "canceledByCustomer"
-    | "canceledByInstructor";
-  instructorId?: number;
-  isRebookable?: boolean;
-}) => {
+export const editClass = async (
+  id: number,
+  classData: {
+    childrenIds?: number[];
+    dateTime?: string;
+    status?: ClassStatus;
+    instructorId?: number;
+    isRebookable?: boolean;
+    rebookableUntil?: string | null;
+  },
+) => {
   // Define the data to be sent to the server side.
-  const classURL = `${BACKEND_ORIGIN}/classes/${editedClass.classId}`;
+  const classURL = `${BACKEND_ORIGIN}/classes/${id}`;
   const headers = { "Content-Type": "application/json" };
-  const body = JSON.stringify(editedClass);
+  const body = JSON.stringify(classData);
 
   const response = await fetch(classURL, {
     method: "PATCH",
diff --git a/frontend/src/app/helper/types.d.ts b/frontend/src/app/helper/types.d.ts
index 6f25825d..09d96a2b 100644
--- a/frontend/src/app/helper/types.d.ts
+++ b/frontend/src/app/helper/types.d.ts
@@ -35,7 +35,8 @@ type ClassStatus =
   | "booked"
   | "completed"
   | "canceledByCustomer"
-  | "canceledByInstructor";
+  | "canceledByInstructor"
+  | "pending";
 
 type ClassType = {
   id: number;
@@ -59,6 +60,7 @@ type ClassType = {
   status: ClassStatus;
   isRebookable: boolean;
   recurringClassId: number;
+  rebookableUntil: string;
 };
 
 type Admin = {
diff --git a/frontend/src/app/helper/utils/dateUtils.ts b/frontend/src/app/helper/utils/dateUtils.ts
index ed53be74..b81f3af5 100644
--- a/frontend/src/app/helper/utils/dateUtils.ts
+++ b/frontend/src/app/helper/utils/dateUtils.ts
@@ -272,3 +272,7 @@ export const formatTime24Hour = (date: Date): string => {
 
   return `${hours}:${minutes.toString().padStart(2, "0")}`;
 };
+
+export const nHoursLater = (n: number, dateTime: Date = new Date()): Date => {
+  return new Date(dateTime.getTime() + n * 60 * 60 * 1000);
+};


Reviews:
user: login: mtkksk1780
body: Thank you for the implementation! I think this feature is one of the most challenging parts since the client’s requirements are somewhat confusing. The logic is well organized and the feature worked correctly as you instructed. While testing the behavior, I noticed the following two points:  

1. The “pending” class still has the same status even after Review Point 4. Since "pending" is typically a temporary status, it might be a good idea to change it to something that indicates the class is no longer in use (Maybe “Replaced” status would be suitable in this context?).
<img width="1066" alt="Screenshot 2025-05-07 at 19 49 43" src="https://github.com/user-attachments/assets/963492e0-97dc-47fb-980b-78c09e61cf4c" />

###

2. The “Canceled by Instructor” status class can no longer be operated after clicking the “Edit” button. Ideally, the button should display both “Cancel” and “Complete”, but currently the “Edit” button is used. I believe this is outside the scope of this PR, but we should keep it in mind to fix later.

<img width="1128" alt="Screenshot 2025-05-07 at 19 48 22" src="https://github.com/user-attachments/assets/22cee08d-baf4-4cfd-b2cc-27e36384f205" />

###

Also I have left some comments and I hope they are helpful for your update.

state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T15:48:57Z
user: login: Shingo023
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T18:44:43Z
user: login: Shingo023
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T19:25:36Z

Review comments:
diff_hunk: @@ -446,7 +440,7 @@ export const checkDoubleBooking = async (
 };
 
 export const getBookableClasses = async (customerId: number) => {
-  const fiveMonthsAgoFirstDay = getFirstDayOfFiveMonthsAgo();
+  const threeHoursFromNow = new Date(new Date().getTime() + 180 * 60 * 1000);
path: backend/src/services/classesService.ts
user: login: mtkksk1780
body: This part can be reused like below if it's added to a utility file.
https://github.com/KivSystemAdmin/aaasobo-management-system/pull/85#discussion_r2075520566

However, the utility function needs a slight update because of the following comment.
https://github.com/KivSystemAdmin/aaasobo-management-system/pull/90#discussion_r2077963994
↓
```
const nHoursLater = (n: number, dateTime: Date = new Date()): Date => {
  return new Date(dateTime.getTime() + n * 60 * 60 * 1000);
};
```
`const baseHours = nHoursLater(3);`
created_at: 2025-05-07T15:46:45Z
author_association: MEMBER
diff_hunk: @@ -580,7 +580,10 @@ export const createCanceledClasses = async ({
         recurringClassId,
         subscriptionId,
         dateTime,
-        status: "canceledByInstructor",
+        status: "pending", // NOTE: the status has been changed from "canceledByInstructor" to "pending"
+        rebookableUntil: new Date(
+          new Date(dateTime).getTime() + 259200 * 60 * 1000, // 180 days (259200 minutes) after the class dateTime
+        ),
path: backend/src/services/classesService.ts
user: login: mtkksk1780
body: This part can also be refactored like this.

`rebookableUntil: nHoursLater(180 * 24, dateTime), // 180 days (* 24 hours) after the class dateTime`
created_at: 2025-05-07T15:47:19Z
author_association: MEMBER
diff_hunk: @@ -31,6 +31,9 @@ export const addRecurringClass = async (
         subscriptionId,
         dateTime,
         status: "booked",
+        rebookableUntil: new Date(
+          new Date(dateTime).getTime() + 259200 * 60 * 1000, // 180 days (259200 minutes) after the class dateTime
+        ),
path: backend/src/services/recurringClassesService.ts
user: login: mtkksk1780
body: This is the same as the above case.

`rebookableUntil: nHoursLater(180 * 24, dateTime), // 180 days (* 24 hours) after the class dateTime`
created_at: 2025-05-07T15:47:46Z
author_association: MEMBER
diff_hunk: @@ -116,16 +110,26 @@ const InstructorClassesTable = ({
 
     const attendedChildrenIds = childrenWithoutEditingAttendance
       ? childrenWithoutEditingAttendance.map((child) => child.id)
-      : Array.from(selectedChildrenIds);
+      : updatedStatus === "canceledByInstructor"
+        ? undefined
+        : Array.from(selectedChildrenIds);
 
     const isRebookable = updatedStatus !== "completed";
+    const rebookableUntil =
+      updatedStatus === "canceledByInstructor"
+        ? new Date(
+            new Date(classStart).getTime() + 259200 * 60 * 1000, // If the class is canceled by the instructor, set rebookableUntil to 180 days (259200 minutes) after the class dateTime
+          ).toISOString()
path: frontend/src/app/components/instructors-dashboard/class-schedule/InstructorClassesTable.tsx
user: login: mtkksk1780
body: Frontend can also use the same concept but in that case, the utility function need to be added to another file like `frontend/src/app/helper/utils/dateUtils.ts`.

```
const nHoursLater = (n: number, dateTime: Date = new Date()): Date => {
  return new Date(dateTime.getTime() + n * 60 * 60 * 1000);
};
```
↓
```
? nHoursLater(180 * 24, dateTime).toISOString() // If the class is canceled by the instructor, set rebookableUntil to 180 days (* 24 * 60 minutes) after the class dateTime
```
created_at: 2025-05-07T15:48:32Z
author_association: MEMBER
diff_hunk: @@ -446,7 +440,7 @@ export const checkDoubleBooking = async (
 };
 
 export const getBookableClasses = async (customerId: number) => {
-  const fiveMonthsAgoFirstDay = getFirstDayOfFiveMonthsAgo();
+  const threeHoursFromNow = new Date(new Date().getTime() + 180 * 60 * 1000);
path: backend/src/services/classesService.ts
user: login: Shingo023
body: I added the above updated `nHoursLater` utility to `dateUtils.ts` and reused it here as well! Thank you!
created_at: 2025-05-07T18:44:43Z
author_association: COLLABORATOR
diff_hunk: @@ -116,16 +110,26 @@ const InstructorClassesTable = ({
 
     const attendedChildrenIds = childrenWithoutEditingAttendance
       ? childrenWithoutEditingAttendance.map((child) => child.id)
-      : Array.from(selectedChildrenIds);
+      : updatedStatus === "canceledByInstructor"
+        ? undefined
+        : Array.from(selectedChildrenIds);
 
     const isRebookable = updatedStatus !== "completed";
+    const rebookableUntil =
+      updatedStatus === "canceledByInstructor"
+        ? new Date(
+            new Date(classStart).getTime() + 259200 * 60 * 1000, // If the class is canceled by the instructor, set rebookableUntil to 180 days (259200 minutes) after the class dateTime
+          ).toISOString()
path: frontend/src/app/components/instructors-dashboard/class-schedule/InstructorClassesTable.tsx
user: login: Shingo023
body: I added the `nHoursLater` utility to `dateUtils.ts` in the frontend and used it here as well! Thank you for your great suggestion!
created_at: 2025-05-07T19:25:36Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-07T04:48:28Z
body: [vc]: #xtePkouzOtE/1dMs3HQBAj6/8kGsMTBOsbYlshRhxSA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhYWFzb2JvLW1hbmFnZW1lbnQtc3lzdGVtLWZyb250ZW5kIiwicm9vdERpcmVjdG9yeSI6ImZyb250ZW5kIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20va2l2c3lzdGVtYWRtaW5zLXByb2plY3RzL2FhYXNvYm8tbWFuYWdlbWVudC1zeXN0ZW0tZnJvbnRlbmQvejNWVGJRcmF1YXNCRzlSWjI1NzI5VDVyVzdIciIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6ImFhYXNvYm8tbWFuYWdlbWVudC1zeXN0ZW0tZi1naXQtZTc0MzkxLWtpdnN5c3RlbWFkbWlucy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0seyJuYW1lIjoiYWFhc29iby1tYW5hZ2VtZW50LXN5c3RlbS1iYWNrZW5kIiwicm9vdERpcmVjdG9yeSI6ImJhY2tlbmQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9raXZzeXN0ZW1hZG1pbnMtcHJvamVjdHMvYWFhc29iby1tYW5hZ2VtZW50LXN5c3RlbS1iYWNrZW5kL0hWb2FibkhuZHZTWlFwRUh3SmtTVlRYckNCenEiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsInByZXZpZXdVcmwiOiJhYWFzb2JvLW1hbmFnZW1lbnQtc3lzdGVtLWItZ2l0LTI0ZWQ1NC1raXZzeXN0ZW1hZG1pbnMtcHJvamVjdHMudmVyY2VsLmFwcCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **aaasobo-management-system-backend** | ⬜️ Ignored ([Inspect](https://vercel.com/kivsystemadmins-projects/aaasobo-management-system-backend/HVoabnHndvSZQpEHwJkSVTXrCBzq)) | [Visit Preview](https://aaasobo-management-system-b-git-24ed54-kivsystemadmins-projects.vercel.app) |  | May 7, 2025 10:35pm |
| **aaasobo-management-system-frontend** | ⬜️ Ignored ([Inspect](https://vercel.com/kivsystemadmins-projects/aaasobo-management-system-frontend/z3VTbQrauasBG9RZ25729T5rW7Hr)) | [Visit Preview](https://aaasobo-management-system-f-git-e74391-kivsystemadmins-projects.vercel.app) |  | May 7, 2025 10:35pm |
</details>



author_association: NONE
user: login: Shingo023
created_at: 2025-05-07T18:40:35Z
body: @mtkksk1780 

Thank you so much for your feedback — it was really helpful!

> The “pending” class still has the same status even after Review Point 4. Since "pending" is typically a temporary status, it might be a good idea to change it to something that indicates the class is no longer in use (Maybe “Replaced” status would be suitable in this context?).

That's a very good point — I didn't think about it.
Unlike classes with other statuses, there's no need to keep pending classes in the database once they're booked. In a future PR, I’ll make sure to delete all pending classes after they are booked.

> The “Canceled by Instructor” status class can no longer be operated after clicking the “Edit” button. Ideally, the button should display both “Cancel” and “Complete”, but currently the “Edit” button is used. I believe this is outside the scope of this PR, but we should keep it in mind to fix later.

Thank you for pointing that out — you're absolutely right.
I’ll refactor that in a future PR as well!
author_association: COLLABORATOR