diff --git a/src/app/(app)/settings/backoffice/shifts/page.tsx b/src/app/(app)/settings/backoffice/shifts/page.tsx new file mode 100644 index 0000000..9dc4bdc --- /dev/null +++ b/src/app/(app)/settings/backoffice/shifts/page.tsx @@ -0,0 +1,499 @@ +"use client"; + +import { AuthCheck } from "@/components/auth-check"; +import SettingsWrapper from "@/components/settings-wrapper"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { twMerge } from "tailwind-merge"; +import clsx from "clsx"; +import Card from "@/components/card"; +import { useEffect, useMemo, useState } from "react"; +import { useGetAllCourses } from "@/lib/queries/courses"; +import { ITimeSlot } from "@/lib/types"; +import { useDeleteTimeslot, useUpdateShift } from "@/lib/mutations/shifts"; +import { SubmitHandler, useForm } from "react-hook-form"; +import Input from "@/components/input"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; + +const getShortShiftType = (shiftType: string) => { + switch (shiftType) { + case "theoretical": + return "T"; + case "theoretical_practical": + return "TP"; + case "practical_laboratory": + return "PL"; + case "tutorial_guidance": + return "OT"; + default: + return shiftType; + } +}; + +interface IShiftsListbox { + selectedItem: string; + setSelectedItem: (item: string) => void; + collection: { id: string; name: string }[]; + label?: string; + className?: string; +} + +export function ShiftsListbox({ + selectedItem, + setSelectedItem, + collection, + label, + className, +}: IShiftsListbox) { + return ( + + +
+

{label}

+ {selectedItem ? ( + + {collection.find((item) => item.id === selectedItem)?.name} + + ) : ( + Select an item + )} +
+
+ + {collection.length === 0 ? ( +
+ No options available +
+ ) : ( + collection.map((item) => ( + +
+
+
+ + {item.name} + +
+ )) + )} +
+
+ ); +} + +export default function Shifts() { + const { data: courses } = useGetAllCourses(); + const updateShift = useUpdateShift(); + const deleteTimeslot = useDeleteTimeslot(); + + const [selectedCourse, setSelectedCourse] = useState(""); + const [selectedShift, setSelectedShift] = useState(""); + + const selectedCourseData = courses?.find( + (course) => course.id === selectedCourse, + ); + + const shifts = useMemo(() => { + return selectedCourseData?.shifts || []; + }, [selectedCourseData]); + + const selectedShiftData = shifts.find((shift) => shift.id === selectedShift); + const [timeslots, setTimeslots] = useState([]); + const [originalTimeslots, setOriginalTimeslots] = useState([]); + + const formSchema = z.object({ + type: z.string(), + number: z.number().min(1), + professor: z.string(), + }); + + type FormSchema = z.infer; + + const { register, handleSubmit, watch, setValue } = useForm({ + resolver: zodResolver(formSchema), + }); + + const onSubmit: SubmitHandler = (data: FormSchema) => { + updateShift.mutate({ + id: selectedShift, + type: data.type, + number: data.number, + professor: data.professor, + timeslots: timeslots, + }); + }; + + const updateTimeslot = ( + index: number, + field: keyof ITimeSlot, + value: string, + ) => { + const updatedTimeslots = [...timeslots]; + updatedTimeslots[index][field] = value; + setTimeslots(updatedTimeslots); + }; + + const addTimeslot = () => { + setTimeslots([ + ...timeslots, + { + id: crypto.randomUUID(), + weekday: "", + start: "", + end: "", + building: "", + room: "", + }, + ]); + }; + + const removeTimeslot = (id: string) => () => { + const isOriginalTimeslot = originalTimeslots.some((slot) => slot.id === id); + + if (isOriginalTimeslot) { + deleteTimeslot.mutate({ shiftId: selectedShift, timeslotId: id }); + } + + setTimeslots(timeslots.filter((slot) => slot.id !== id)); + }; + + useEffect(() => { + if (updateShift.isSuccess) { + const timer = setTimeout(() => { + updateShift.reset(); + }, 3000); + + return () => clearTimeout(timer); + } + }, [updateShift.isSuccess, updateShift]); + + const weekdays = [ + { id: "monday", name: "Monday" }, + { id: "tuesday", name: "Tuesday" }, + { id: "wednesday", name: "Wednesday" }, + { id: "thursday", name: "Thursday" }, + { id: "friday", name: "Friday" }, + { id: "saturday", name: "Saturday" }, + { id: "sunday", name: "Sunday" }, + ]; + + const orderedShifts = useMemo(() => { + const transformedShifts = shifts.map((shift) => ({ + id: shift.id, + name: `${getShortShiftType(shift.type)}${shift.number}`, + })); + + return transformedShifts.sort((a, b) => { + const numberA = parseInt(a.name.match(/\d+/g)?.[0] || "0", 10); + const numberB = parseInt(b.name.match(/\d+/g)?.[0] || "0", 10); + + return numberA - numberB; + }); + }, [shifts]); + + useEffect(() => { + if (selectedShiftData?.timeslots) { + setTimeslots(selectedShiftData.timeslots); + setOriginalTimeslots(selectedShiftData.timeslots); + } else { + setTimeslots([]); + setOriginalTimeslots([]); + } + if (selectedShiftData?.type) { + setValue("type", selectedShiftData.type); + } + if (selectedShiftData?.number) { + setValue("number", selectedShiftData.number); + } + if (selectedShiftData?.professor) { + setValue("professor", selectedShiftData.professor); + } + }, [selectedShiftData, setValue]); + + useEffect(() => { + setSelectedShift(""); + }, [selectedCourse]); + + useEffect(() => { + if (selectedCourse && !selectedShift && orderedShifts.length > 0) { + setSelectedShift(orderedShifts[0]?.id); + } + }, [selectedCourse, selectedShift, orderedShifts]); + + return ( + <> + Configurations | Pombo + + +
+
+

Shifts Editing

+

Here you can edit the shifts.

+
+
+
+

Select Course

+
+ + ({ + id: shift.id, + name: shift.name, + }))} + label="Shift" + /> +
+
+
+
+
+

Shift Details

+ {selectedShift ? ( +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + {timeslots.length > 0 ? ( +
+ {timeslots.map((timeslot, index) => ( +
+
+ + + updateTimeslot(index, "weekday", value) + } + collection={weekdays} + className="rounded-xl border-gray-200" + /> +
+ +
+ + + updateTimeslot( + index, + "start", + e.target.value, + ) + } + value={timeslot.start} + /> +
+ +
+ + + updateTimeslot( + index, + "end", + e.target.value, + ) + } + value={timeslot.end} + /> +
+ +
+ + + updateTimeslot( + index, + "building", + e.target.value, + ) + } + value={timeslot.building || ""} + /> +
+ +
+ + + updateTimeslot( + index, + "room", + e.target.value, + ) + } + value={timeslot.room || ""} + /> +
+ +
+ +
+
+ ))} +
+ ) : ( +

+ There are no timeslots for this shift. +

+ )} +
+
+ +
+ {timeslots.length === 0 && ( +

+ Warning: No timeslots defined for this shift. +

+ )} + {updateShift.isError && ( +

+ Invalid data +

+ )} + {updateShift.isSuccess && ( +

+ Shift updated successfully! +

+ )} +
+
+
+
+ ) : ( +

+ Select a shift to see details. +

+ )} +
+
+
+
+
+ + ); +} diff --git a/src/components/sidebar-settings.tsx b/src/components/sidebar-settings.tsx index d62abc0..8ec2c6b 100644 --- a/src/components/sidebar-settings.tsx +++ b/src/components/sidebar-settings.tsx @@ -73,6 +73,10 @@ export default function SidebarSettings() { > + + + + )} diff --git a/src/lib/mutations/shifts.ts b/src/lib/mutations/shifts.ts new file mode 100644 index 0000000..e051057 --- /dev/null +++ b/src/lib/mutations/shifts.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteTimeslot, updateShift } from "../shifts"; + +export function useUpdateShift() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: updateShift, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["shift"] }); + }, + }); +} + +export function useDeleteTimeslot() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: deleteTimeslot, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["shifts"] }); + }, + }); +} diff --git a/src/lib/queries/shifts.ts b/src/lib/queries/shifts.ts new file mode 100644 index 0000000..744b05b --- /dev/null +++ b/src/lib/queries/shifts.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { getShifts } from "../shifts"; + +export function useGetShifts() { + return useQuery({ + queryKey: ["shifts"], + queryFn: () => getShifts(), + }); +} diff --git a/src/lib/shifts.ts b/src/lib/shifts.ts new file mode 100644 index 0000000..31e3730 --- /dev/null +++ b/src/lib/shifts.ts @@ -0,0 +1,26 @@ +import { api } from "./api"; +import { ITimeSlot } from "./types"; + +export async function getShifts() { + return await api.get(`/shifts`); +} + +interface IPutShift { + id: string; + type?: string; + number?: number; + professor?: string; + timeslots?: ITimeSlot[]; + enrollment_status?: string | null; +} + +export async function updateShift(data: IPutShift) { + return await api.put(`/shifts/${data.id}`, data); +} + +export async function deleteTimeslot(data: { + shiftId: string; + timeslotId: string; +}) { + return await api.delete(`/timeslots/${data.timeslotId}`); +}