Merge pull request 'zarządzanie kierunkami - frontend' (#21) from asie/system-praktyk-front:feature/management-course into master
This commit is contained in:
commit
e0775e8742
@ -1,11 +1,28 @@
|
|||||||
import { Course } from "@/data";
|
import { Course } from "@/data";
|
||||||
import { sampleCourse } from "@/provider/dummy";
|
|
||||||
import { axios } from "@/api";
|
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<Course[]> {
|
export async function all(): Promise<Course[]> {
|
||||||
const response = await axios.get<Course[]>(COURSE_INDEX_ENDPOINT);
|
const response = await axios.get<CourseDTO[]>(COURSE_INDEX_ENDPOINT);
|
||||||
return response.data;
|
return response.data.map(dto => courseDtoTransformer.transform(dto))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(type: OneOrMany<Course>): Promise<void> {
|
||||||
|
await Promise.all(encapsulate(type).map(
|
||||||
|
type => axios.delete(prepare(COURSE_ENDPOINT, { id: type.id as string }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(type: Course): Promise<Course> {
|
||||||
|
await axios.put<Course>(
|
||||||
|
COURSE_INDEX_ENDPOINT,
|
||||||
|
courseDtoTransformer.reverseTransform(type)
|
||||||
|
);
|
||||||
|
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
|
import * as course from "./course"
|
||||||
import * as edition from "./edition"
|
import * as edition from "./edition"
|
||||||
import * as page from "./page"
|
import * as page from "./page"
|
||||||
import * as type from "./type"
|
import * as type from "./type"
|
||||||
import * as course from "./course"
|
|
||||||
import * as internship from "./internship"
|
import * as internship from "./internship"
|
||||||
import * as document from "./document"
|
import * as document from "./document"
|
||||||
import * as field from "./field"
|
import * as field from "./field"
|
||||||
import * as report from "./report"
|
import * as report from "./report"
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
course,
|
||||||
edition,
|
edition,
|
||||||
page,
|
page,
|
||||||
type,
|
type,
|
||||||
course,
|
|
||||||
internship,
|
internship,
|
||||||
document,
|
document,
|
||||||
field,
|
field,
|
||||||
|
47
src/management/course/edit.tsx
Normal file
47
src/management/course/edit.tsx
Normal file
@ -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 <Dialog { ...props } maxWidth="lg">
|
||||||
|
<Formik initialValues={ initialValues } onSubmit={ handleSubmit }>
|
||||||
|
<Form className={ spacing.vertical }>
|
||||||
|
<DialogTitle>{ t(value ? "type.edit.title" : "type.create.title") }</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<CourseForm />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Actions>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Save /> } type="submit">{ t("save") }</Button>
|
||||||
|
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||||
|
</Actions>
|
||||||
|
</DialogActions>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</Dialog>
|
||||||
|
}
|
||||||
|
|
58
src/management/course/form.tsx
Normal file
58
src/management/course/form.tsx
Normal file
@ -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<Course, 'id'>;
|
||||||
|
|
||||||
|
export const initialCourseFormValues: CourseFormValues = {
|
||||||
|
name: "",
|
||||||
|
desiredSemesters: []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const courseFormValuesTransformer: Transformer<Course, CourseFormValues> = identityTransformer;
|
||||||
|
|
||||||
|
export const DesiredSemestersField = ({ field, form, meta, ...props }: FieldProps<Semester[]>) => {
|
||||||
|
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(
|
||||||
|
<Grid item xs={3}>
|
||||||
|
<Checkbox edge="start" onChange={ toggle(sid) } checked={ isChecked(sid) }/>
|
||||||
|
{ t("course.field.desiredSemester", {semesterId: semesterId})}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Grid container spacing={3}>
|
||||||
|
{ desiredSemesterCheckboxes }
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CourseForm() {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
return <div className={ spacing.vertical }>
|
||||||
|
<Field label={ t("page.field.title") } name="name" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Field name="desiredSemesters" component={ DesiredSemestersField } />
|
||||||
|
</div>
|
||||||
|
}
|
139
src/management/course/list.tsx
Normal file
139
src/management/course/list.tsx
Normal file
@ -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<Course[]>();
|
||||||
|
const [selected, setSelected] = useState<Course[]>([]);
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
const updateCourseList = () => {
|
||||||
|
setCoursesPromise(api.course.all());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCourseDelete = async (type: OneOrMany<Course>) => {
|
||||||
|
await api.course.remove(type);
|
||||||
|
updateCourseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(updateCourseList, []);
|
||||||
|
|
||||||
|
const DeleteCourseAction = createDeleteAction({ label, onDelete: handleCourseDelete });
|
||||||
|
|
||||||
|
const CreateCourseAction = () => {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleCourseCreation = async (value: Course) => {
|
||||||
|
await api.course.save(value);
|
||||||
|
setOpen(false);
|
||||||
|
updateCourseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||||
|
{ open && createPortal(
|
||||||
|
<EditCourseDialog open={ open } onSave={ handleCourseCreation } onClose={ () => setOpen(false) }/>,
|
||||||
|
document.getElementById("modals") as Element
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditCourseAction = ({ resource }: { resource: Course }) => {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleCourseCreation = async (value: Course) => {
|
||||||
|
await api.course.save(value);
|
||||||
|
setOpen(false);
|
||||||
|
updateCourseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Tooltip title={ t("actions.edit") as any }>
|
||||||
|
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{ open && createPortal(
|
||||||
|
<EditCourseDialog open={ open } onSave={ handleCourseCreation } value={ resource } onClose={ () => setOpen(false) }/>,
|
||||||
|
document.getElementById("modals") as Element
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Column<Course>[] = [
|
||||||
|
{
|
||||||
|
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 => <>
|
||||||
|
<DeleteCourseAction resource={ type }/>
|
||||||
|
<EditCourseAction resource={ type }/>
|
||||||
|
</>)
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Page>
|
||||||
|
<Page.Header maxWidth="lg">
|
||||||
|
<Management.Breadcrumbs>
|
||||||
|
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||||
|
</Management.Breadcrumbs>
|
||||||
|
<Page.Title>{ t(title) }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
|
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||||
|
<Actions>
|
||||||
|
<CreateCourseAction />
|
||||||
|
<Button onClick={ updateCourseList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||||
|
</Actions>
|
||||||
|
{ selected.length > 0 && <BulkActions>
|
||||||
|
<DeleteCourseAction resource={ selected }>
|
||||||
|
{ action => <Button startIcon={ <Delete /> } onClick={ action }>{ t("actions.delete") }</Button> }
|
||||||
|
</DeleteCourseAction>
|
||||||
|
</BulkActions> }
|
||||||
|
<Async async={ result } keepValue>{
|
||||||
|
pages => <MaterialTable
|
||||||
|
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||||
|
columns={ columns }
|
||||||
|
data={ pages }
|
||||||
|
onSelectionChange={ pages => setSelected(pages) }
|
||||||
|
options={ { selection: true, pageSize: 10 } }
|
||||||
|
/>
|
||||||
|
}</Async>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
}
|
@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { useTranslation } from "react-i18next";
|
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) =>
|
export const ManagementLink = ({ icon, route, children }: ManagementLinkProps) =>
|
||||||
<ListItem button component={ RouterLink } to={ route }>
|
<ListItem button component={ RouterLink } to={ route }>
|
||||||
@ -40,6 +40,9 @@ export const ManagementIndex = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Paper elevation={ 2 }>
|
<Paper elevation={ 2 }>
|
||||||
<Management.Menu>
|
<Management.Menu>
|
||||||
|
<ManagementLink icon={ <TableOfContents /> } route={ route("management:courses") }>
|
||||||
|
{ t("management:course.index.title") }
|
||||||
|
</ManagementLink>
|
||||||
<ManagementLink icon={ <CalendarClock /> } route={ route("management:editions") }>
|
<ManagementLink icon={ <CalendarClock /> } route={ route("management:editions") }>
|
||||||
{ t("management:edition.index.title") }
|
{ t("management:edition.index.title") }
|
||||||
</ManagementLink>
|
</ManagementLink>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Route } from "@/routing";
|
import { Route } from "@/routing";
|
||||||
import { isManagerMiddleware } from "@/management/middleware";
|
import { isManagerMiddleware } from "@/management/middleware";
|
||||||
|
import { CourseManagement } from "@/management/course/list";
|
||||||
import { EditionsManagement } from "@/management/edition/list";
|
import { EditionsManagement } from "@/management/edition/list";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ManagementIndex } from "@/management/main";
|
import { ManagementIndex } from "@/management/main";
|
||||||
@ -17,6 +18,7 @@ import { EditionReportSchema } from "@/management/edition/report-schema";
|
|||||||
export const managementRoutes: Route[] = ([
|
export const managementRoutes: Route[] = ([
|
||||||
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
||||||
|
|
||||||
|
{ name: "courses", path: "/courses", content: CourseManagement },
|
||||||
{ name: "edition_router", path: "/editions/:edition", content: EditionRouter },
|
{ name: "edition_router", path: "/editions/:edition", content: EditionRouter },
|
||||||
{ name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] },
|
{ name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] },
|
||||||
{ name: "edition_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"], exact: true },
|
{ name: "edition_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"], exact: true },
|
||||||
|
Loading…
Reference in New Issue
Block a user