365 lines
14 KiB
TypeScript
365 lines
14 KiB
TypeScript
import React, { HTMLProps, useMemo, useState } from "react";
|
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Grid, TextField, Typography } from "@material-ui/core";
|
|
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
|
import { CompanyForm } from "@/forms/company";
|
|
import { StudentForm } from "@/forms/student";
|
|
import { sampleStudent } from "@/provider/dummy/student";
|
|
import { Company, Internship, InternshipType, internshipTypeLabels, Office, Student } from "@/data";
|
|
import { Nullable } from "@/helpers";
|
|
import moment, { Moment } from "moment";
|
|
import { computeWorkingHours } from "@/utils/date";
|
|
import { Autocomplete } from "@material-ui/lab";
|
|
import { emptyInternship } from "@/provider/dummy/internship";
|
|
import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useSelector } from "react-redux";
|
|
import { AppState } from "@/state/reducer";
|
|
import { Link as RouterLink, useHistory } from "react-router-dom";
|
|
import { route } from "@/routing";
|
|
import { getInternshipProposal } from "@/state/reducer/proposal";
|
|
import { Actions } from "@/components";
|
|
import { Field, Form, Formik, useFormikContext } from "formik";
|
|
import * as Yup from "yup";
|
|
import { Transformer } from "@/serialization";
|
|
import { TextField as TextFieldFormik } from "formik-material-ui"
|
|
import { Edition } from "@/data/edition";
|
|
import { useUpdateEffect } from "@/hooks";
|
|
|
|
export type InternshipFormValues = {
|
|
startDate: Moment | null;
|
|
endDate: Moment | null;
|
|
hours: number | "";
|
|
workingHours: number;
|
|
companyName: string;
|
|
companyNip: string;
|
|
city: string;
|
|
postalCode: string;
|
|
country: string;
|
|
street: string;
|
|
building: string;
|
|
mentorFirstName: string;
|
|
mentorLastName: string;
|
|
mentorEmail: string;
|
|
mentorPhone: string;
|
|
kindOther: string | null;
|
|
|
|
// relations
|
|
kind: InternshipType | null;
|
|
company: Company | null;
|
|
office: Office | null;
|
|
student: Student;
|
|
}
|
|
|
|
const emptyInternshipValues: InternshipFormValues = {
|
|
building: "",
|
|
city: "",
|
|
company: null,
|
|
companyName: "",
|
|
companyNip: "",
|
|
country: "",
|
|
street: "",
|
|
endDate: null,
|
|
hours: "",
|
|
kind: null,
|
|
kindOther: "",
|
|
mentorEmail: "",
|
|
mentorFirstName: "",
|
|
mentorLastName: "",
|
|
mentorPhone: "",
|
|
office: null,
|
|
postalCode: "",
|
|
startDate: null,
|
|
student: sampleStudent,
|
|
workingHours: 40,
|
|
}
|
|
|
|
export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType } & HTMLProps<any>) => {
|
|
const info = internshipTypeLabels[type];
|
|
|
|
return (
|
|
<div className="internship=type-item" { ...props }>
|
|
<div>{ info.label }</div>
|
|
{ info.description && <Typography variant="caption">{ info.description }</Typography> }
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const InternshipProgramForm = () => {
|
|
const { t } = useTranslation();
|
|
const { values, handleBlur, setFieldValue, errors } = useFormikContext<InternshipFormValues>();
|
|
|
|
return (
|
|
<Grid container>
|
|
<Grid item md={ 4 }>
|
|
<Autocomplete renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.kind") } fullWidth error={ !!errors.kind } helperText={ errors.kind }/> }
|
|
getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label }
|
|
renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> }
|
|
options={ Object.values(InternshipType) as InternshipType[] }
|
|
disableClearable
|
|
value={ values.kind || undefined }
|
|
onChange={ (_, value) => setFieldValue("kind", value) }
|
|
onBlur={ handleBlur }
|
|
/>
|
|
</Grid>
|
|
<Grid item md={ 8 }>
|
|
{
|
|
values.kind === InternshipType.Other &&
|
|
<Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } />
|
|
}
|
|
</Grid>
|
|
</Grid>
|
|
)
|
|
}
|
|
|
|
const InternshipDurationForm = () => {
|
|
const { t } = useTranslation();
|
|
const {
|
|
values: { startDate, endDate, workingHours },
|
|
errors,
|
|
touched,
|
|
setFieldTouched,
|
|
setFieldValue
|
|
} = useFormikContext<InternshipFormValues>();
|
|
|
|
const [overrideHours, setHoursOverride] = useState<number | null>(null)
|
|
|
|
const computedHours = useMemo(() => startDate && endDate && computeWorkingHours(startDate, endDate, workingHours / 5), [startDate, endDate, workingHours]);
|
|
|
|
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
|
|
const weeks = useMemo(() => hours !== null ? Math.floor(hours / workingHours) : null, [ hours ]);
|
|
|
|
useUpdateEffect(() => {
|
|
setFieldTouched("hours", true);
|
|
setFieldValue("hours", hours, true);
|
|
}, [ hours ]);
|
|
|
|
return (
|
|
<Grid container>
|
|
<Grid item md={ 6 }>
|
|
<DatePicker value={ startDate } onChange={ value => setFieldValue("startDate", value) }
|
|
format="DD MMMM yyyy"
|
|
disableToolbar fullWidth
|
|
variant="inline" label={ t("forms.internship.fields.start-date") }
|
|
minDate={ moment() }
|
|
/>
|
|
</Grid>
|
|
<Grid item md={ 6 }>
|
|
<DatePicker value={ endDate } onChange={ value => setFieldValue("endDate", value) }
|
|
format="DD MMMM yyyy"
|
|
disableToolbar fullWidth
|
|
variant="inline" label={ t("forms.internship.fields.end-date") }
|
|
minDate={ startDate || moment() }
|
|
/>
|
|
</Grid>
|
|
<Grid item md={ 4 }>
|
|
<Field component={ TextFieldFormik }
|
|
name="workingHours"
|
|
label={ t("forms.internship.fields.working-hours") }
|
|
helperText={ t("forms.internship.help.working-hours") }
|
|
fullWidth
|
|
/>
|
|
</Grid>
|
|
<Grid item md={ 4 }>
|
|
<TextField fullWidth
|
|
label={ t("forms.internship.fields.total-hours") }
|
|
error={ !!errors.hours && touched.hours }
|
|
helperText={ touched.hours && errors.hours }
|
|
value={ hours || "" }
|
|
onChange={ ev => setHoursOverride(parseInt(ev.target.value) || 0) }
|
|
/>
|
|
</Grid>
|
|
<Grid item md={ 4 }>
|
|
<TextField fullWidth label={ t("forms.internship.fields.weeks") }
|
|
value={ weeks || "" }
|
|
disabled
|
|
helperText={ t("forms.internship.help.weeks") }
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
type InternshipConverterContext = {
|
|
internship: Internship,
|
|
}
|
|
|
|
const converter: Transformer<Nullable<Internship>, InternshipFormValues, InternshipConverterContext> = {
|
|
transform(internship: Nullable<Internship>): InternshipFormValues {
|
|
return {
|
|
student: internship.intern as Student,
|
|
kind: internship.type,
|
|
kindOther: "",
|
|
startDate: internship.startDate,
|
|
endDate: internship.endDate,
|
|
hours: internship.hours || "",
|
|
building: internship.office?.address?.building || "",
|
|
office: internship.office,
|
|
city: internship.office?.address?.city || "",
|
|
postalCode: internship.office?.address?.postalCode || "",
|
|
street: internship.office?.address?.street || "",
|
|
country: internship.office?.address?.country || "",
|
|
company: internship.company,
|
|
companyName: internship.company?.name || "",
|
|
companyNip: internship.company?.nip || "",
|
|
mentorEmail: internship.mentor?.email || "",
|
|
mentorFirstName: internship.mentor?.name || "",
|
|
mentorLastName: internship.mentor?.surname || "",
|
|
mentorPhone: internship.mentor?.phone || "",
|
|
workingHours: 40,
|
|
}
|
|
},
|
|
reverseTransform(form: InternshipFormValues, context: InternshipConverterContext): Nullable<Internship> {
|
|
return {
|
|
...context.internship,
|
|
startDate: form.startDate as Moment,
|
|
endDate: form.endDate as Moment,
|
|
office: form.office || {
|
|
address: {
|
|
street: form.street,
|
|
postalCode: form.postalCode,
|
|
country: form.country,
|
|
city: form.city,
|
|
building: form.building,
|
|
}
|
|
},
|
|
mentor: {
|
|
surname: form.mentorLastName,
|
|
name: form.mentorFirstName,
|
|
email: form.mentorEmail,
|
|
phone: form.mentorPhone,
|
|
},
|
|
company: form.company || {
|
|
name: form.companyName,
|
|
nip: form.companyNip,
|
|
offices: [],
|
|
},
|
|
hours: form.hours as number,
|
|
type: form.kind as InternshipType,
|
|
}
|
|
}
|
|
}
|
|
|
|
export const InternshipForm: React.FunctionComponent = () => {
|
|
const initialInternship = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || {
|
|
...emptyInternship,
|
|
office: null,
|
|
company: null,
|
|
mentor: null,
|
|
intern: sampleStudent
|
|
});
|
|
|
|
const edition = useSelector<AppState, Edition>(state => state.edition as Edition);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
const dispatch = useDispatch();
|
|
const history = useHistory();
|
|
|
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
|
|
|
|
const validationSchema = Yup.object<Partial<InternshipFormValues>>({
|
|
mentorFirstName: Yup.string().required(t("validation.required")),
|
|
mentorLastName: Yup.string().required(t("validation.required")),
|
|
mentorEmail: Yup.string()
|
|
.required(t("validation.required"))
|
|
.email(t("validation.email")),
|
|
mentorPhone: Yup.string()
|
|
.required(t("validation.required"))
|
|
.matches(/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/, t("validation.phone")),
|
|
hours: Yup.number()
|
|
.min(edition.minimumInternshipHours, t("validation.internship.minimum-hours", { hours: edition.minimumInternshipHours })),
|
|
companyName: Yup.string().when("company", {
|
|
is: null,
|
|
then: Yup.string().required(t("validation.required"))
|
|
}),
|
|
companyNip: Yup.string().when("company", {
|
|
is: null,
|
|
then: Yup.string()
|
|
.required(t("validation.required"))
|
|
}),
|
|
street: Yup.string().required(t("validation.required")),
|
|
country: Yup.string().required(t("validation.required")),
|
|
city: Yup.string().required(t("validation.required")),
|
|
postalCode: Yup.string().required(t("validation.required")),
|
|
building: Yup.string().required(t("validation.required")),
|
|
kindOther: Yup.string().when("kind", {
|
|
is: (values: InternshipFormValues) => values?.kind === InternshipType.Other,
|
|
then: Yup.string().required(t("validation.required"))
|
|
})
|
|
})
|
|
|
|
const values = converter.transform(initialInternship);
|
|
|
|
const handleSubmit = (values: InternshipFormValues) => {
|
|
setConfirmDialogOpen(false);
|
|
|
|
dispatch({
|
|
type: InternshipProposalActions.Send,
|
|
internship: converter.reverseTransform(values, {
|
|
internship: initialInternship as Internship,
|
|
}) as Internship
|
|
});
|
|
|
|
history.push(route("home"))
|
|
}
|
|
|
|
const InnerForm = () => {
|
|
const { submitForm, validateForm } = useFormikContext();
|
|
|
|
const handleSubmitConfirmation = async () => {
|
|
const errors = await validateForm();
|
|
|
|
if (Object.keys(errors).length == 0) {
|
|
setConfirmDialogOpen(true);
|
|
} else {
|
|
console.warn(errors);
|
|
}
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
setConfirmDialogOpen(false);
|
|
}
|
|
|
|
return <Form>
|
|
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
|
|
<StudentForm />
|
|
<Typography variant="h3" className="section-header">{ t('internship.sections.kind' )}</Typography>
|
|
<InternshipProgramForm />
|
|
<Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography>
|
|
<InternshipDurationForm />
|
|
<Typography variant="h3" className="section-header">{ t('internship.sections.place') }</Typography>
|
|
<CompanyForm />
|
|
<Actions>
|
|
<Button variant="contained" color="primary" onClick={ handleSubmitConfirmation }>{ t("confirm") }</Button>
|
|
|
|
<Button component={ RouterLink } to={ route("home") }>
|
|
{ t('go-back') }
|
|
</Button>
|
|
</Actions>
|
|
|
|
<Dialog open={ confirmDialogOpen } onClose={ handleCancel }>
|
|
<DialogContent>
|
|
<DialogContentText>{ t('forms.internship.send-confirmation') }</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
|
|
<Button color="primary" autoFocus onClick={ submitForm }>{ t('confirm') }</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Form>
|
|
}
|
|
|
|
return (
|
|
<Formik initialValues={ values }
|
|
onSubmit={ handleSubmit }
|
|
validationSchema={ validationSchema }
|
|
validateOnChange={ false }
|
|
validateOnBlur={ true }
|
|
>
|
|
<InnerForm />
|
|
</Formik>
|
|
)
|
|
}
|
|
|