Skip to content

Commit

Permalink
Clean up some components, add button row
Browse files Browse the repository at this point in the history
  • Loading branch information
ngoerlitz committed May 18, 2024
1 parent 05acd3a commit aae9b8d
Show file tree
Hide file tree
Showing 20 changed files with 434 additions and 528 deletions.
108 changes: 56 additions & 52 deletions backend/src/controllers/course/CourseInformationController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Request, Response } from "express";
import { NextFunction, Request, Response } from "express";
import { Course } from "../../models/Course";
import { User } from "../../models/User";
import { TrainingSession } from "../../models/TrainingSession";
import { ActionRequirement } from "../../models/ActionRequirement";
import RequirementHelper from "../../utility/helper/RequirementHelper";
import { HttpStatusCode } from "axios";
import { ForbiddenException } from "../../exceptions/ForbiddenException";

/**
* Returns course information based on the provided uuid (request.query.uuid)
Expand Down Expand Up @@ -75,66 +76,69 @@ async function getUserCourseInformationByUUID(request: Request, response: Respon
* @param request
* @param response
*/
async function getCourseTrainingInformationByUUID(request: Request, response: Response) {
const user: User = response.locals.user;
const course_uuid: string = request.query.uuid?.toString() ?? "";
async function getCourseTrainingInformationByUUID(request: Request, response: Response, next: NextFunction) {
try {
const user: User = response.locals.user;
const course_uuid: string = request.query.uuid?.toString() ?? "";

if (!(await user.isMemberOfCourse(course_uuid))) {
response.status(HttpStatusCode.Forbidden).send({ message: "You are not enroled in this course" });
return;
}
if (!(await user.isMemberOfCourse(course_uuid))) {
throw new ForbiddenException("You are not enrolled in this course!", true);
}

const data: User | null = await User.findOne({
where: {
id: user.id,
},
include: [
{
association: User.associations.training_sessions,
through: {
as: "training_session_belongs_to_users",
attributes: ["passed", "log_id"],
where: {
user_id: user.id,
},
},
include: [
{
association: TrainingSession.associations.training_logs,
attributes: ["uuid", "id"],
through: { attributes: [] },
},
{
association: TrainingSession.associations.training_type,
attributes: ["id", "name", "type"],
},
{
association: TrainingSession.associations.course,
const data: User | null = await User.findOne({
where: {
id: user.id,
},
include: [
{
association: User.associations.training_sessions,
through: {
as: "training_session_belongs_to_users",
attributes: ["passed", "log_id"],
where: {
uuid: course_uuid,
user_id: user.id,
},
attributes: ["uuid"],
as: "course",
},
],
},
],
});

if (data == null) {
response.sendStatus(HttpStatusCode.InternalServerError);
return;
}
include: [
{
association: TrainingSession.associations.training_logs,
attributes: ["uuid", "id"],
through: { attributes: [] },
},
{
association: TrainingSession.associations.training_type,
attributes: ["id", "name", "type"],
},
{
association: TrainingSession.associations.course,
where: {
uuid: course_uuid,
},
attributes: ["uuid"],
as: "course",
},
],
},
],
});

data.training_sessions?.sort((a, b) => {
if (a.date == null || b.date == null) {
return 0;
if (data == null) {
response.sendStatus(HttpStatusCode.InternalServerError);
return;
}

return a.date > b.date ? 1 : -1;
});
data.training_sessions?.sort((a, b) => {
if (a.date == null || b.date == null) {
return 0;
}

return a.date > b.date ? 1 : -1;
});

response.send(data.training_sessions);
response.send(data.training_sessions);
} catch (e) {
next(e);
}
}

async function validateCourseRequirements(request: Request, response: Response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TrainingRequest } from "../../models/TrainingRequest";
import dayjs from "dayjs";
import NotificationLibrary from "../../libraries/notification/NotificationLibrary";
import { HttpStatusCode } from "axios";
import { ForbiddenException } from "../../exceptions/ForbiddenException";

/**
* Gets a list of upcoming training sessions
Expand Down Expand Up @@ -75,11 +76,24 @@ async function getByUUID(request: Request, response: Response) {
attributes: ["id"],
through: { attributes: [] },
},
TrainingSession.associations.mentor,
TrainingSession.associations.cpt,
TrainingSession.associations.training_type,
TrainingSession.associations.training_station,
TrainingSession.associations.course,
{
association: TrainingSession.associations.mentor,
},
{
association: TrainingSession.associations.cpt,
},
{
association: TrainingSession.associations.training_type,
attributes: ["id", "name", "type"],
},
{
association: TrainingSession.associations.training_station,
attributes: ["id", "callsign", "frequency"],
},
{
association: TrainingSession.associations.course,
attributes: ["uuid", "name", "name_en"],
},
],
});

Expand All @@ -91,20 +105,26 @@ async function getByUUID(request: Request, response: Response) {

// Check if the user even exists in this session, else deny the request
if (session?.users?.find((u: User) => u.id == user.id) == null) {
response.sendStatus(HttpStatusCode.Forbidden);
return;
throw new ForbiddenException("You don't have permission to view this log", true);
}

const requestingUserPassed = await TrainingSessionBelongsToUsers.findOne({
where: {
user_id: user.id,
training_session_id: session.id,
},
include: [
{
association: TrainingSessionBelongsToUsers.associations.training_log,
attributes: ["uuid"],
},
],
});

response.send({
...session.toJSON(),
user_passed: requestingUserPassed == null ? null : requestingUserPassed.passed,
log_id: requestingUserPassed?.training_log?.uuid ?? null,
});
}

Expand Down
8 changes: 7 additions & 1 deletion backend/src/exceptions/ForbiddenException.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
export class ForbiddenException extends Error {
public readonly error_: string;
public readonly pageStay_: boolean;

constructor(error: string) {
constructor(error: string, pageStay: boolean = false) {
super();
this.error_ = error;
this.pageStay_ = pageStay;
}

public getPageStayAttribute(): boolean {
return this.pageStay_;
}
}
1 change: 1 addition & 0 deletions backend/src/middlewares/ExceptionInterceptorMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function exceptionInterceptorMiddleware(error: any, request: Reques
method: request.method,
code: HttpStatusCode.Forbidden,
message: error.message,
stay: error.getPageStayAttribute(),
});
return;
}
Expand Down
5 changes: 4 additions & 1 deletion backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"skipLibCheck": true
},
"include": [
"./**/*"
]
}
8 changes: 3 additions & 5 deletions frontend/src/components/conditionals/MapArray.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { ReactElement } from "react";

type MapArrayProps<T> = {
data: T[];
mapFunction: (value: T, index: number) => ReactElement | ReactElement[];
mapFunction: (value: T, index: number) => any;
};

export function MapArray(props: MapArrayProps<any>) {
return <>{props.data.map(props.mapFunction)}</>;
export function MapArray<T>(props: MapArrayProps<T>) {
return <>{props.data.map<T>(props.mapFunction)}</>;
}
11 changes: 11 additions & 0 deletions frontend/src/components/ui/Button/ButtonRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ReactElement } from "react";

/**
* Utility component which wraps an arbitrary number of buttons
* and causes them to equally wrap on all pages!
* @param children
* @constructor
*/
export function ButtonRow({ children }: { children: ReactElement | ReactElement[] }) {
return <div className={"flex flex-wrap flex-col md:flex-row gap-2"}>{children}</div>;
}
12 changes: 10 additions & 2 deletions frontend/src/components/ui/UserFilter/UserFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import useApi from "@/utils/hooks/useApi";
import { Table } from "@/components/ui/Table/Table";
import { Input } from "@/components/ui/Input/Input";
import { useFilter } from "@/utils/hooks/useFilter";
import { useMemo, useState } from "react";
import { useState } from "react";
import { useDebounce } from "@/utils/hooks/useDebounce";
import { fuzzySearch } from "@/utils/helper/fuzzysearch/FuzzySearchHelper";
import UserFilterTypes from "@/components/ui/UserFilter/UserFilter.types";
Expand All @@ -12,6 +12,7 @@ import { IMinimalUser } from "@models/User";
interface IUserFilterProps {
onUserSelect: (user: IMinimalUser) => any;
removeIDs?: number[];
description?: string;
}

function filterFunction(user: IMinimalUser, searchValue: string) {
Expand All @@ -37,7 +38,14 @@ export function UserFilter(props: IUserFilterProps) {

return (
<>
<Input label={"Benutzer Suchen"} labelSmall placeholder={"CID, Name"} value={searchValue} onChange={e => setSearchValue(e.target.value)} />
<Input
label={"Benutzer Suchen"}
labelSmall
description={props.description}
placeholder={"CID, Name"}
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
/>

<RenderIf
truthValue={searchValue != ""}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Badge } from "@/components/ui/Badge/Badge";
import { COLOR_OPTS } from "@/assets/theme.config";
import { Separator } from "@/components/ui/Separator/Separator";
import { Table } from "@/components/ui/Table/Table";
import TSCParticipantListTypes from "@/pages/administration/mentor/training-session/session-create/_types/TSCParticipantList.types";
import { Card } from "@/components/ui/Card/Card";
import React, { Dispatch, useState } from "react";
import { UserFilter } from "@/components/ui/UserFilter/UserFilter";
import { IMinimalUser } from "@models/User";

interface ITrainingSessionParticipants {
participants: IMinimalUser[];
setParticipants: Dispatch<IMinimalUser[]>;
submitting: boolean;
}

export function TrainingSessionParticipants({ participants, setParticipants, submitting }: ITrainingSessionParticipants) {
function addUser(user: IMinimalUser) {
setParticipants([...participants, user]);
}

return (
<Card
header={"Teilnehmer"}
headerBorder
className={"mt-5"}
headerExtra={participants.length == 0 ? <Badge color={COLOR_OPTS.DANGER}>Mindestens ein Teilnehmer erforderlich</Badge> : undefined}>
<UserFilter
onUserSelect={addUser}
removeIDs={participants.map(p => p.id)}
description={
"Benutzer, die nicht in dem ausgewählten Kurs eingeschrieben sind werden nicht berücksichtigt und der Session entsprechend nicht hinzugefügt."
}
/>

<Separator />

<Table paginate columns={TSCParticipantListTypes.getColumns(participants, setParticipants)} data={participants} />
</Card>
);
}
Loading

0 comments on commit aae9b8d

Please sign in to comment.