diff --git a/backend/src/Router.ts b/backend/src/Router.ts index 1e853bd..d5e398d 100644 --- a/backend/src/Router.ts +++ b/backend/src/Router.ts @@ -11,7 +11,6 @@ import TrainingRequestController from "./controllers/training-request/TrainingRe import TrainingTypeController from "./controllers/training-type/TrainingTypeController"; import UserInformationAdminController from "./controllers/user/UserInformationAdminController"; import UserNoteAdminController from "./controllers/user/UserNoteAdminController"; -import UserController from "./controllers/user/UserAdminController"; import TrainingRequestAdminController from "./controllers/training-request/TrainingRequestAdminController"; import TrainingTypeAdministrationController from "./controllers/training-type/TrainingTypeAdminController"; import FastTrackAdministrationController from "./controllers/fast-track/FastTrackAdminController"; @@ -38,6 +37,7 @@ import SyslogAdminController from "./controllers/admin-logs/SyslogAdminControlle import JoblogAdminController from "./controllers/admin-logs/JoblogAdminController"; import UserInformationController from "./controllers/user/UserInformationController"; import CourseInformationAdministrationController from "./controllers/course/CourseInformationAdministrationController"; +import UserAdminController from "./controllers/user/UserAdminController"; export const routerGroup = (callback: (router: Router) => void) => { const router = Router(); @@ -180,9 +180,9 @@ router.use( r.get("/notes", UserNoteAdminController.getGeneralUserNotes); r.get("/notes/course", UserNoteAdminController.getNotesByCourseID); - r.get("/", UserController.getAll); - r.get("/min", UserController.getAllUsersMinimalData); - r.get("/sensitive", UserController.getAllSensitive); + r.get("/", UserAdminController.getAll); + r.get("/min", UserAdminController.getAllUsersMinimalData); + r.get("/sensitive", UserAdminController.getAllSensitive); r.post("/enrol", UserCourseAdminController.enrolUser); diff --git a/backend/src/controllers/user/UserAdminController.ts b/backend/src/controllers/user/UserAdminController.ts index 80ddd4b..84a4186 100644 --- a/backend/src/controllers/user/UserAdminController.ts +++ b/backend/src/controllers/user/UserAdminController.ts @@ -1,5 +1,7 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { User } from "../../models/User"; +import { ConversionUtils } from "turbocommons-ts"; +import PermissionHelper from "../../utility/helper/PermissionHelper"; /** * Gets all users including their user data (VATSIM Data) @@ -29,15 +31,39 @@ async function getAllSensitive(request: Request, response: Response) { /** * Gets all users with minimal data only (CID, Name) + * If the optional ?users= parameter is given, the users are filtered by this. + * Note that the base64 string consists of JSON.stringify([..., cid, ...]) * @param request * @param response + * @param next */ -async function getAllUsersMinimalData(request: Request, response: Response) { - const users = await User.findAll({ - attributes: ["id", "first_name", "last_name"], - }); +async function getAllUsersMinimalData(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const query = request.query as { users?: string }; - response.send(users); + PermissionHelper.checkUserHasPermission(user, "mentor.view"); + + let users: User[]; + + if (query.users == null) { + users = await User.findAll({ + attributes: ["id", "first_name", "last_name"], + }); + } else { + const userIds = JSON.parse(ConversionUtils.base64ToString(query.users)) as number[]; + users = await User.findAll({ + where: { + id: userIds, + }, + attributes: ["id", "first_name", "last_name"], + }); + } + + response.send(users); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/user/UserCourseAdminController.ts b/backend/src/controllers/user/UserCourseAdminController.ts index 4a84ada..9f64675 100644 --- a/backend/src/controllers/user/UserCourseAdminController.ts +++ b/backend/src/controllers/user/UserCourseAdminController.ts @@ -83,7 +83,7 @@ async function enrolUser(request: Request, response: Response, next: NextFunctio user_id: Number(body.user_id), course_id: Number(body.course_id), completed: false, - next_training_type: course.training_type?.id ?? null, + next_training_type: course.initial_training_type ?? null, }, }); diff --git a/frontend/src/pages/administration/mentor/request/open-request-list/OpenTrainingRequestList.tsx b/frontend/src/pages/administration/mentor/request/open-request-list/OpenTrainingRequestList.tsx index 6bbf496..3171088 100644 --- a/frontend/src/pages/administration/mentor/request/open-request-list/OpenTrainingRequestList.tsx +++ b/frontend/src/pages/administration/mentor/request/open-request-list/OpenTrainingRequestList.tsx @@ -16,6 +16,8 @@ import { Card } from "@/components/ui/Card/Card"; import { Tabs } from "@/components/ui/Tabs/Tabs"; import { Badge } from "@/components/ui/Badge/Badge"; import useApi from "@/utils/hooks/useApi"; +import OTRLessonListTypes from "@/pages/administration/mentor/request/open-request-list/_types/OTRLessonList.types"; +import { ORLLessonSubpage } from "@/pages/administration/mentor/request/open-request-list/subpages/ORLLesson.subpage"; type SearchFilter = { modal_open: boolean; @@ -45,7 +47,6 @@ export function OpenTrainingRequestList() { const [searchInputValue, setSearchInputValue] = useState(""); const debouncedInput = useDebounce(searchInputValue, 250); - const columns: TableColumn[] = OpenRequestListTypes.getColumns(navigate); const filteredTrainingRequests = useFilter(trainingRequests ?? [], searchInputValue, debouncedInput, filterTrainingRequestFunction); return ( @@ -95,18 +96,12 @@ export function OpenTrainingRequestList() { paginate paginationPerPage={15} className={"mt-5"} - columns={columns} + columns={OpenRequestListTypes.getColumns(navigate)} data={filteredTrainingRequests.filter((f: TrainingRequestModel) => f.training_type?.type != "lesson")} loading={loading} /> - f.training_type?.type == "lesson")} - loading={loading} - /> + + diff --git a/frontend/src/pages/administration/mentor/request/open-request-list/_types/OTRLessonList.types.tsx b/frontend/src/pages/administration/mentor/request/open-request-list/_types/OTRLessonList.types.tsx new file mode 100644 index 0000000..28aca2b --- /dev/null +++ b/frontend/src/pages/administration/mentor/request/open-request-list/_types/OTRLessonList.types.tsx @@ -0,0 +1,69 @@ +import { TableColumn } from "react-data-table-component"; +import { Button } from "@/components/ui/Button/Button"; +import { COLOR_OPTS, SIZE_OPTS } from "@/assets/theme.config"; +import { TbEye } from "react-icons/tb"; +import { Link, NavigateFunction } from "react-router-dom"; +import { TrainingRequestModel } from "@/models/TrainingRequestModel"; +import dayjs from "dayjs"; +import { Config } from "@/core/Config"; +import { Checkbox } from "@/components/ui/Checkbox/Checkbox"; + +function getColumns(navigate: NavigateFunction, toggleSelectedUser: (cid: number) => any): TableColumn[] { + return [ + { + name: "Auswahl", + cell: row => { + return toggleSelectedUser(row.user?.id ?? -1)}>; + }, + }, + { + name: "Trainee", + cell: row => + row.user == null ? ( + "N/A" + ) : ( + + + {row.user.first_name} {row.user.last_name} ({row.user.id}) + + + ), + }, + { + name: "Training", + selector: row => row.training_type?.name ?? "N/A", + }, + { + name: "Station", + selector: row => row.training_station?.callsign ?? "N/A", + }, + { + name: "Solo Ende", + selector: row => "xx.xx.xxxx", + }, + { + name: "Angefragt", + selector: row => dayjs.utc(row.createdAt).format(Config.DATE_FORMAT), + }, + { + name: "Aktion", + cell: row => { + return ( + + ); + }, + }, + ]; +} + +export default { + getColumns, +}; diff --git a/frontend/src/pages/administration/mentor/request/open-request-list/subpages/ORLLesson.subpage.tsx b/frontend/src/pages/administration/mentor/request/open-request-list/subpages/ORLLesson.subpage.tsx new file mode 100644 index 0000000..c526fba --- /dev/null +++ b/frontend/src/pages/administration/mentor/request/open-request-list/subpages/ORLLesson.subpage.tsx @@ -0,0 +1,68 @@ +import { Table } from "@/components/ui/Table/Table"; +import OTRLessonListTypes from "@/pages/administration/mentor/request/open-request-list/_types/OTRLessonList.types"; +import { TrainingRequestModel } from "@/models/TrainingRequestModel"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/Button/Button"; +import { COLOR_OPTS, TYPE_OPTS } from "@/assets/theme.config"; +import { useState } from "react"; +import { ButtonRow } from "@/components/ui/Button/ButtonRow"; +import { ConversionUtils } from "turbocommons-ts"; +import { Alert } from "@/components/ui/Alert/Alert"; + +export function ORLLessonSubpage({ filteredTrainingRequests, loading }: { filteredTrainingRequests: TrainingRequestModel[]; loading: boolean }) { + const navigate = useNavigate(); + const [selectedUsers, setSelectedUsers] = useState([]); + + function toggleSelectedUser(cid: number) { + if (selectedUsers.includes(cid)) { + setSelectedUsers(selectedUsers.filter(u => u !== cid)); + return; + } + + setSelectedUsers([...selectedUsers, cid]); + } + + return ( + <> + + Eine Lesson kann über mehrere Arten erstellt werden. Zum Einen können auf der linken Seite die Teilnehmer der Lesson ausgewählt und mit dem + unteren Button "Lesson Erstellen" an die Session-Erstellen Seite übertragen werden. Zum Anderen kann über die Funktion "Ansehen" eine normale + Session mit einem Benutzer erstellt werden. + + +
f.training_type?.type == "lesson")} + loading={loading} + /> + + + + + + ); +} diff --git a/frontend/src/pages/administration/mentor/request/open-request-view/OpenTrainingRequest.view.tsx b/frontend/src/pages/administration/mentor/request/open-request-view/OpenTrainingRequest.view.tsx index f2d5502..b4d71cc 100644 --- a/frontend/src/pages/administration/mentor/request/open-request-view/OpenTrainingRequest.view.tsx +++ b/frontend/src/pages/administration/mentor/request/open-request-view/OpenTrainingRequest.view.tsx @@ -16,6 +16,7 @@ import { TrainingRequestModel } from "@/models/TrainingRequestModel"; import { RenderIf } from "@/components/conditionals/RenderIf"; import { OpenTrainingRequestSkeleton } from "@/pages/administration/mentor/request/open-request-view/_skeletons/OpenTrainingRequest.skeleton"; import useApi from "@/utils/hooks/useApi"; +import { ConversionUtils } from "turbocommons-ts"; export function OpenTrainingRequestView() { const navigate = useNavigate(); @@ -108,7 +109,13 @@ export function OpenTrainingRequestView() { className={"lg:mr-3"} variant={"twoTone"} color={COLOR_OPTS.PRIMARY} - onClick={() => navigate(`/administration/training-session/create/${trainingRequest?.uuid}`)} + onClick={() => + navigate( + `/administration/training-session/create?users=${ConversionUtils.stringToBase64( + JSON.stringify([trainingRequest?.user_id]) + )}&request_uuid=${trainingRequest?.uuid}` + ) + } icon={}> Session Erstellen diff --git a/frontend/src/pages/administration/mentor/training-session/session-create/TrainingSessionCreate.view.tsx b/frontend/src/pages/administration/mentor/training-session/session-create/TrainingSessionCreate.view.tsx index a0425d8..e0e1e96 100644 --- a/frontend/src/pages/administration/mentor/training-session/session-create/TrainingSessionCreate.view.tsx +++ b/frontend/src/pages/administration/mentor/training-session/session-create/TrainingSessionCreate.view.tsx @@ -2,15 +2,12 @@ import { Card } from "@/components/ui/Card/Card"; import { PageHeader } from "@/components/ui/PageHeader/PageHeader"; import { useNavigate } from "react-router-dom"; import { Input } from "@/components/ui/Input/Input"; -import { TbCalendarEvent, TbCalendarPlus, TbUser } from "react-icons/tb"; +import { TbCalendarEvent, TbCalendarPlus } from "react-icons/tb"; import dayjs from "dayjs"; -import React, { FormEvent, useState } from "react"; -import { Table } from "@/components/ui/Table/Table"; +import React, { useEffect, useState } from "react"; import { Separator } from "@/components/ui/Separator/Separator"; import { Button } from "@/components/ui/Button/Button"; -import { COLOR_OPTS, SIZE_OPTS, TYPE_OPTS } from "@/assets/theme.config"; -import { UserModel } from "@/models/UserModel"; -import TSCParticipantListTypes from "@/pages/administration/mentor/training-session/session-create/_types/TSCParticipantList.types"; +import { COLOR_OPTS, TYPE_OPTS } from "@/assets/theme.config"; import { RenderIf } from "@/components/conditionals/RenderIf"; import { TrainingSessionCreateSkeleton } from "@/pages/administration/mentor/training-session/session-create/_skeletons/TrainingSessionCreate.skeleton"; import { Select } from "@/components/ui/Select/Select"; @@ -19,18 +16,35 @@ import { TrainingStationModel } from "@/models/TrainingStationModel"; import useApi from "@/utils/hooks/useApi"; import { CourseModel } from "@/models/CourseModel"; import { TrainingTypeModel } from "@/models/TrainingTypeModel"; -import { Badge } from "@/components/ui/Badge/Badge"; import { Alert } from "@/components/ui/Alert/Alert"; import TrainingSessionCreateService from "@/pages/administration/mentor/training-session/session-create/_services/TrainingSessionCreate.service"; -import { Calendar, dayjsLocalizer, Views } from "react-big-calendar"; - import "react-big-calendar/lib/css/react-big-calendar.css"; -import { TrainingSessionCalendar } from "@/pages/administration/mentor/training-session/_components/TrainingSessionCalendar"; import { TrainingSessionParticipants } from "@/pages/administration/mentor/training-session/_components/TrainingSessionParticipants"; import { IMinimalUser } from "@models/User"; +import { axiosInstance } from "@/utils/network/AxiosInstance"; +import { TrainingRequestModel } from "@/models/TrainingRequestModel"; +import ToastHelper from "@/utils/helper/ToastHelper"; + +interface ISessionParams { + users: string | null; + request_uuid: string | null; +} + +function getURLParams(): ISessionParams { + const url = new URL(window.location.toString()); + + let users = url.searchParams.get("users"); + let request_uuid = url.searchParams.get("request_uuid"); + + return { + users: users, + request_uuid: request_uuid, + }; +} export function TrainingSessionCreateView() { const navigate = useNavigate(); + const params = getURLParams(); const { data: courses, loading: loadingCourses } = useApi({ url: "/administration/course/mentorable", @@ -43,12 +57,56 @@ export function TrainingSessionCreateView() { const [trainingTypeID, setTrainingTypeID] = useState(undefined); const [participants, setParticipants] = useState([]); + const [loadingTrainingRequestData, setLoadingTrainingRequestData] = useState(true); + const [defaultRequestState, setDefaultRequestState] = useState(undefined); + + // Load the initial session data (including course, training type and users...) + useEffect(() => { + if (params.users == null && params.request_uuid == null) { + setLoadingTrainingRequestData(false); + return; + } + + let userPromise; + let sessionPromise; + + if (params.users != null) { + // Load the users here + userPromise = axiosInstance.get("/administration/user/min", { + params: { + users: params.users, + }, + }); + } + + if (params.request_uuid != null) { + // Load the session data + sessionPromise = axiosInstance.get(`/administration/training-request/${params.request_uuid}`); + } + + Promise.all([userPromise, sessionPromise]) + .then(results => { + const users = results[0]!.data as IMinimalUser[]; + const request = results[1]!.data as TrainingRequestModel; + + setCourseUUID(request.course?.uuid); + setTrainingTypeID(request.training_type_id); + + setDefaultRequestState(request); + setParticipants(users); + }) + .catch(() => { + ToastHelper.error("Fehler beim Laden der Informationen aus den Trainingsanfragen. Versuche es bitte später erneut."); + }) + .finally(() => setLoadingTrainingRequestData(false)); + }, []); + return ( <> } elementFalse={ <> @@ -68,7 +126,7 @@ export function TrainingSessionCreateView() { label={`Kurs Auswählen`} labelSmall name={"course_uuid"} - defaultValue={"none"} + defaultValue={courseUUID ?? "none"} onChange={value => { if (value == "none") { setCourseUUID(undefined); @@ -95,7 +153,7 @@ export function TrainingSessionCreateView() { labelSmall disabled={courses?.find(c => c.uuid == courseUUID)?.training_types?.length == 0 || courseUUID == null} name={"training_type_id"} - defaultValue={"none"} + defaultValue={trainingTypeID ?? "none"} onChange={value => { if (value == "none") { setTrainingTypeID(undefined); @@ -143,7 +201,7 @@ export function TrainingSessionCreateView() { courseUUID == null } name={"training_station_id"} - defaultValue={"none"}> + defaultValue={defaultRequestState?.training_station?.id ?? "none"}> (false); - - const [participants, setParticipants] = useState([]); - - const { data: trainingRequest, loading } = useApi({ - url: `/administration/training-request/${courseUUID}`, - method: "get", - onLoad: trainingRequest => { - if (trainingRequest.user != null) { - let p = [...participants]; - p.push(trainingRequest.user); - setParticipants(p); - } - }, - }); - - return ( - <> - - - } - elementFalse={ - <> -
{ - await TrainingSessionCreateService.createSession({ - event: e, - setSubmitting: setSubmitting, - participants: participants, - navigate: navigate, - fromRequest: true, - trainingRequest: trainingRequest, - }); - }}> - -
- } disabled readOnly value={trainingRequest?.course?.name} /> - } - disabled - readOnly - value={`${trainingRequest?.training_type?.name} (${trainingRequest?.training_type?.type})`} - /> -
-
- } - value={dayjs().utc().format("YYYY-MM-DD HH:mm")} - /> - -
- - - -
- - - - - } - /> - - ); -} diff --git a/frontend/src/pages/administration/mentor/training-session/session-create/_services/TrainingSessionCreate.service.ts b/frontend/src/pages/administration/mentor/training-session/session-create/_services/TrainingSessionCreate.service.ts index 4fdcad9..c2eb9c4 100644 --- a/frontend/src/pages/administration/mentor/training-session/session-create/_services/TrainingSessionCreate.service.ts +++ b/frontend/src/pages/administration/mentor/training-session/session-create/_services/TrainingSessionCreate.service.ts @@ -1,4 +1,3 @@ -import { UserModel } from "@/models/UserModel"; import { axiosInstance } from "@/utils/network/AxiosInstance"; import ToastHelper from "@/utils/helper/ToastHelper"; import { Dispatch, FormEvent } from "react"; diff --git a/frontend/src/pages/administration/mentor/users/view/_modals/UVCreateNote.modal.tsx b/frontend/src/pages/administration/mentor/users/view/_modals/UVCreateNote.modal.tsx index 983200d..54fcce6 100644 --- a/frontend/src/pages/administration/mentor/users/view/_modals/UVCreateNote.modal.tsx +++ b/frontend/src/pages/administration/mentor/users/view/_modals/UVCreateNote.modal.tsx @@ -68,7 +68,7 @@ export function UVCreateNoteModal(props: CreateUserNotePartialProps) { 'Wähle einen Kurs aus um diese Notiz dem Kurs zuzuordnen. Falls kein Kurs ausgewählt wurde, gilt diese Notiz "global", d.h. sie wird keinem Kurs zugewiesen.' }> } /> } /> } /> - } />