186 lines
7.7 KiB
TypeScript
186 lines
7.7 KiB
TypeScript
import React, { useCallback } from "react";
|
|
import { Edition } from "@/data/edition";
|
|
import { Nullable } from "@/helpers";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FieldProps, Field, FieldArrayRenderProps, FieldArray, getIn } from "formik";
|
|
import { identityTransformer, Transformer } from "@/serialization";
|
|
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 { Moment } from "moment-timezone";
|
|
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
|
import { TextField as TextFieldFormik } from "formik-material-ui";
|
|
import { Course, Identifiable, InternshipProgramEntry, InternshipType } from "@/data";
|
|
import { Autocomplete } from "@material-ui/lab";
|
|
import { useAsync } from "@/hooks";
|
|
import api from "@/management/api";
|
|
import { Async } from "@/components/async";
|
|
import { AccountCheck, ArrowDown, ArrowUp, ShieldCheck, TrashCan } from "mdi-material-ui";
|
|
import { Actions } from "@/components";
|
|
import { Add } from "@material-ui/icons";
|
|
|
|
export type EditionFormValues = Nullable<Edition>;
|
|
|
|
export const initialEditionFormValues: EditionFormValues = {
|
|
course: null,
|
|
endDate: null,
|
|
minimumInternshipHours: 80,
|
|
proposalDeadline: null,
|
|
reportingEnd: null,
|
|
reportingStart: null,
|
|
startDate: null,
|
|
types: [],
|
|
program: [],
|
|
}
|
|
|
|
export const editionFormValuesTransformer: Transformer<Edition, EditionFormValues> = identityTransformer;
|
|
|
|
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, swap, push, form, name, ...props }: FieldArrayRenderProps) => {
|
|
const value = getIn(form.values, name) as InternshipProgramEntry[];
|
|
|
|
const { t } = useTranslation("management");
|
|
|
|
return <>
|
|
{ value.map((entry, index) => <Card>
|
|
<CardHeader
|
|
subheader={ t('edition.program.entry', { index: index + 1 }) }
|
|
action={ <>
|
|
{ index < value.length - 1 && <IconButton onClick={ () => swap(index, index + 1) }><ArrowDown /></IconButton> }
|
|
{ index > 0 && <IconButton onClick={ () => swap(index, index - 1) }><ArrowUp /></IconButton> }
|
|
<IconButton onClick={ () => remove(index) }><TrashCan /></IconButton>
|
|
</> }
|
|
/>
|
|
<CardContent>
|
|
<Field component={ TextFieldFormik }
|
|
label={ t('edition.program.field.description') }
|
|
name={`${name}[${index}].description`}
|
|
fullWidth
|
|
/>
|
|
</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 { t } = useTranslation("management");
|
|
|
|
return <Autocomplete
|
|
options={ courses.isLoading ? [] : courses.value as Course[] }
|
|
renderInput={ props => <TextField { ...props } label={ t("edition.field.course") } fullWidth/> }
|
|
getOptionLabel={ course => course.name }
|
|
value={ field.value }
|
|
onChange={ field.onChange }
|
|
onBlur={ field.onBlur }
|
|
/>
|
|
}
|
|
|
|
export const DatePickerField = ({ field, form, meta, ...props }: FieldProps<Moment>) => {
|
|
const { value, onChange, onBlur } = field;
|
|
|
|
return <DatePicker value={ value }
|
|
onChange={ onChange }
|
|
onBlur={ onBlur }
|
|
{ ...props }
|
|
format="DD.MM.yyyy"
|
|
disableToolbar fullWidth
|
|
variant="inline"
|
|
/>
|
|
}
|
|
|
|
export const EditionForm = () => {
|
|
const { t } = useTranslation("management");
|
|
const spacing = useSpacing(2);
|
|
|
|
return <div className={ spacing.vertical }>
|
|
<Typography variant="h5">{ t("edition.fields.basic") }</Typography>
|
|
<Grid container>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="startDate" component={ DatePickerField } label={ t("edition.field.start") } />
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="endDate" component={ DatePickerField } label={ t("edition.field.end") } />
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="course" component={ CoursePickerField } label={ t("edition.field.course") } />
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="minimumInternshipHours" component={ TextFieldFormik } label={ t("edition.field.minimumInternshipHours") } />
|
|
</Grid>
|
|
</Grid>
|
|
<Typography variant="h5">{ t("edition.fields.deadlines") }</Typography>
|
|
<Grid container>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="proposalDeadline" component={ DatePickerField } label={ t("edition.field.proposalDeadline") } />
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="reportingStart" component={ DatePickerField } label={ t("edition.field.reportingStart") } />
|
|
</Grid>
|
|
<Grid item xs={ 12 } md={ 6 }>
|
|
<Field name="reportingEnd" component={ DatePickerField } label={ t("edition.field.reportingEnd") } />
|
|
</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>
|
|
}
|