diff --git a/src/management/api/course.ts b/src/management/api/course.ts index e245843..2877959 100644 --- a/src/management/api/course.ts +++ b/src/management/api/course.ts @@ -1,11 +1,28 @@ import { Course } from "@/data"; -import { sampleCourse } from "@/provider/dummy"; import { axios } from "@/api"; -import { EditionDTO, editionDtoTransformer } from "@/api/dto/edition"; +import { CourseDTO, courseDtoTransformer } from "@/api/dto/course"; +import { encapsulate, OneOrMany } from "@/helpers"; +import { prepare } from "@/routing"; -const COURSE_INDEX_ENDPOINT = "/management/course"; +const COURSE_INDEX_ENDPOINT = '/management/course' +const COURSE_ENDPOINT = COURSE_INDEX_ENDPOINT + "/:id"; -export async function all(): Promise { - const response = await axios.get(COURSE_INDEX_ENDPOINT); - return response.data; +export async function all(): Promise { + const response = await axios.get(COURSE_INDEX_ENDPOINT); + return response.data.map(dto => courseDtoTransformer.transform(dto)) +} + +export async function remove(type: OneOrMany): Promise { + await Promise.all(encapsulate(type).map( + type => axios.delete(prepare(COURSE_ENDPOINT, { id: type.id as string })) + )); +} + +export async function save(type: Course): Promise { + await axios.put( + COURSE_INDEX_ENDPOINT, + courseDtoTransformer.reverseTransform(type) + ); + + return type; } diff --git a/src/management/api/index.ts b/src/management/api/index.ts index 2c6309a..ad08c49 100644 --- a/src/management/api/index.ts +++ b/src/management/api/index.ts @@ -1,17 +1,17 @@ +import * as course from "./course" import * as edition from "./edition" import * as page from "./page" import * as type from "./type" -import * as course from "./course" import * as internship from "./internship" import * as document from "./document" import * as field from "./field" import * as report from "./report" export const api = { + course, edition, page, type, - course, internship, document, field, diff --git a/src/management/course/edit.tsx b/src/management/course/edit.tsx new file mode 100644 index 0000000..fb9fbf0 --- /dev/null +++ b/src/management/course/edit.tsx @@ -0,0 +1,47 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core"; +import React from "react"; +import { Form, Formik } from "formik"; +import { initialStaticPageFormValues, StaticPageForm, StaticPageFormValues, staticPageFormValuesTransformer } from "@/management/page/form"; +import { Actions } from "@/components"; +import { Save } from "@material-ui/icons"; +import { useTranslation } from "react-i18next"; +import { Cancel } from "mdi-material-ui"; +import { useSpacing } from "@/styles"; +import { initialCourseFormValues, CourseForm, CourseFormValues, courseFormValuesTransformer } from "@/management/course/form"; +import { Course } from "@/data"; + +export type EditCourseDialogProps = { + onSave?: (page: Course) => void; + value?: Course; +} & DialogProps; + +export function EditCourseDialog({ onSave, value, ...props }: EditCourseDialogProps) { + const { t } = useTranslation("management"); + const spacing = useSpacing(3); + + const handleSubmit = (values: CourseFormValues) => { + onSave?.(courseFormValuesTransformer.reverseTransform(values)); + }; + + const initialValues = value + ? courseFormValuesTransformer.transform(value) + : initialCourseFormValues; + + return + +
+ { t(value ? "type.edit.title" : "type.create.title") } + + + + + + + + + +
+
+
+} + diff --git a/src/management/course/form.tsx b/src/management/course/form.tsx new file mode 100644 index 0000000..bd82e59 --- /dev/null +++ b/src/management/course/form.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Course } from "@/data"; +import { Semester } from "@/data/student"; +import { useTranslation } from "react-i18next"; +import { useSpacing } from "@/styles"; +import { Field, FieldProps } from "formik"; +import { TextField as TextFieldFormik } from "formik-material-ui"; +import { Checkbox, Grid } from "@material-ui/core"; +import { identityTransformer, Transformer } from "@/serialization"; + +export type CourseFormValues = Omit; + +export const initialCourseFormValues: CourseFormValues = { + name: "", + desiredSemesters: [] +} + +export const courseFormValuesTransformer: Transformer = identityTransformer; + +export const DesiredSemestersField = ({ field, form, meta, ...props }: FieldProps) => { + const { name, value = [] } = field; + const { t } = useTranslation("management"); + + const toggle = (sid: Semester) => () => { + if (!value.includes(sid)) { + form.setFieldValue(name, [...value, sid]); + } else { + form.setFieldValue(name, value.filter((a) => a != sid)); + } + } + const isChecked = (sid: Semester) => value.includes(sid); + + const desiredSemesterCheckboxes = []; + for (var semesterId = 1; semesterId <= 10; semesterId++) { + const sid = semesterId; + + desiredSemesterCheckboxes.push( + + + { t("course.field.desiredSemester", {semesterId: semesterId})} + + ); + } + + return + { desiredSemesterCheckboxes } + +} + +export function CourseForm() { + const { t } = useTranslation("management"); + const spacing = useSpacing(2); + + return
+ + +
+} diff --git a/src/management/course/list.tsx b/src/management/course/list.tsx new file mode 100644 index 0000000..2b572f3 --- /dev/null +++ b/src/management/course/list.tsx @@ -0,0 +1,139 @@ +import { Page } from "@/pages/base"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAsyncState } from "@/hooks"; +import { Course } from "@/data"; +import api from "@/management/api"; +import { Management } from "@/management/main"; +import { Button, Container, IconButton, Tooltip, Typography } from "@material-ui/core"; +import { Async } from "@/components/async"; +import MaterialTable, { Column } from "material-table"; +import { MaterialTableTitle } from "@/management/common/MaterialTableTitle"; +import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers"; +import { AccountCheck, Delete, Refresh, ShieldCheck } from "mdi-material-ui"; +import { OneOrMany } from "@/helpers"; +import { createDeleteAction } from "@/management/common/DeleteResourceAction"; +import { BulkActions } from "@/management/common/BulkActions"; +import { useSpacing } from "@/styles"; +import { Actions } from "@/components"; +import { MultilingualCell } from "@/management/common/MultilangualCell"; +import { default as StaticPage } from "@/data/page"; +import { Add, Edit } from "@material-ui/icons"; +import { createPortal } from "react-dom"; +import { EditStaticPageDialog } from "@/management/page/edit"; +import { EditCourseDialog } from "@/management/course/edit"; + +const title = "course.index.title"; + +const label = (course: Course) => course?.name; + +export const CourseManagement = () => { + const { t } = useTranslation("management"); + const [result, setCoursesPromise] = useAsyncState(); + const [selected, setSelected] = useState([]); + const spacing = useSpacing(2); + + const updateCourseList = () => { + setCoursesPromise(api.course.all()); + } + + const handleCourseDelete = async (type: OneOrMany) => { + await api.course.remove(type); + updateCourseList(); + } + + useEffect(updateCourseList, []); + + const DeleteCourseAction = createDeleteAction({ label, onDelete: handleCourseDelete }); + + const CreateCourseAction = () => { + const [ open, setOpen ] = useState(false); + + const handleCourseCreation = async (value: Course) => { + await api.course.save(value); + setOpen(false); + updateCourseList(); + } + + return <> + + { open && createPortal( + setOpen(false) }/>, + document.getElementById("modals") as Element + ) } + + } + + const EditCourseAction = ({ resource }: { resource: Course }) => { + const [ open, setOpen ] = useState(false); + + const handleCourseCreation = async (value: Course) => { + await api.course.save(value); + setOpen(false); + updateCourseList(); + } + + return <> + + setOpen(true) }> + + { open && createPortal( + setOpen(false) }/>, + document.getElementById("modals") as Element + ) } + + } + + const columns: Column[] = [ + { + field: "id", + title: "ID", + width: 0, + defaultSort: "asc", + filtering: false, + }, + { + title: t("course.field.name"), + render: type => type.name, + customSort: (a, b) => a.name.localeCompare(b.name), + }, + { + title: t("course.field.desiredSemesters"), + render: type => type.desiredSemesters.slice().sort().join(", "), + sorting: false + }, + actionsColumn(type => <> + + + ) + ]; + + return + + + { t(title) } + + { t(title) } + + + + + + + { selected.length > 0 && + + { action => } + + } + { + pages => } + columns={ columns } + data={ pages } + onSelectionChange={ pages => setSelected(pages) } + options={ { selection: true, pageSize: 10 } } + /> + } + + +} diff --git a/src/management/main.tsx b/src/management/main.tsx index ba98c8f..d46e065 100644 --- a/src/management/main.tsx +++ b/src/management/main.tsx @@ -4,7 +4,7 @@ import React from "react"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; -import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline, FormatPageBreak } from "mdi-material-ui"; +import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline, FormatPageBreak, TableOfContents } from "mdi-material-ui"; export const ManagementLink = ({ icon, route, children }: ManagementLinkProps) => @@ -40,6 +40,9 @@ export const ManagementIndex = () => { + } route={ route("management:courses") }> + { t("management:course.index.title") } + } route={ route("management:editions") }> { t("management:edition.index.title") } diff --git a/src/management/routing.tsx b/src/management/routing.tsx index efdf24a..052e971 100644 --- a/src/management/routing.tsx +++ b/src/management/routing.tsx @@ -1,5 +1,6 @@ import { Route } from "@/routing"; import { isManagerMiddleware } from "@/management/middleware"; +import { CourseManagement } from "@/management/course/list"; import { EditionsManagement } from "@/management/edition/list"; import React from "react"; import { ManagementIndex } from "@/management/main"; @@ -17,6 +18,7 @@ import { EditionReportSchema } from "@/management/edition/report-schema"; export const managementRoutes: Route[] = ([ { name: "index", path: "/", content: ManagementIndex, exact: true }, + { name: "courses", path: "/courses", content: CourseManagement }, { name: "edition_router", path: "/editions/:edition", content: EditionRouter }, { name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] }, { name: "edition_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"], exact: true },