Edition types and program form fields

This commit is contained in:
Kacper Donat 2021-01-04 00:35:05 +01:00
parent 78377a934e
commit 2c8bb5b1ba
4 changed files with 108 additions and 8 deletions

View File

@ -4,6 +4,7 @@ import { OneWayTransformer, Transformer } from "@/serialization";
import { Edition } from "@/data/edition"; import { Edition } from "@/data/edition";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { Subset } from "@/helpers"; import { Subset } from "@/helpers";
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
export interface ProgramEntryDTO extends Identifiable { export interface ProgramEntryDTO extends Identifiable {
description: string; description: string;
@ -16,6 +17,7 @@ export interface EditionDTO extends Identifiable {
reportingStart: string, reportingStart: string,
course: CourseDTO, course: CourseDTO,
availableSubjects: ProgramEntryDTO[], availableSubjects: ProgramEntryDTO[],
availableInternshipTypes: InternshipTypeDTO[],
} }
export interface EditionTeaserDTO extends Identifiable { export interface EditionTeaserDTO extends Identifiable {
@ -45,7 +47,8 @@ export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
editionStart: subject.startDate.toISOString(), editionStart: subject.startDate.toISOString(),
course: courseDtoTransformer.reverseTransform(subject.course), course: courseDtoTransformer.reverseTransform(subject.course),
reportingStart: subject.reportingStart.toISOString(), reportingStart: subject.reportingStart.toISOString(),
availableSubjects: [], availableSubjects: subject.program.map(entry => programEntryDtoTransformer.reverseTransform(entry)),
availableInternshipTypes: subject.types.map(entry => internshipTypeDtoTransformer.reverseTransform(entry))
}; };
}, },
transform(subject: EditionDTO, context: undefined): Edition { transform(subject: EditionDTO, context: undefined): Edition {
@ -59,6 +62,8 @@ export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
proposalDeadline: moment(subject.reportingStart), proposalDeadline: moment(subject.reportingStart),
reportingStart: moment(subject.reportingStart), reportingStart: moment(subject.reportingStart),
reportingEnd: moment(subject.reportingStart).add(1, 'month'), reportingEnd: moment(subject.reportingStart).add(1, 'month'),
program: (subject.availableSubjects || []).map(entry => programEntryDtoTransformer.transform(entry)),
types: (subject.availableInternshipTypes || []).map(entry => internshipTypeDtoTransformer.transform(entry))
}; };
} }
} }

View File

@ -1,6 +1,7 @@
import { Moment } from "moment-timezone"; import { Moment } from "moment-timezone";
import { Course } from "@/data/course"; import { Course } from "@/data/course";
import { Identifiable } from "@/data/common"; import { Identifiable } from "@/data/common";
import { InternshipProgramEntry, InternshipType } from "@/data/internship";
export type Edition = { export type Edition = {
course: Course; course: Course;
@ -11,6 +12,8 @@ export type Edition = {
reportingEnd: Moment, reportingEnd: Moment,
minimumInternshipHours: number; minimumInternshipHours: number;
maximumInternshipHours?: number; maximumInternshipHours?: number;
program: InternshipProgramEntry[];
types: InternshipType[];
} & Identifiable } & Identifiable
export type Deadlines = { export type Deadlines = {

View File

@ -2,19 +2,37 @@ import React, { useCallback } from "react";
import { Edition } from "@/data/edition"; import { Edition } from "@/data/edition";
import { Nullable } from "@/helpers"; import { Nullable } from "@/helpers";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FieldProps, useFormikContext, Field } from "formik"; import { FieldProps, Field, FieldArrayRenderProps, FieldArray, getIn } from "formik";
import { identityTransformer, Transformer } from "@/serialization"; import { identityTransformer, Transformer } from "@/serialization";
import { Grid, TextField, Typography } from "@material-ui/core"; import {
Button, Card, CardContent, CardHeader,
Checkbox,
Grid, IconButton,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
Paper,
TextField,
Tooltip,
Typography
} from "@material-ui/core";
import { useSpacing } from "@/styles"; import { useSpacing } from "@/styles";
import { Moment } from "moment-timezone"; import { Moment } from "moment-timezone";
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
import { TextField as TextFieldFormik } from "formik-material-ui"; import { TextField as TextFieldFormik } from "formik-material-ui";
import { Course } from "@/data"; import { Course, Identifiable, InternshipProgramEntry, InternshipType } from "@/data";
import { Autocomplete } from "@material-ui/lab"; import { Autocomplete } from "@material-ui/lab";
import { useAsync } from "@/hooks"; import { useAsync } from "@/hooks";
import api from "@/management/api"; import api from "@/management/api";
import { Async } from "@/components/async";
import { AccountCheck, ShieldCheck, TrashCan } from "mdi-material-ui";
import { Actions } from "@/components";
import { Add } from "@material-ui/icons";
export type EditionFormValues = Nullable<Edition>; export type EditionFormValues = Nullable<Edition>;
export const initialEditionFormValues: EditionFormValues = { export const initialEditionFormValues: EditionFormValues = {
course: null, course: null,
endDate: null, endDate: null,
@ -22,12 +40,76 @@ export const initialEditionFormValues: EditionFormValues = {
proposalDeadline: null, proposalDeadline: null,
reportingEnd: null, reportingEnd: null,
reportingStart: null, reportingStart: null,
startDate: null startDate: null,
types: [],
program: [],
} }
export const editionFormValuesTransformer: Transformer<Edition, EditionFormValues> = identityTransformer; export const editionFormValuesTransformer: Transformer<Edition, EditionFormValues> = identityTransformer;
export const CoursePickerField = ({ field, form, meta, ...props}: FieldProps<Course>) => { function toggleValueInArray<T extends Identifiable>(array: T[], value: T, comparator: (a: T, b: T) => boolean = (a, b) => a == b): T[] {
return array.findIndex(other => comparator(other, value)) === -1
? [ ...array, value ]
: array.filter(other => !comparator(other, value));
}
export const ProgramField = ({ remove, push, form, name, ...props }: FieldArrayRenderProps) => {
const value = getIn(form.values, name) as InternshipProgramEntry[];
const setValue = (value: InternshipProgramEntry[]) => form.setFieldValue(name, value, false);
const { t } = useTranslation("management");
return <>
{ value.map((entry, index) => <Card>
<CardHeader
subheader={ t('edition.program.entry', { index: index + 1 }) }
action={ <>
<IconButton onClick={ () => remove(index) }>
<TrashCan />
</IconButton>
</> }
/>
<CardContent>
{ JSON.stringify(entry) }
</CardContent>
</Card>) }
<Actions>
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => push({ description: "" }) }>{ t("actions.add") }</Button>
</Actions>
</>
}
export const TypesField = ({ field, form, meta, ...props }: FieldProps<InternshipType[]>) => {
const { name, value = [] } = field;
const types = useAsync(useCallback(() => api.type.all(), []));
const { t } = useTranslation("management");
const toggle = (type: InternshipType) => () => form.setFieldValue(name, toggleValueInArray(value, type, (a, b) => a.id == b.id));
const isChecked = (type: InternshipType) => value.findIndex(v => v.id == type.id) !== -1;
return <Async async={ types }>
{ types => <List>{
types.map(type => <ListItem dense button onClick={ toggle(type) }>
<ListItemIcon>
<Checkbox edge="start" onChange={ toggle(type) } checked={ isChecked(type) }/>
</ListItemIcon>
<ListItemText>
<div>{ type.label.pl }</div>
<Typography variant="caption">{ type.description?.pl }</Typography>
</ListItemText>
<ListItemSecondaryAction>
<div style={{ display: "flex", flexDirection: "column" }}>
{ type.requiresDeanApproval && <Tooltip title={ t("type.flag.dean-approval") as string }><AccountCheck/></Tooltip> }
{ type.requiresInsurance && <Tooltip title={ t("type.flag.insurance") as string }><ShieldCheck/></Tooltip> }
</div>
</ListItemSecondaryAction>
</ListItem>)
}</List> }
</Async>
}
export const CoursePickerField = ({ field, form, meta, ...props }: FieldProps<Course>) => {
const courses = useAsync(useCallback(() => api.course.all(), [])); const courses = useAsync(useCallback(() => api.course.all(), []));
const { t } = useTranslation("management"); const { t } = useTranslation("management");
@ -59,7 +141,7 @@ export const EditionForm = () => {
const spacing = useSpacing(2); const spacing = useSpacing(2);
return <div className={ spacing.vertical }> return <div className={ spacing.vertical }>
<Typography variant="subtitle1">{ t("edition.fields.basic") }</Typography> <Typography variant="h5">{ t("edition.fields.basic") }</Typography>
<Grid container> <Grid container>
<Grid item xs={ 12 } md={ 6 }> <Grid item xs={ 12 } md={ 6 }>
<Field name="startDate" component={ DatePickerField } label={ t("edition.field.start") } /> <Field name="startDate" component={ DatePickerField } label={ t("edition.field.start") } />
@ -76,7 +158,7 @@ export const EditionForm = () => {
<Field name="minimumInternshipHours" component={ TextFieldFormik } label={ t("edition.field.minimumInternshipHours") } /> <Field name="minimumInternshipHours" component={ TextFieldFormik } label={ t("edition.field.minimumInternshipHours") } />
</Grid> </Grid>
</Grid> </Grid>
<Typography variant="subtitle1">{ t("edition.fields.deadlines") }</Typography> <Typography variant="h5">{ t("edition.fields.deadlines") }</Typography>
<Grid container> <Grid container>
<Grid item xs={ 12 } md={ 6 }> <Grid item xs={ 12 } md={ 6 }>
<Field name="proposalDeadline" component={ DatePickerField } label={ t("edition.field.proposalDeadline") } /> <Field name="proposalDeadline" component={ DatePickerField } label={ t("edition.field.proposalDeadline") } />
@ -90,5 +172,11 @@ export const EditionForm = () => {
<Field name="reportingEnd" component={ DatePickerField } label={ t("edition.field.reportingEnd") } /> <Field name="reportingEnd" component={ DatePickerField } label={ t("edition.field.reportingEnd") } />
</Grid> </Grid>
</Grid> </Grid>
<Typography variant="h5">{ t("edition.fields.program") }</Typography>
<FieldArray name="program" component={ ProgramField as any } />
<Typography variant="h5">{ t("edition.fields.types") }</Typography>
<Paper elevation={ 2 }>
<Field name="types" component={ TypesField } />
</Paper>
</div> </div>
} }

View File

@ -28,6 +28,8 @@ edition:
fields: fields:
basic: "Podstawowe" basic: "Podstawowe"
deadlines: "Terminy" deadlines: "Terminy"
program: "Ramowy program praktyk"
types: "Dostępne typy praktyki"
report-fields: report-fields:
title: "Pola formularza raportu praktyki" title: "Pola formularza raportu praktyki"
manage: manage:
@ -35,6 +37,8 @@ edition:
internships: "Zarządzanie praktykami" internships: "Zarządzanie praktykami"
settings: settings:
title: "Konfiguracja edycji" title: "Konfiguracja edycji"
program:
entry: "Punkt ramowego programu praktyki #{{ index }}"
report-field: report-field:
preview: Podgląd preview: Podgląd