diff --git a/dev.sh b/dev.sh index 35e7fa2e..bf0ed298 100755 --- a/dev.sh +++ b/dev.sh @@ -1,4 +1,4 @@ #!/bin/bash set -e # abort script if any command fails -docker compose up $1 tts-frontend +docker compose up $1 tts-frontend \ No newline at end of file diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index cc0a1a10..ac0a7dd8 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -132,6 +132,7 @@ export type MarketplaceRequest = { classes?: Array, pending_motive?: DirectExchangePendingMotive, accepted: boolean, + admin_state: string, canceled: boolean } diff --git a/src/api/services/exchangeRequestService.ts b/src/api/services/exchangeRequestService.ts index e60aa281..a79d5bc3 100644 --- a/src/api/services/exchangeRequestService.ts +++ b/src/api/services/exchangeRequestService.ts @@ -46,7 +46,7 @@ const submitExchangeRequest = async (requests: Map, u const retrieveMarketplaceRequest = async (url: string): Promise => { return fetch(url).then(async (res) => { const json = await res.json(); - return json.data; + return json; }).catch((e) => { console.error(e); return []; diff --git a/src/components/admin/AdminMainContent.tsx b/src/components/admin/AdminMainContent.tsx index ef279fa1..1a0ecb32 100644 --- a/src/components/admin/AdminMainContent.tsx +++ b/src/components/admin/AdminMainContent.tsx @@ -8,6 +8,7 @@ import { AdminRequestState } from "../../contexts/admin/RequestFiltersContext"; import RequestFiltersContext from "../../contexts/admin/RequestFiltersContext"; import { AdminPagination } from "./AdminPagination"; import AdminPaginationContext from "../../contexts/admin/AdminPaginationContext"; +import { AdminMarketplaceExhanges } from "./requests/AdminMarketplaceExhanges"; export const AdminMainContent = () => { const [activeCourse, setActiveCourse] = useState(undefined); @@ -39,24 +40,30 @@ export const AdminMainContent = () => { - setCurrPage(1)} > Trocas entre estudantes - setCurrPage(1)} > Trocas individuais - setCurrPage(1)} > Inscrições + setCurrPage(1)} + > + Marketplace + @@ -67,6 +74,9 @@ export const AdminMainContent = () => { + + +
diff --git a/src/components/admin/AdminMarketplaceExchangesCard.tsx b/src/components/admin/AdminMarketplaceExchangesCard.tsx new file mode 100644 index 00000000..b01b2bcb --- /dev/null +++ b/src/components/admin/AdminMarketplaceExchangesCard.tsx @@ -0,0 +1,126 @@ +import { useState } from "react" +import { Button } from "../ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" +import { ExchangeStatus } from "./requests/cards/ExchangeStatus" +import { Person } from "./requests/cards/Person" +import { RequestDate } from "./requests/cards/RequestDate" +import { ArrowRightIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { AdminPreviewSchedule } from "./requests/AdminPreviewSchedule" +import useStudentsSchedule from "../../hooks/admin/useStudentsSchedule" +import { ClassDescriptor, MarketplaceRequest } from "../../@types" +import { AdminRequestCardFooter } from "./requests/cards/AdminRequestCardFooter" +import { listEmailExchanges } from "../../utils/mail" +import { AdminRequestType } from "../../utils/exchange" + +type Props = { + exchange: MarketplaceRequest +} + +export const AdminMarketplaceExhangesCard = ({ + exchange +}: Props) => { + const [open, setOpen] = useState(false); + const [exchangeState, setExchangeState] = useState(exchange); + + const { schedule } = useStudentsSchedule(exchange.issuer_nmec); + + return (<> + + +
+
+
+ +

+ {`#${exchange.id}`} +

+
+ +
+ +
+ {!open && <> + + } + +
+
+ +
+
+ + + {open && +
+
+ +
+
+ <>{exchange.options.map((option) => ( +
+

{option.course_info.acronym}

+
+

{option.class_issuer_goes_from.name}

+ +

{option.class_issuer_goes_to.name}

+
+
+ ))} + +
+
+
+ { + return { + classInfo: option.class_issuer_goes_to, + courseInfo: option.course_info, + slotInfo: null + } + }) + } + + /> +
+
+
+ } +
+ + {open && + ({ + participant_name: undefined, + participant_nmec: exchange.issuer_nmec, + goes_from: option.class_issuer_goes_from?.name, + goes_to: option.class_issuer_goes_to.name, + course_acronym: option.course_info.acronym + })) + )} + requestType={AdminRequestType.URGENT_EXCHANGE} + requestId={exchange.id} + setExchange={setExchangeState} + courseId={exchange.options.map(option => option.course_info.course)} + /> + } +
+ + ) +} \ No newline at end of file diff --git a/src/components/admin/requests/AdminMarketplaceExhanges.tsx b/src/components/admin/requests/AdminMarketplaceExhanges.tsx new file mode 100644 index 00000000..97ae4fc2 --- /dev/null +++ b/src/components/admin/requests/AdminMarketplaceExhanges.tsx @@ -0,0 +1,24 @@ +import { useContext } from "react"; +import RequestFiltersContext from "../../../contexts/admin/RequestFiltersContext"; +import useAdminAllMarketplaceExchanges from "../../../hooks/admin/useAdminAllMarketplaceExchanges" +import AdminPaginationContext from "../../../contexts/admin/AdminPaginationContext"; +import { BarLoader } from "react-spinners"; +import { AdminMarketplaceExhangesCard } from "../AdminMarketplaceExchangesCard"; + +export const AdminMarketplaceExhanges = () => { + const filterContext = useContext(RequestFiltersContext); + const { currPage } = useContext(AdminPaginationContext); + const { exchanges, loading } = useAdminAllMarketplaceExchanges(filterContext, currPage); + + return (<> + {loading && } + + {exchanges?.map((exchange) => ( + + ))} + + ) +} \ No newline at end of file diff --git a/src/components/admin/requests/AdminSendEmail.tsx b/src/components/admin/requests/AdminSendEmail.tsx index 602747a1..2581b016 100644 --- a/src/components/admin/requests/AdminSendEmail.tsx +++ b/src/components/admin/requests/AdminSendEmail.tsx @@ -16,7 +16,11 @@ export const AdminSendEmail = ({ message="" }: Props) => { return <> - + diff --git a/src/components/exchange/enrollments/Enrollments.tsx b/src/components/exchange/enrollments/Enrollments.tsx index 4e2d2330..f9dcb4e5 100644 --- a/src/components/exchange/enrollments/Enrollments.tsx +++ b/src/components/exchange/enrollments/Enrollments.tsx @@ -9,7 +9,6 @@ import { Button } from "../../ui/button"; import courseUnitEnrollmentService from "../../../api/services/courseUnitEnrollmentService"; import { ExchangeSidebarStatus } from "../../../pages/Exchange" import { useToast } from "../../ui/use-toast"; -import useLocalStorage from "../../../hooks/useLocalStorage"; import useStudentCourseUnits from "../../../hooks/useStudentCourseUnits"; import { AlreadyEnrolledCourseUnitCard } from "./AlreadyEnrolledCourseUnitCard"; import { EnrollingCourseUnitCard } from "./EnrollingCourseUnitCard"; @@ -32,7 +31,7 @@ export const Enrollments = ({ }: Props) => { const parentCourseContext = useContext(CourseContext); - const [enrollCourses, setEnrollCourses] = useLocalStorage("enrollCourses", []); + const [enrollCourses, setEnrollCourses] = useState([]); const [enrollmentChoices, setEnrollmentChoices] = useState>(new Map()); const [disenrollmentChoices, setDisenrollmentChoices] = useState>(new Map()); const [coursesInfo, setCoursesInfo] = useState([]); @@ -112,7 +111,7 @@ export const Enrollments = ({ ))}
- {(enrollmentChoices.size > 0) && + {(enrollmentChoices.size > 0 || disenrollmentChoices.size > 0) &&
{ e.preventDefault(); @@ -125,6 +124,8 @@ export const Enrollments = ({ description: 'Pedido de inscrição submetida com sucesso', }); setEnrollCourses([]); + setEnrollmentChoices(new Map()); + setDisenrollmentChoices(new Map()); } else { const json = await res.json(); toast({ diff --git a/src/components/exchange/requests/issue/CustomizeRequest.tsx b/src/components/exchange/requests/issue/CustomizeRequest.tsx index e4a60349..515d6c76 100644 --- a/src/components/exchange/requests/issue/CustomizeRequest.tsx +++ b/src/components/exchange/requests/issue/CustomizeRequest.tsx @@ -33,6 +33,7 @@ export const CustomizeRequest = ({ const [submittingRequest, setSubmittingRequest] = useState(false); const [previewingForm, setPreviewingForm] = useState(false); + const submitRequest = async (urgentMessage: string) => { setSubmittingRequest(true); const res = await exchangeRequestService.submitExchangeRequest(requests, urgentMessage); diff --git a/src/components/exchange/requests/issue/PreviewRequestForm.tsx b/src/components/exchange/requests/issue/PreviewRequestForm.tsx index 739ac881..7db5eda1 100644 --- a/src/components/exchange/requests/issue/PreviewRequestForm.tsx +++ b/src/components/exchange/requests/issue/PreviewRequestForm.tsx @@ -17,6 +17,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormField, FormItem, FormMessage } from "../../../ui/form"; import ConflictsContext from "../../../../contexts/ConflictsContext"; +import exchangeRequestService from "../../../../api/services/exchangeRequestService"; type Props = { requests: Map @@ -49,7 +50,6 @@ const PreviewRequestForm = ({ requests, requestSubmitHandler, previewingFormHook @@ -87,13 +87,17 @@ const PreviewRequestForm = ({ requests, requestSubmitHandler, previewingFormHook {requests.size > 0 && -
+ {!exchangeRequestService.isDirectExchange(requests.values()) &&
setSendUrgentMessage(checked)} + onCheckedChange={(checked: boolean) => { + setSendUrgentMessage(checked) + if(!checked) form.setValue("urgentMessage", "") + }} />

O meu pedido é urgente por razões médicas ou outras

+ } {sendUrgentMessage && {submittingRequest ?

A processar pedido...

diff --git a/src/components/exchange/requests/view/ViewRequests.tsx b/src/components/exchange/requests/view/ViewRequests.tsx index 15ab336e..cbf735ca 100644 --- a/src/components/exchange/requests/view/ViewRequests.tsx +++ b/src/components/exchange/requests/view/ViewRequests.tsx @@ -1,5 +1,5 @@ import { PlusIcon } from "@heroicons/react/24/outline"; -import { Dispatch, SetStateAction, useContext, useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction, useContext, useRef, useState } from "react"; import { DirectExchangeRequest, MarketplaceRequest } from "../../../../@types"; import ScheduleContext from "../../../../contexts/ScheduleContext"; import useMarketplaceRequests from "../../../../hooks/useMarketplaceRequests"; @@ -13,6 +13,7 @@ import { ReceivedRequestCard } from "./cards/ReceivedRequestCard"; import { RequestCard } from "./cards/RequestCard"; import { ViewRequestsFilters } from "./ViewRequestsFilters"; import { ExchangeSidebarStatus } from "../../../../pages/Exchange"; +import { MoonLoader } from "react-spinners"; type Props = { setExchangeSidebarStatus: Dispatch> @@ -53,6 +54,34 @@ const RequestCardSkeletons = () => { } +const ViewMoreButton = ({ + hasNext, + setSize, + size, + isValidating +}: { + hasNext: boolean + setSize: Dispatch> + size: number + isValidating: boolean +}) => { + return <> + {hasNext && + + } + +} + export const ViewRequests = ({ setExchangeSidebarStatus }: Props) => { @@ -66,31 +95,10 @@ export const ViewRequests = ({ // This is to keep track of the request of the request card that is currently open const [chosenRequest, setChosenRequest] = useState(null); - const { data, size, setSize, isLoading } = useMarketplaceRequests( + const { requests, size, setSize, isLoading, hasNext, isValidating } = useMarketplaceRequests( filterCourseUnitNames, requestTypeFilters[currentRequestTypeFilter], classesFilter ); - const requests = data ? [].concat(...data) : []; - - const onScroll = () => { - if (!requestCardsContainerRef.current) return; - - if ((requestCardsContainerRef.current.scrollHeight - requestCardsContainerRef.current.scrollTop) - <= requestCardsContainerRef.current.clientHeight + 100 - ) { - setSize(size + 1); - } - } - - useEffect(() => { - if (!requestCardsContainerRef.current) return; - - requestCardsContainerRef.current.addEventListener('scroll', onScroll); - return () => { - if (requestCardsContainerRef.current) requestCardsContainerRef.current.removeEventListener('scroll', onScroll); - } - }, []); - return

Pedidos

@@ -138,6 +146,12 @@ export const ViewRequests = ({ ))} + } @@ -170,6 +184,12 @@ export const ViewRequests = ({ ))} + }
@@ -198,6 +218,12 @@ export const ViewRequests = ({ /> ))} + }
diff --git a/src/components/exchange/requests/view/cards/CommonCardHeader.tsx b/src/components/exchange/requests/view/cards/CommonCardHeader.tsx index 33e8e367..94c88548 100644 --- a/src/components/exchange/requests/view/cards/CommonCardHeader.tsx +++ b/src/components/exchange/requests/view/cards/CommonCardHeader.tsx @@ -106,7 +106,7 @@ export const CommonCardHeader = ({
{request.options?.map((option) => { return ( { if (updatedOptions.get(option.course_info.acronym) === true) { const matchingClass = (type === "directexchange" ? option.class_participant_goes_to : option.class_issuer_goes_from); + if(!matchingClass) return; matchingClass.slots.forEach((slot) => { newExchangeSchedule.push({ courseInfo: option.course_info, diff --git a/src/components/exchange/requests/view/cards/ListRequestChanges.tsx b/src/components/exchange/requests/view/cards/ListRequestChanges.tsx index b51ecbf4..f7f03a25 100644 --- a/src/components/exchange/requests/view/cards/ListRequestChanges.tsx +++ b/src/components/exchange/requests/view/cards/ListRequestChanges.tsx @@ -79,8 +79,8 @@ export const ListRequestChanges = ({ - {type === "directexchange" ? (option as DirectExchangeParticipant).class_participant_goes_from.name : (option as ExchangeOption).class_issuer_goes_from.name} - {type === "directexchange" ? (option as DirectExchangeParticipant).class_participant_goes_to.name : (option as ExchangeOption).class_issuer_goes_to.name} + {type === "directexchange" ? (option as DirectExchangeParticipant).class_participant_goes_from.name : (option as ExchangeOption).class_issuer_goes_from?.name} + {type === "directexchange" ? (option as DirectExchangeParticipant).class_participant_goes_to.name : (option as ExchangeOption).class_issuer_goes_to?.name} diff --git a/src/components/exchange/requests/view/cards/RequestCard.tsx b/src/components/exchange/requests/view/cards/RequestCard.tsx index 36ad22c6..f05fd563 100644 --- a/src/components/exchange/requests/view/cards/RequestCard.tsx +++ b/src/components/exchange/requests/view/cards/RequestCard.tsx @@ -96,7 +96,7 @@ export const RequestCard = () => { {request.options?.map((option) => ( 1; + const {originalExchangeSchedule} = useContext(ScheduleContext); // Needs to change the entry with the id of this lesson to contain the correct ConflictInfo when the classes change useEffect(() => { @@ -69,7 +71,13 @@ const LessonBox = ({ } } } + + const hasNewClasses = !newConflictInfo.conflictingClasses.every((conflictingClass) => originalExchangeSchedule.some((originalClass) => originalClass.classInfo.id === conflictingClass.classInfo.id)); + if(!hasNewClasses && newConflictInfo.severe) { + newConflictInfo.severe = false; + } + setConflict(newConflictInfo); }, [classInfo, classes, hasConflict]); diff --git a/src/components/planner/sidebar/CoursesController/ClassItem.tsx b/src/components/planner/sidebar/CoursesController/ClassItem.tsx index c6a17668..0c88059b 100644 --- a/src/components/planner/sidebar/CoursesController/ClassItem.tsx +++ b/src/components/planner/sidebar/CoursesController/ClassItem.tsx @@ -43,7 +43,7 @@ const ClassItem = ({ course_id, classInfo, onSelect, onMouseEnter, onMouseLeave let maxSeverity = 0; for (const otherClass of otherClasses) { - maxSeverity = Math.max(maxSeverity, classesConflictSeverity(classInfo, otherClass)); + maxSeverity = Math.max(maxSeverity, classesConflictSeverity(classInfo, otherClass)); } return maxSeverity; diff --git a/src/components/planner/sidebar/sessionController/ExchangeCoursePicker.tsx b/src/components/planner/sidebar/sessionController/ExchangeCoursePicker.tsx index bad77e69..de3d207b 100644 --- a/src/components/planner/sidebar/sessionController/ExchangeCoursePicker.tsx +++ b/src/components/planner/sidebar/sessionController/ExchangeCoursePicker.tsx @@ -15,7 +15,7 @@ export const ExchangeCoursePicker = ({ enrollCourses, setEnrollCourses }: Props) => { - const [checkboxedCourses, setCheckboxedCourses] = useLocalStorage("enrollCoursesCheckboxedCourses", []); + const [checkboxedCourses, setCheckboxedCourses] = useState([]); const [coursesInfo, setCoursesInfo] = useState([]); const [modalOpen, setModalOpen] = useState(false) const [selectedMajor, setSelectedMajor] = useLocalStorage("enrollMajor",null); diff --git a/src/hooks/admin/useAdminAllMarketplaceExchanges.tsx b/src/hooks/admin/useAdminAllMarketplaceExchanges.tsx new file mode 100644 index 00000000..5a972b37 --- /dev/null +++ b/src/hooks/admin/useAdminAllMarketplaceExchanges.tsx @@ -0,0 +1,42 @@ +import { useMemo } from "react"; +import api from "../../api/backend"; +import useSWR from "swr"; +import { RequestFiltersContextContent } from "../../contexts/admin/RequestFiltersContext"; +import { buildUrlWithFilterParams } from "../../utils/admin/filters"; + +/** + * Gets the exchanges that a student made not involving any other student. +*/ +export default (filterContext: RequestFiltersContextContent, pageIndex: number) => { + const getExchanges = async (url: string) => { + try { + const res = await fetch(url, { + credentials: "include" + }); + + if(res.ok) { + return await res.json(); + } + } catch (error) { + console.error(error); + } + }; + + const { data, error, mutate } = useSWR( + buildUrlWithFilterParams(`${api.BACKEND_URL}/exchange/admin/marketplace?page=${pageIndex}`, filterContext), + getExchanges + ); + + const exchanges = useMemo(() => data ? [].concat(...data["exchanges"]) : null, [data]); + const totalPages = useMemo(() => data ? data["total_pages"] : null, [data]); + + return { + exchanges, + totalPages, + error, + loading: !data, + mutate, + }; +}; + + diff --git a/src/hooks/admin/useStudentsSchedule.tsx b/src/hooks/admin/useStudentsSchedule.tsx index fdaad7cb..1974c129 100644 --- a/src/hooks/admin/useStudentsSchedule.tsx +++ b/src/hooks/admin/useStudentsSchedule.tsx @@ -21,7 +21,7 @@ export default (nmec: string) => { } - const { data, error, mutate, isValidating } = useSWR("schedule", getSchedule, {}); + const { data, error, mutate, isValidating } = useSWR("schedule-" + nmec, getSchedule, {}); const schedule = useMemo(() => data ? data.schedule : null, [data]); const sigarraSynced = data ? data.noChanges : null; diff --git a/src/hooks/useMarketplaceAcceptExchange.tsx b/src/hooks/useMarketplaceAcceptExchange.tsx index c660caf2..7ba79e7e 100644 --- a/src/hooks/useMarketplaceAcceptExchange.tsx +++ b/src/hooks/useMarketplaceAcceptExchange.tsx @@ -17,8 +17,8 @@ export default (request: MarketplaceRequest | DirectExchangeRequest, selectedOpt { courseUnitId: option.course_info.id, courseUnitName: option.course_info.name, - classNameRequesterGoesTo: (option as ExchangeOption).class_issuer_goes_from.name, - classNameRequesterGoesFrom: (option as ExchangeOption).class_issuer_goes_to.name, + classNameRequesterGoesTo: (option as ExchangeOption).class_issuer_goes_from?.name, + classNameRequesterGoesFrom: (option as ExchangeOption).class_issuer_goes_to?.name, other_student: { name: request.issuer_name, mecNumber: request.issuer_nmec diff --git a/src/hooks/useMarketplaceRequests.tsx b/src/hooks/useMarketplaceRequests.tsx index abf5c786..c9ca3a56 100644 --- a/src/hooks/useMarketplaceRequests.tsx +++ b/src/hooks/useMarketplaceRequests.tsx @@ -2,6 +2,7 @@ import useSWRInfinite from "swr/infinite"; import { MarketplaceRequest } from "../@types"; import api from "../api/backend"; import exchangeRequestService from "../api/services/exchangeRequestService"; +import { useEffect, useState } from "react"; const getUrl = (requestType: string) => { switch (requestType) { @@ -22,6 +23,8 @@ const getUrl = (requestType: string) => { * */ export default (courseUnitNameFilter: Set, requestType: string, classesFilter: Map>) => { + const [hasNext, setHasNext] = useState(true); + const classesFilterArray = Array.from(classesFilter, ([key, value]) => [key, Array.from(value)]); const classesFilterBase64 = btoa(JSON.stringify(classesFilterArray)); const filters = `courseUnitNameFilter=${Array.from(courseUnitNameFilter).join(",")}&classesFilter=${classesFilterBase64}`; @@ -37,8 +40,18 @@ export default (courseUnitNameFilter: Set, requestType: string, classesF exchangeRequestService.retrieveMarketplaceRequest ); + + const requests = data ? [].concat(...data.map((el) => el["data"])) : []; + + useEffect(() => { + if(data) { + setHasNext(data[data.length - 1]["page"]["has_next"]); + } + }, [data]) + return { - data, + requests, + hasNext, isLoading, size, isValidating, diff --git a/src/utils/index.ts b/src/utils/index.ts index f0f80a4c..9002b7ab 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -332,6 +332,9 @@ const getAllPickedSlots = (selected_courses: PickedCourses, option: Option) => { if (!course.picked_class_id) return [] const courseInfo = selected_courses.find((selected_course) => selected_course.id === course.course_id) const classInfo = courseInfo.classes.find((classInfo) => classInfo.id === course.picked_class_id) + + if (!classInfo) return []; + return classInfo.slots }) }