Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/backend/src/data-layer/utils/DateUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,33 @@ describe("DateUtils", () => {
)
})

it("returns dates that all fall on the correct UTC weekday", () => {
const semester = {
...semesterMock,
startDate: "2025-02-24",
endDate: "2025-05-30",
breakStart: "2025-04-14",
breakEnd: "2025-04-25",
}

const result = getWeeklySessionDates("friday" as Weekday, semester)

expect(result.length).toBeGreaterThan(0)
for (const date of result) {
expect(date.getUTCDay()).toBe(5) // 5 is friday index
}
})

it("returns dates with exactly 7 days between consecutive sessions", () => {
const result = getWeeklySessionDates("wednesday" as Weekday, semesterMock)

for (let i = 1; i < result.length; i++) {
const diff = result[i].getTime() - result[i - 1].getTime()
const daysDiff = diff / (1000 * 60 * 60 * 24)
expect(daysDiff === 7 || daysDiff > 7).toBe(true)
}
})

it("returns empty array if no sessions fall outside break", () => {
const semester = {
...semesterMock,
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/src/data-layer/utils/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export function getWeeklySessionDates(day: Weekday, semester: Semester): Date[]

const sessionDate = new Date(semesterStart)
const dayOffSet = getDaysBetweenWeekdays(Object.values(Weekday)[sessionDate.getUTCDay()], day)
sessionDate.setDate(sessionDate.getUTCDate() + dayOffSet)
sessionDate.setUTCDate(sessionDate.getUTCDate() + dayOffSet)

while (sessionDate <= semesterEnd) {
if (sessionDate < breakStart || sessionDate > breakEnd) {
dates.push(new Date(sessionDate))
}
sessionDate.setDate(sessionDate.getUTCDate() + 7)
sessionDate.setUTCDate(sessionDate.getUTCDate() + 7)
}

return dates
Expand All @@ -40,7 +40,7 @@ export function getWeeklySessionDates(day: Weekday, semester: Semester): Date[]
export function createGameSessionTimes(schedule: GameSessionSchedule, date: Date) {
const day = date.getUTCDate()
const month = date.getUTCMonth()
const year = date.getFullYear()
const year = date.getUTCFullYear()

const start = new Date(schedule.startTime)
const end = new Date(schedule.endTime)
Expand Down
65 changes: 65 additions & 0 deletions apps/backend/src/utils/date.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,78 @@ import {
getDateTimeStatus,
getDaysBetweenWeekdays,
getGameSessionOpenDay,
isoToTimeInput,
isSameDate,
timeInputToIso,
Weekday,
} from "@repo/shared"
import { semesterMock } from "@repo/shared/mocks"
import type { Semester } from "@repo/shared/payload-types"
import { vi } from "vitest"

describe("isoToTimeInput", () => {
it("should return empty string for undefined", () => {
expect(isoToTimeInput(undefined)).toBe("")
})

it("should return empty string for empty string", () => {
expect(isoToTimeInput("")).toBe("")
})

it("should return empty string for an invalid date string", () => {
expect(isoToTimeInput("not-a-date")).toBe("")
})

it("should format a UTC morning time correctly", () => {
expect(isoToTimeInput("2024-01-01T09:30:00.000Z")).toBe("09:30")
})

it("should format a UTC afternoon time correctly", () => {
expect(isoToTimeInput("2024-01-01T14:05:00.000Z")).toBe("14:05")
})

it("should format midnight correctly", () => {
expect(isoToTimeInput("2024-01-01T00:00:00.000Z")).toBe("00:00")
})

it("should format the end of day correctly", () => {
expect(isoToTimeInput("2024-01-01T23:59:00.000Z")).toBe("23:59")
})

it("should pad single-digit hours and minutes with a leading zero", () => {
expect(isoToTimeInput("2024-06-15T08:05:00.000Z")).toBe("08:05")
})

it("should use UTC time regardless of the date's timezone offset", () => {
expect(isoToTimeInput("2024-01-01T09:00:00+05:00")).toBe("04:00")
})
})

describe("timeInputToIso", () => {
it("should convert a morning time to ISO string", () => {
expect(timeInputToIso("09:30")).toBe("1970-01-01T09:30:00.000Z")
})

it("should convert an afternoon time to ISO string", () => {
expect(timeInputToIso("14:05")).toBe("1970-01-01T14:05:00.000Z")
})

it("should convert midnight to ISO string", () => {
expect(timeInputToIso("00:00")).toBe("1970-01-01T00:00:00.000Z")
})

it("should convert end of day to ISO string", () => {
expect(timeInputToIso("23:59")).toBe("1970-01-01T23:59:00.000Z")
})

it("should be the inverse of isoToTimeInput", () => {
const times = ["00:00", "09:30", "12:00", "14:05", "23:59"]
for (const time of times) {
expect(isoToTimeInput(timeInputToIso(time))).toBe(time)
}
})
})

describe("getDaysBetweenWeekdays", () => {
it("should return 0 when fromDay and toDay are the same", () => {
expect(getDaysBetweenWeekdays(Weekday.monday, Weekday.monday)).toBe(0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Popup } from "@repo/shared/enums"
import type { GameSessionSchedule, Semester } from "@repo/shared/payload-types"
import type { CreateGameSchedulePopUpFormValues } from "@repo/shared/schemas"
import type { CreateSemesterData } from "@repo/shared/types"
import { CreateSemesterPopUpFlow } from "@repo/ui/components/Generic"
import { timeInputToIso } from "@repo/shared/utils"
import { CreateGameSchedulePopUp, CreateSemesterPopUpFlow } from "@repo/ui/components/Generic"
import { Accordion, Button, Center, Dialog, useDisclosure, useNotice } from "@yamada-ui/react"
import { parseAsBoolean, useQueryState } from "nuqs"
import { useCallback, useState } from "react"
import { useDeleteGameSessionSchedule } from "@/services/admin/game-session-schedule/AdminGameSessionScheduleMutations"
import {
useCreateGameSessionSchedule,
useDeleteGameSessionSchedule,
useUpdateGameSessionSchedule,
} from "@/services/admin/game-session-schedule/AdminGameSessionScheduleMutations"
import {
useCreateSemester,
useDeleteSemester,
useUpdateSemester,
} from "@/services/admin/semester/AdminSemesterMutations"
import { useGetAllSemesters } from "@/services/semester/SemesterQueries"
import { SemesterScheduleAccordionItem } from "./SemesterScheduleAccordionItem"
Expand Down Expand Up @@ -79,6 +87,156 @@ export const AdminSemesters = () => {
[createSemesterMutation, notice],
)

// Edit semester logic
const [semesterToEdit, setSemesterToEdit] = useState<Semester | null>(null)
const {
open: openUpdateSemester,
onOpen: onOpenUpdateSemester,
onClose: onCloseUpdateSemester,
} = useDisclosure()
const updateSemesterMutation = useUpdateSemester()
const handleEditSemester = useCallback(
(semester: Semester) => {
setSemesterToEdit(semester)
onOpenUpdateSemester()
},
[onOpenUpdateSemester],
)
const handleEditSemesterConfirm = useCallback(
(data: CreateSemesterData) => {
if (!semesterToEdit) {
return
}
updateSemesterMutation.mutate(
{
id: semesterToEdit.id,
data,
},
{
onSuccess: () => {
notice({
title: "Update successful",
description: "Semester has been updated",
status: "success",
})
onCloseUpdateSemester()
setSemesterToEdit(null)
},
onError: () => {
notice({
title: "Update failed",
description: "Failed to update semester",
status: "error",
})
},
},
)
},
[updateSemesterMutation, semesterToEdit, notice, onCloseUpdateSemester],
)

// Create game session schedule logic
const [scheduleTargetSemesterId, setScheduleTargetSemesterId] = useState<string | null>(null)
const {
open: openCreateSchedule,
onOpen: onOpenCreateSchedule,
onClose: onCloseCreateSchedule,
} = useDisclosure()
const createScheduleMutation = useCreateGameSessionSchedule()
const handleAddSchedule = useCallback(
(semesterId: string) => {
setScheduleTargetSemesterId(semesterId)
onOpenCreateSchedule()
},
[onOpenCreateSchedule],
)
const handleCreateScheduleConfirm = useCallback(
(data: CreateGameSchedulePopUpFormValues) => {
if (!scheduleTargetSemesterId) {
return
}
createScheduleMutation.mutate(
{
...data,
semester: scheduleTargetSemesterId,
startTime: timeInputToIso(data.startTime),
endTime: timeInputToIso(data.endTime),
},
{
onSuccess: () => {
notice({
title: "Creation successful",
description: "Session schedule has been created",
status: "success",
})
onCloseCreateSchedule()
setScheduleTargetSemesterId(null)
},
onError: () => {
notice({
title: "Creation failed",
description: "Failed to create session schedule",
status: "error",
})
},
},
)
},
[createScheduleMutation, scheduleTargetSemesterId, notice, onCloseCreateSchedule],
)

// Edit game session schedule logic
const [scheduleToEdit, setScheduleToEdit] = useState<GameSessionSchedule | null>(null)
const {
open: openEditSchedule,
onOpen: onOpenEditSchedule,
onClose: onCloseEditSchedule,
} = useDisclosure()
const updateScheduleMutation = useUpdateGameSessionSchedule()
const handleEditSchedule = useCallback(
(schedule: GameSessionSchedule) => {
setScheduleToEdit(schedule)
onOpenEditSchedule()
},
[onOpenEditSchedule],
)
const handleEditScheduleConfirm = useCallback(
(data: CreateGameSchedulePopUpFormValues) => {
if (!scheduleToEdit) {
return
}
updateScheduleMutation.mutate(
{
id: scheduleToEdit.id,
data: {
...data,
startTime: timeInputToIso(data.startTime),
endTime: timeInputToIso(data.endTime),
},
},
{
onSuccess: () => {
notice({
title: "Update successful",
description: "Session schedule has been updated",
status: "success",
})
onCloseEditSchedule()
setScheduleToEdit(null)
},
onError: () => {
notice({
title: "Update failed",
description: "Failed to update session schedule",
status: "error",
})
},
},
)
},
[updateScheduleMutation, scheduleToEdit, notice, onCloseEditSchedule],
)

// Delete game session logic
const deleteScheduleMutation = useDeleteGameSessionSchedule()
const handleDeleteSchedule = useCallback(
Expand Down Expand Up @@ -127,11 +285,14 @@ export const AdminSemesters = () => {
<SemesterScheduleAccordionItem
enabled={expandedIndexes.has(index)}
key={sem.id}
onAddSchedule={() => handleAddSchedule(sem.id)}
onDeleteSchedule={handleDeleteSchedule}
onDeleteSemester={(semId) => {
setSemesterToDelete(semId)
onOpenDeleteSemester()
}}
onEditSchedule={handleEditSchedule}
onEditSemester={handleEditSemester}
semester={sem}
/>
))
Expand All @@ -140,13 +301,48 @@ export const AdminSemesters = () => {
)}
</Accordion>
{/* Action flows and confirmations */}
<CreateGameSchedulePopUp
key={scheduleTargetSemesterId}
onClose={onCloseCreateSchedule}
onConfirm={handleCreateScheduleConfirm}
open={openCreateSchedule}
/>
<CreateGameSchedulePopUp
key={scheduleToEdit?.id}
onClose={() => {
onCloseEditSchedule()
setScheduleToEdit(null)
}}
onConfirm={handleEditScheduleConfirm}
open={openEditSchedule}
scheduleToEdit={scheduleToEdit} // Editing schedule
title="Edit Game Schedule"
/>
<CreateSemesterPopUpFlow
onClose={() => {
setOpenCreateSemester(false)
}}
onComplete={handleCreateSemester}
open={!!openCreateSemester}
/>
<CreateSemesterPopUpFlow
confirmationTitle="Semester Edit Confirmation"
initialValues={
semesterToEdit
? {
...semesterToEdit,
}
: undefined
}
key={semesterToEdit?.id}
nameStepTitle="Edit Semester"
onClose={() => {
onCloseUpdateSemester()
setSemesterToEdit(null)
}}
onComplete={handleEditSemesterConfirm}
open={openUpdateSemester}
/>
<Dialog
cancel="Cancel"
header="Delete Semester"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface SemesterScheduleAccordionItemProps {
/**
* Callback function to edit the semester.
*/
onEditSemester?: (semesterId: string) => void
onEditSemester?: (semester: Semester) => void
/**
* Callback function to delete the semester by its ID.
*/
Expand Down
Loading
Loading