Skip to content

Commit

Permalink
create lesson by directly selecting a set of users within the request…
Browse files Browse the repository at this point in the history
… list
  • Loading branch information
ngoerlitz committed May 19, 2024
1 parent 5b69d8b commit 8fa3f4d
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 162 deletions.
8 changes: 4 additions & 4 deletions backend/src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
38 changes: 32 additions & 6 deletions backend/src/controllers/user/UserAdminController.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -29,15 +31,39 @@ async function getAllSensitive(request: Request, response: Response) {

/**
* Gets all users with minimal data only (CID, Name)
* If the optional ?users=<base64> 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 {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/controllers/user/UserCourseAdminController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,7 +47,6 @@ export function OpenTrainingRequestList() {
const [searchInputValue, setSearchInputValue] = useState<string>("");
const debouncedInput = useDebounce(searchInputValue, 250);

const columns: TableColumn<TrainingRequestModel>[] = OpenRequestListTypes.getColumns(navigate);
const filteredTrainingRequests = useFilter<TrainingRequestModel>(trainingRequests ?? [], searchInputValue, debouncedInput, filterTrainingRequestFunction);

return (
Expand Down Expand Up @@ -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}
/>
<Table
paginate
paginationPerPage={15}
className={"mt-5"}
columns={columns}
data={filteredTrainingRequests.filter((f: TrainingRequestModel) => f.training_type?.type == "lesson")}
loading={loading}
/>

<ORLLessonSubpage filteredTrainingRequests={filteredTrainingRequests} loading={loading} />
</Tabs>
</Card>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TrainingRequestModel>[] {
return [
{
name: "Auswahl",
cell: row => {
return <Checkbox onChange={() => toggleSelectedUser(row.user?.id ?? -1)}></Checkbox>;
},
},
{
name: "Trainee",
cell: row =>
row.user == null ? (
"N/A"
) : (
<Link to={"/administration/users/" + row.user?.id + "?r"} target={"_blank"}>
<span className={"text-primary hover:cursor-pointer hover:underline"}>
{row.user.first_name} {row.user.last_name} ({row.user.id})
</span>
</Link>
),
},
{
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 (
<Button
className={"my-3"}
onClick={() => navigate(`${row.uuid}`)}
size={SIZE_OPTS.SM}
variant={"twoTone"}
color={COLOR_OPTS.PRIMARY}
icon={<TbEye size={20} />}>
Ansehen
</Button>
);
},
},
];
}

export default {
getColumns,
};
Original file line number Diff line number Diff line change
@@ -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<number[]>([]);

function toggleSelectedUser(cid: number) {
if (selectedUsers.includes(cid)) {
setSelectedUsers(selectedUsers.filter(u => u !== cid));
return;
}

setSelectedUsers([...selectedUsers, cid]);
}

return (
<>
<Alert type={TYPE_OPTS.INFO} showIcon>
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.
</Alert>

<Table
paginate
paginationPerPage={15}
className={"mt-5"}
columns={OTRLessonListTypes.getColumns(navigate, toggleSelectedUser)}
data={filteredTrainingRequests.filter((f: TrainingRequestModel) => f.training_type?.type == "lesson")}
loading={loading}
/>

<ButtonRow>
<Button
className={"mt-5"}
type="button"
variant="twoTone"
color={COLOR_OPTS.PRIMARY}
disabled={selectedUsers.length === 0}
onClick={() => {
const selectedUsersBase64 = ConversionUtils.stringToBase64(JSON.stringify(selectedUsers));

const firstRequestUUID = filteredTrainingRequests.find(f => f.user?.id == selectedUsers[0] && f.training_type?.type == "lesson")?.uuid;
const requestsByCIDs = filteredTrainingRequests.filter(f => selectedUsers.includes(f.user?.id ?? -1));

// Check if every request is requesting the same training type. If this isn't the case, we don't want to pre-populate the create session page
if (!requestsByCIDs.every(r => r.training_type_id == requestsByCIDs[0].training_type_id)) {
navigate(`/administration/training-session/create?users=${selectedUsersBase64}`);
return;
}

navigate(`/administration/training-session/create?users=${selectedUsersBase64}&request_uuid=${firstRequestUUID}`);
}}>
Lesson Erstellen ({selectedUsers.length} Teilnehmer)
</Button>
</ButtonRow>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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={<TbCalendarPlus size={20} />}>
Session Erstellen
</Button>
Expand Down
Loading

0 comments on commit 8fa3f4d

Please sign in to comment.