Skip to content

Commit

Permalink
Properly show course endorsement when completed
Browse files Browse the repository at this point in the history
  • Loading branch information
ngoerlitz committed May 19, 2024
1 parent 8fa3f4d commit 13fc990
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 132 deletions.
18 changes: 11 additions & 7 deletions backend/src/controllers/course/CourseInformationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ActionRequirement } from "../../models/ActionRequirement";
import RequirementHelper from "../../utility/helper/RequirementHelper";
import { HttpStatusCode } from "axios";
import { ForbiddenException } from "../../exceptions/ForbiddenException";
import { EndorsementGroup } from "../../models/EndorsementGroup";

/**
* Returns course information based on the provided uuid (request.query.uuid)
Expand All @@ -14,9 +15,6 @@ import { ForbiddenException } from "../../exceptions/ForbiddenException";
*/
async function getInformationByUUID(request: Request, response: Response) {
const uuid: string = request.query.uuid?.toString() ?? "";
const user: User = response.locals.user;

const userCourses: Course[] = await user.getCourses();

const course: Course | null = await Course.findOne({
where: {
Expand All @@ -43,7 +41,10 @@ async function getInformationByUUID(request: Request, response: Response) {
return;
}

response.send(course);
const endorsementID = (course?.information?.data as any)?.endorsement_id;
let endorsement: EndorsementGroup | null = await EndorsementGroup.findByPk(endorsementID);

response.send({ ...course.toJSON(), endorsement: endorsement });
}

/**
Expand All @@ -61,11 +62,14 @@ async function getUserCourseInformationByUUID(request: Request, response: Respon
return;
}

const courses = await user.getCourses();
const course = courses.find(c => c.uuid == query.uuid);
const courses = await user.getCoursesWithInformation();
let course = courses.find(c => c.uuid == query.uuid);

const endorsementID = (course?.information?.data as any)?.endorsement_id;
let endorsement: EndorsementGroup | null = await EndorsementGroup.findByPk(endorsementID);

if (course) {
response.send(course);
response.send({ ...course.toJSON(), endorsement: endorsement });
} else {
response.sendStatus(HttpStatusCode.BadRequest);
}
Expand Down
1 change: 1 addition & 0 deletions backend/src/models/Course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MentorGroup } from "./MentorGroup";
import { User } from "./User";
import { CourseInformation } from "./CourseInformation";
import { COURSE_TABLE_ATTRIBUTES, COURSE_TABLE_NAME } from "../../db/migrations/20221115171247-create-courses-table";
import { EndorsementGroup } from "./EndorsementGroup";

export class Course extends Model<InferAttributes<Course>, InferCreationAttributes<Course>> {
//
Expand Down
1 change: 1 addition & 0 deletions backend/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class User extends Model<InferAttributes<User>, InferCreationAttributes<U
getGroupAdminMentorGroups = UserExtensions.getGroupAdminMentorGroups.bind(this);
getCourseCreatorMentorGroups = UserExtensions.getCourseCreatorMentorGroups.bind(this);
getCourses = UserExtensions.getCourses.bind(this);
getCoursesWithInformation = UserExtensions.getCoursesWithInformation.bind(this);
canManageCourseInMentorGroup = UserExtensions.canManageCourseInMentorGroup.bind(this);
canEditCourse = UserExtensions.canEditCourse.bind(this);

Expand Down
14 changes: 14 additions & 0 deletions backend/src/models/extensions/UserExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ async function getCourses(this: User): Promise<Course[]> {
return user?.courses ?? [];
}

async function getCoursesWithInformation(this: User): Promise<Course[]> {
const user: User | null = await User.findByPk(this.id, {
include: [
{
association: User.associations.courses,
include: [Course.associations.information],
},
],
});

return user?.courses ?? [];
}

export default {
hasRole,
hasPermission,
Expand All @@ -142,4 +155,5 @@ export default {
canManageCourseInMentorGroup,
canEditCourse,
getCourses,
getCoursesWithInformation,
};
20 changes: 19 additions & 1 deletion frontend/src/app/features/notificationSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export const notificationSlice = createSlice({
state.unreadNotifications = action.payload.filter(n => !n.read);
state.loadingNotifications = false;
},
appendNotifications: (state, action: PayloadAction<NotificationModel[]>) => {
state.notifications = [...state.notifications, ...action.payload];
state.unreadNotifications = action.payload.filter(n => !n.read);
},
clearUnreadNotifications: state => {
state.notifications = state.notifications.map(n => ({ ...n, read: true }));
state.unreadNotifications = [];
Expand All @@ -44,8 +48,22 @@ export function loadNotifications(dispatch: AppDispatch) {
});
}

export function loadUnreadNotifications(dispatch: AppDispatch) {
axiosInstance
.get("/notification/unread")
.then((res: AxiosResponse) => {
const data = res.data as NotificationModel[];
if (data.length == 0) return;

dispatch(appendNotifications(data));
})
.catch((err: AxiosError) => {
console.error("Failed to update Notifications");
});
}

export const useNotificationSelector = () => useAppSelector(store => store.notificationReducer);

export const { setNotifications, clearUnreadNotifications } = notificationSlice.actions;
export const { setNotifications, clearUnreadNotifications, appendNotifications } = notificationSlice.actions;

export default notificationSlice.reducer;
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import NotificationHelper from "../../../utils/helper/NotificationHelper";
import dayjs from "dayjs";
import { Tooltip } from "../../ui/Tooltip/Tooltip";
import { RenderIf } from "../../conditionals/RenderIf";
import ToastHelper from "../../../utils/helper/ToastHelper";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/Button/Button";
import { COLOR_OPTS, SIZE_OPTS } from "@/assets/theme.config";
import { axiosInstance } from "@/utils/network/AxiosInstance";
import { useSettingsSelector } from "@/app/features/settingsSlice";
import { useDropdown } from "@/utils/hooks/useDropdown";
import { useAppDispatch } from "@/app/hooks";
import { clearUnreadNotifications, loadNotifications, setNotifications, useNotificationSelector } from "@/app/features/notificationSlice";
import { clearUnreadNotifications, loadNotifications, loadUnreadNotifications, useNotificationSelector } from "@/app/features/notificationSlice";

export function NotificationHeader() {
const { language } = useSettingsSelector();
Expand All @@ -28,7 +27,7 @@ export function NotificationHeader() {
loadNotifications(dispatch);

setInterval(() => {
loadNotifications(dispatch);
loadUnreadNotifications(dispatch);
}, 1000 * 60 * 2);
}, []);

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/models/CourseModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TrainingTypeModel } from "./TrainingTypeModel";
import { EndorsementGroupModel } from "@/models/EndorsementGroupModel";

export type CourseModel = {
id: number;
Expand All @@ -18,6 +19,7 @@ export type CourseModel = {
training_types?: TrainingTypeModel[]; // All Training Types associated to this course
action_requirements?: ActionRequirementModel;
information?: CourseInformationModel;
endorsement?: EndorsementGroupModel;

UsersBelongsToCourses?: {
id: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { RenderIf } from "@/components/conditionals/RenderIf";
import { CCourseInformationSkeleton } from "@/pages/authenticated/course/_skeletons/CCourseInformation.skeleton";
import { Input } from "@/components/ui/Input/Input";
import { TbCalendar, TbCertificate, TbClock, TbId } from "react-icons/tb";
import dayjs from "dayjs";
import { Config } from "@/core/Config";
import { getAtcRatingCombined } from "@/utils/helper/vatsim/AtcRatingHelper";
import React from "react";
import { CourseInformationModel, CourseModel } from "@/models/CourseModel";
import { TextArea } from "@/components/ui/Textarea/TextArea";
import { TLanguage, useSettingsSelector } from "@/app/features/settingsSlice";

function getDuration(info?: CourseInformationModel) {
const language: TLanguage = useSettingsSelector().language;

if (!info) {
return "N/A";
}

let dur = dayjs.duration(info.data.duration, info.data.duration_unit).locale(language);
return dur.humanize();
}

export function CGeneralInformationPartial({
loading,
course,
showDescription = false,
showDuration = false,
}: {
loading: boolean;
course?: CourseModel;
showDescription?: boolean;
showDuration?: boolean;
}) {
return (
<RenderIf
truthValue={loading}
elementTrue={<CCourseInformationSkeleton />}
elementFalse={
<>
<div className={"grid grid-cols-1 md:grid-cols-2 gap-5"}>
<Input preIcon={<TbId size={20} />} labelSmall label={"Kurs Name"} disabled value={course?.name} />
<Input
preIcon={<TbCalendar size={20} />}
label={"Eingeschrieben am"}
labelSmall
disabled
value={dayjs.utc(course?.UsersBelongsToCourses?.createdAt).format(Config.DATETIME_FORMAT)}
/>
<Input
preIcon={<TbCertificate size={20} />}
label={"Rating nach Abschluss"}
labelSmall
disabled
value={course?.information?.data.rating ? getAtcRatingCombined(course?.information?.data?.rating) : "Keine Angabe"}
/>
<Input
preIcon={<TbCertificate size={20} />}
label={"Endorsement nach Abschluss"}
labelSmall
disabled
value={course?.endorsement?.name ?? "Keine Angabe"}
/>
<RenderIf
truthValue={showDuration}
elementTrue={
<Input
preIcon={<TbClock size={20} />}
label={"Ungefähre Dauer"}
description={
"Hierbei handelt es sich um einen Richtwert. Je nach Verfügbarkeit, Motivation, usw. kann die eigentliche Dauer von diesem Wert abweichen."
}
labelSmall
disabled
value={getDuration(course?.information)}
/>
}
/>
</div>

<RenderIf
truthValue={showDescription}
elementTrue={<TextArea labelSmall className={"mt-5"} disabled label={"Kursbeschreibung"} value={course?.description} />}
/>
</>
}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CCourseInformationSkeleton } from "@/pages/authenticated/course/_skelet
import { ICourseInformationData } from "@models/CourseInformation";
import { TLanguage, useSettingsSelector } from "@/app/features/settingsSlice";
import genericTranslation from "@/assets/lang/generic.translation";
import { CGeneralInformationPartial } from "@/pages/authenticated/course/_partials/CGeneralInformation.partial";

type ActiveCourseInformationPartialProps = {
showRequestTrainingModal: boolean;
Expand All @@ -27,21 +28,6 @@ type ActiveCourseInformationPartialProps = {
trainingRequests: TrainingRequestModel[];
};

function getDuration(data: ICourseInformationData, language: TLanguage) {
if (data == null || data.duration == null) return "Keine Angabe";

const duration = data.duration;
const unit = data?.duration_unit;

return `${duration} ${genericTranslation.durations[unit ?? "day"][language]}`;
}

function getEndorsement(data?: ICourseInformationData) {
if (data?.endorsement_id == null) return "Keine Angabe";

return `${data.endorsement_id} (TODO: Name)`;
}

export function CInformationPartial(props: ActiveCourseInformationPartialProps) {
const language = useSettingsSelector().language;
const [showWithdrawModal, setShowWithdrawModal] = useState<boolean>(false);
Expand All @@ -66,37 +52,7 @@ export function CInformationPartial(props: ActiveCourseInformationPartialProps)
elementTrue={<CCourseInformationSkeleton />}
elementFalse={
<Card header={"Allgemeine Informationen"} headerBorder headerExtra={<Badge color={COLOR_OPTS.PRIMARY}>Eingeschrieben</Badge>}>
<div className={"grid grid-cols-1 md:grid-cols-2 gap-5"}>
<Input preIcon={<TbId size={20} />} labelSmall label={"Kurs Name"} disabled value={props.course?.name} />
<Input
preIcon={<TbCalendar size={20} />}
label={"Eingeschrieben am"}
labelSmall
disabled
value={dayjs.utc(props.course?.UsersBelongsToCourses?.createdAt).format(Config.DATETIME_FORMAT)}
/>
<Input
labelSmall
preIcon={<TbClock size={20} />}
label={"Ungefähre Dauer"}
disabled
value={getDuration(props.course?.information?.data, language)}
/>
<Input
preIcon={<TbCertificate size={20} />}
label={"Rating nach Abschluss"}
labelSmall
disabled
value={getAtcRatingCombined(props.course?.information?.data?.rating)}
/>
<Input
labelSmall
preIcon={<TbCertificate size={20} />}
label={"Endorsement nach Abschluss (TODO)"}
disabled
value={getEndorsement(props.course?.information?.data)}
/>
</div>
<CGeneralInformationPartial loading={props.loadingCourse} course={props.course} showDuration />

<div className={"flex mt-7 lg:flex-row flex-col"}>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { CourseModel } from "@/models/CourseModel";
import { useParams } from "react-router-dom";
import { CTrainingHistoryPartial } from "@/pages/authenticated/course/_partials/CTrainingHistory.partial";
import { UserTrainingSessionModel } from "@/models/TrainingSessionModel";
import { CGeneralInformationPartial } from "@/pages/authenticated/course/_partials/CGeneralInformation.partial";
import { Badge } from "@/components/ui/Badge/Badge";
import { COLOR_OPTS } from "@/assets/theme.config";
import React from "react";
import { Card } from "@/components/ui/Card/Card";

export function CourseCompletedView() {
const { uuid } = useParams();
Expand Down Expand Up @@ -39,7 +44,9 @@ export function CourseCompletedView() {
<>
<PageHeader title={"Kurs Ansehen"} />

<CCompletedInformationPartial course={course} loadingCourse={loading} />
<Card header={"Allgemeine Informationen"} headerBorder headerExtra={<Badge color={COLOR_OPTS.SUCCESS}>Abgeschlossen</Badge>}>
<CGeneralInformationPartial course={course} loading={loading} />
</Card>

<CTrainingHistoryPartial trainingData={trainingData ?? []} loading={loading} />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dayjs from "dayjs";
import { Config } from "@/core/Config";
import { RenderIf } from "@/components/conditionals/RenderIf";
import { CCourseInformationSkeleton } from "@/pages/authenticated/course/_skeletons/CCourseInformation.skeleton";
import { CGeneralInformationPartial } from "@/pages/authenticated/course/_partials/CGeneralInformation.partial";

type ActiveCourseInformationPartialProps = {
course?: CourseModel;
Expand All @@ -23,30 +24,7 @@ export function CCompletedInformationPartial(props: ActiveCourseInformationParti
elementTrue={<CCourseInformationSkeleton />}
elementFalse={
<Card header={"Allgemeine Informationen"} headerBorder headerExtra={<Badge color={COLOR_OPTS.SUCCESS}>Abgeschlossen</Badge>}>
<div className={"grid grid-cols-1 md:grid-cols-2 gap-5"}>
<Input preIcon={<TbId size={20} />} labelSmall label={"Kurs Name"} disabled value={props.course?.name} />
<Input
preIcon={<TbCalendar size={20} />}
label={"Eingeschrieben am"}
labelSmall
disabled
value={dayjs.utc(props.course?.UsersBelongsToCourses?.createdAt).format(Config.DATETIME_FORMAT)}
/>
<Input
preIcon={<TbCertificate size={20} />}
label={"Rating nach Abschluss"}
labelSmall
disabled
value={getAtcRatingCombined(props.course?.information?.data?.rating_on_complete)}
/>
<Input
preIcon={<TbCertificate size={20} />}
label={"Endorsement nach Abschluss"}
labelSmall
disabled
value={getAtcRatingCombined(props.course?.information?.data?.rating_on_complete)}
/>
</div>
<CGeneralInformationPartial course={props.course} loading={props.loadingCourse} showDescription />
</Card>
}
/>
Expand Down
Loading

0 comments on commit 13fc990

Please sign in to comment.