Add validation based on formik and Yup
This commit is contained in:
parent
99e12f7681
commit
1857785c84
@ -12,10 +12,10 @@ export interface Company extends Identifiable {
|
|||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
nip: string;
|
nip: string;
|
||||||
offices: BranchOffice[];
|
offices: Office[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BranchOffice extends Identifiable {
|
export interface Office extends Identifiable {
|
||||||
address: Address;
|
address: Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export const emptyAddress: Address = {
|
|||||||
building: ""
|
building: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export const emptyBranchOffice: BranchOffice = {
|
export const emptyBranchOffice: Office = {
|
||||||
address: emptyAddress,
|
address: emptyAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ export type Edition = {
|
|||||||
startDate: Moment;
|
startDate: Moment;
|
||||||
endDate: Moment;
|
endDate: Moment;
|
||||||
proposalDeadline: Moment;
|
proposalDeadline: Moment;
|
||||||
|
minimumInternshipHours: number;
|
||||||
|
maximumInternshipHours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Deadlines = {
|
export type Deadlines = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Moment } from "moment";
|
import { Moment } from "moment";
|
||||||
import { Identifiable } from "./common";
|
import { Identifiable } from "./common";
|
||||||
import { Student } from "@/data/student";
|
import { Student } from "@/data/student";
|
||||||
import { BranchOffice, Company } from "@/data/company";
|
import { Company, Office } from "@/data/company";
|
||||||
|
|
||||||
export enum InternshipType {
|
export enum InternshipType {
|
||||||
FreeInternship = "FreeInternship",
|
FreeInternship = "FreeInternship",
|
||||||
@ -64,7 +64,7 @@ export interface Internship extends Identifiable {
|
|||||||
hours: number;
|
hours: number;
|
||||||
mentor: Mentor;
|
mentor: Mentor;
|
||||||
company: Company;
|
company: Company;
|
||||||
office: BranchOffice;
|
office: Office;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Plan extends Identifiable {
|
export interface Plan extends Identifiable {
|
||||||
|
@ -1,23 +1,13 @@
|
|||||||
import React, { HTMLProps, useMemo } from "react";
|
import React, { HTMLProps, useMemo } from "react";
|
||||||
import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data";
|
import { Company, formatAddress, Office } from "@/data";
|
||||||
import { sampleCompanies } from "@/provider/dummy";
|
import { sampleCompanies } from "@/provider/dummy";
|
||||||
import { Autocomplete } from "@material-ui/lab";
|
import { Autocomplete } from "@material-ui/lab";
|
||||||
import { Grid, TextField, Typography } from "@material-ui/core";
|
import { Grid, TextField, Typography } from "@material-ui/core";
|
||||||
import { BoundProperty, formFieldProps } from "./helpers";
|
import { InternshipFormValues } from "@/forms/internship";
|
||||||
import { InternshipFormSectionProps } from "@/forms/internship";
|
|
||||||
import { emptyMentor } from "@/provider/dummy/internship";
|
|
||||||
import { useProxyState } from "@/hooks";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Field } from "formik";
|
import { Field, useFormikContext } from "formik";
|
||||||
import { TextField as TextFieldFormik } from "formik-material-ui"
|
import { TextField as TextFieldFormik } from "formik-material-ui"
|
||||||
|
|
||||||
export type CompanyFormProps = {} & InternshipFormSectionProps;
|
|
||||||
|
|
||||||
export type BranchOfficeProps = {
|
|
||||||
disabled?: boolean;
|
|
||||||
offices?: BranchOffice[];
|
|
||||||
} & BoundProperty<BranchOffice, "onChange", "value">
|
|
||||||
|
|
||||||
export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => (
|
export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => (
|
||||||
<div className="company-item" { ...props }>
|
<div className="company-item" { ...props }>
|
||||||
<div>{ company.name }</div>
|
<div>{ company.name }</div>
|
||||||
@ -25,43 +15,65 @@ export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLPr
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTMLProps<any>) => (
|
export const OfficeItem = ({ office, ...props }: { office: Office } & HTMLProps<any>) => (
|
||||||
<div className="office-item" { ...props }>
|
<div className="office-item" { ...props }>
|
||||||
<div>{ office.address.city }</div>
|
<div>{ office.address.city }</div>
|
||||||
<Typography variant="caption">{ formatAddress(office.address) }</Typography>
|
<Typography variant="caption">{ formatAddress(office.address) }</Typography>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const BranchForm: React.FC<BranchOfficeProps> = ({ value: office, onChange: setOffice, offices = [], disabled = false }) => {
|
export const BranchForm: React.FC = () => {
|
||||||
const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]);
|
const { values, errors, setValues, touched, setFieldTouched } = useFormikContext<InternshipFormValues>();
|
||||||
const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address }))
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleCityChange = (event: any, value: BranchOffice | string | null) => {
|
const disabled = useMemo(() => !values.companyName, [values.companyName]);
|
||||||
|
const offices = useMemo(() => values.company?.offices || [], [values.company]);
|
||||||
|
const canEdit = useMemo(() => !values.office && !disabled, [values.office, disabled]);
|
||||||
|
|
||||||
|
const handleCityChange = (event: any, value: Office | string | null) => {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
setOffice({
|
setValues({
|
||||||
...emptyBranchOffice,
|
...values,
|
||||||
address: {
|
office: null,
|
||||||
...emptyAddress,
|
|
||||||
city: value,
|
city: value,
|
||||||
}
|
}, true);
|
||||||
});
|
|
||||||
} else if (typeof value === "object" && value !== null) {
|
} else if (typeof value === "object" && value !== null) {
|
||||||
setOffice(value);
|
const office = value as Office;
|
||||||
} else {
|
|
||||||
setOffice(emptyBranchOffice);
|
setValues({
|
||||||
|
...values,
|
||||||
|
office,
|
||||||
|
city: office.address.city,
|
||||||
|
country: office.address.country,
|
||||||
|
street: office.address.street,
|
||||||
|
building: office.address.building,
|
||||||
|
postalCode: office.address.postalCode,
|
||||||
|
}, true)
|
||||||
|
} else { // null
|
||||||
|
setValues({
|
||||||
|
...values,
|
||||||
|
office: null,
|
||||||
|
city: "",
|
||||||
|
country: "",
|
||||||
|
street: "",
|
||||||
|
building: "",
|
||||||
|
postalCode: "",
|
||||||
|
}, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCityInput = (event: any, value: string) => {
|
const handleCityInput = (event: any, value: string) => {
|
||||||
const base = office.id ? emptyBranchOffice : office;
|
setValues( {
|
||||||
setOffice({
|
...values,
|
||||||
...base,
|
office: null,
|
||||||
address: {
|
...(values.office ? {
|
||||||
...base.address,
|
country: "",
|
||||||
|
street: "",
|
||||||
|
building: "",
|
||||||
|
postalCode: "",
|
||||||
|
} : { }),
|
||||||
city: value,
|
city: value,
|
||||||
}
|
}, true);
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -72,25 +84,34 @@ export const BranchForm: React.FC<BranchOfficeProps> = ({ value: office, onChang
|
|||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
|
getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
|
||||||
renderOption={ office => <OfficeItem office={ office }/> }
|
renderOption={ office => <OfficeItem office={ office }/> }
|
||||||
renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.city") } fullWidth/> }
|
renderInput={
|
||||||
|
props =>
|
||||||
|
<TextField { ...props }
|
||||||
|
label={ t("forms.internship.fields.city") }
|
||||||
|
fullWidth
|
||||||
|
error={ touched.city && !!errors.city }
|
||||||
|
helperText={ touched.city && errors.city }
|
||||||
|
/>
|
||||||
|
}
|
||||||
onChange={ handleCityChange }
|
onChange={ handleCityChange }
|
||||||
onInputChange={ handleCityInput }
|
onInputChange={ handleCityInput }
|
||||||
inputValue={ office.address.city }
|
onBlur={ ev => setFieldTouched("city", true) }
|
||||||
value={ office.id ? office : null }
|
inputValue={ values.city }
|
||||||
|
value={ values.office ? values.office : null }
|
||||||
freeSolo
|
freeSolo
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 2 }>
|
<Grid item md={ 2 }>
|
||||||
<TextField label={ t("forms.internship.fields.postal-code") } fullWidth disabled={ !canEdit } { ...fieldProps("postalCode") }/>
|
<Field label={ t("forms.internship.fields.postal-code") } name="postalCode" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 3 }>
|
<Grid item md={ 3 }>
|
||||||
<TextField label={ t("forms.internship.fields.country") } fullWidth disabled={ !canEdit } { ...fieldProps("country") }/>
|
<Field label={ t("forms.internship.fields.country") } name="country" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 10 }>
|
<Grid item md={ 10 }>
|
||||||
<TextField label={ t("forms.internship.fields.street") } fullWidth disabled={ !canEdit } { ...fieldProps("street") }/>
|
<Field label={ t("forms.internship.fields.street") } name="street" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 2 }>
|
<Grid item md={ 2 }>
|
||||||
<TextField label={ t("forms.internship.fields.building") } fullWidth disabled={ !canEdit } { ...fieldProps("building") }/>
|
<Field label={ t("forms.internship.fields.building") } name="building" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
@ -118,26 +139,34 @@ export const MentorForm = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ internship, onChange }) => {
|
export const CompanyForm: React.FunctionComponent = () => {
|
||||||
const [company, setCompany] = useProxyState<Company>(internship.company || emptyCompany, company => onChange({ ...internship, company }));
|
const { values, setValues, errors, touched, setFieldTouched } = useFormikContext<InternshipFormValues>();
|
||||||
const [mentor, setMentor] = useProxyState<Mentor>(internship.mentor || emptyMentor, mentor => onChange({ ...internship, mentor }));
|
|
||||||
const [office, setOffice] = useProxyState<BranchOffice>(internship.office || emptyBranchOffice, office => onChange({ ...internship, office }));
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const canEdit = useMemo(() => !company.id, [company.id]);
|
const canEdit = useMemo(() => !values.company, [values.company]);
|
||||||
|
|
||||||
const fieldProps = formFieldProps(company, setCompany)
|
|
||||||
|
|
||||||
const handleCompanyChange = (event: any, value: Company | string | null) => {
|
const handleCompanyChange = (event: any, value: Company | string | null) => {
|
||||||
|
setFieldTouched("companyName", true);
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
setCompany({
|
setValues({
|
||||||
...emptyCompany,
|
...values,
|
||||||
name: value,
|
company: null,
|
||||||
});
|
companyName: value
|
||||||
|
}, true)
|
||||||
} else if (typeof value === "object" && value !== null) {
|
} else if (typeof value === "object" && value !== null) {
|
||||||
setCompany(value);
|
setValues({
|
||||||
|
...values,
|
||||||
|
company: value as Company,
|
||||||
|
companyName: value.name,
|
||||||
|
companyNip: value.nip,
|
||||||
|
}, true)
|
||||||
} else {
|
} else {
|
||||||
setCompany(emptyCompany);
|
setValues({
|
||||||
|
...values,
|
||||||
|
company: null,
|
||||||
|
companyName: "",
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,24 +175,22 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns
|
|||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Autocomplete options={ sampleCompanies }
|
<Autocomplete options={ sampleCompanies }
|
||||||
getOptionLabel={ option => option.name }
|
getOptionLabel={ option => typeof option === "string" ? option : option.name }
|
||||||
renderOption={ company => <CompanyItem company={ company }/> }
|
renderOption={ company => <CompanyItem company={ company }/> }
|
||||||
renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.company-name") } fullWidth/> }
|
renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.company-name") } fullWidth
|
||||||
onChange={ handleCompanyChange } value={ company }
|
error={ touched.companyName && !!errors.companyName } helperText={ touched.companyName && errors.companyName }/> }
|
||||||
|
onChange={ handleCompanyChange } value={ values.company || values.companyName }
|
||||||
freeSolo
|
freeSolo
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
<TextField label={ t("forms.internship.fields.nip") } fullWidth { ...fieldProps("nip") } disabled={ !canEdit }/>
|
<Field label={ t("forms.internship.fields.nip") } fullWidth name="companyNip" disabled={ !canEdit } component={ TextFieldFormik }/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/*<Grid item md={ 8 }>*/}
|
|
||||||
{/* <TextField label={ "Url" } fullWidth { ...fieldProps("url") } disabled={ !canEdit }/>*/}
|
|
||||||
{/*</Grid>*/}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Typography variant="subtitle1" className="subsection-header">{ t("internship.mentor") }</Typography>
|
<Typography variant="subtitle1" className="subsection-header">{ t("internship.mentor") }</Typography>
|
||||||
<MentorForm/>
|
<MentorForm/>
|
||||||
<Typography variant="subtitle1" className="subsection-header">{ t("internship.office") }</Typography>
|
<Typography variant="subtitle1" className="subsection-header">{ t("internship.office") }</Typography>
|
||||||
<BranchForm value={ office } onChange={ setOffice } offices={ company.offices } />
|
<BranchForm/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
|
import React, { HTMLProps, useMemo, useState } from "react";
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Grid, TextField, Typography } from "@material-ui/core";
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Grid, TextField, Typography } from "@material-ui/core";
|
||||||
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
||||||
import { CompanyForm } from "@/forms/company";
|
import { CompanyForm } from "@/forms/company";
|
||||||
import { StudentForm } from "@/forms/student";
|
import { StudentForm } from "@/forms/student";
|
||||||
import { sampleStudent } from "@/provider/dummy/student";
|
import { sampleStudent } from "@/provider/dummy/student";
|
||||||
import { BranchOffice, Company, Internship, InternshipType, internshipTypeLabels, Student } from "@/data";
|
import { Company, Internship, InternshipType, internshipTypeLabels, Office, Student } from "@/data";
|
||||||
import { Nullable } from "@/helpers";
|
import { Nullable } from "@/helpers";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
import { computeWorkingHours } from "@/utils/date";
|
import { computeWorkingHours } from "@/utils/date";
|
||||||
import { Autocomplete } from "@material-ui/lab";
|
import { Autocomplete } from "@material-ui/lab";
|
||||||
import { formFieldProps } from "@/forms/helpers";
|
|
||||||
import { emptyInternship } from "@/provider/dummy/internship";
|
import { emptyInternship } from "@/provider/dummy/internship";
|
||||||
import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -17,28 +16,26 @@ import { useSelector } from "react-redux";
|
|||||||
import { AppState } from "@/state/reducer";
|
import { AppState } from "@/state/reducer";
|
||||||
import { Link as RouterLink, useHistory } from "react-router-dom";
|
import { Link as RouterLink, useHistory } from "react-router-dom";
|
||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { useProxyState } from "@/hooks";
|
|
||||||
import { getInternshipProposal } from "@/state/reducer/proposal";
|
import { getInternshipProposal } from "@/state/reducer/proposal";
|
||||||
import { Actions } from "@/components";
|
import { Actions } from "@/components";
|
||||||
import { Form, Formik } from "formik";
|
import { Field, Form, Formik, useFormikContext } from "formik";
|
||||||
import * as Yup from "yup";
|
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 InternshipFormProps = {}
|
export type InternshipFormValues = {
|
||||||
|
|
||||||
export type InternshipFormSectionProps = {
|
|
||||||
internship: Nullable<Internship>,
|
|
||||||
onChange: (internship: Nullable<Internship>) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InternshipFormState = {
|
|
||||||
startDate: Moment | null;
|
startDate: Moment | null;
|
||||||
endDate: Moment | null;
|
endDate: Moment | null;
|
||||||
hours: number | null;
|
hours: number | "";
|
||||||
|
workingHours: number;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
companyNip: string;
|
companyNip: string;
|
||||||
city: string;
|
city: string;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
country: string;
|
country: string;
|
||||||
|
street: string;
|
||||||
building: string;
|
building: string;
|
||||||
mentorFirstName: string;
|
mentorFirstName: string;
|
||||||
mentorLastName: string;
|
mentorLastName: string;
|
||||||
@ -49,19 +46,20 @@ export type InternshipFormState = {
|
|||||||
// relations
|
// relations
|
||||||
kind: InternshipType | null;
|
kind: InternshipType | null;
|
||||||
company: Company | null;
|
company: Company | null;
|
||||||
office: BranchOffice | null;
|
office: Office | null;
|
||||||
student: Student | null;
|
student: Student;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyInternshipValues: InternshipFormState = {
|
const emptyInternshipValues: InternshipFormValues = {
|
||||||
building: "",
|
building: "",
|
||||||
city: "",
|
city: "",
|
||||||
company: null,
|
company: null,
|
||||||
companyName: "",
|
companyName: "",
|
||||||
companyNip: "",
|
companyNip: "",
|
||||||
country: "",
|
country: "",
|
||||||
|
street: "",
|
||||||
endDate: null,
|
endDate: null,
|
||||||
hours: null,
|
hours: "",
|
||||||
kind: null,
|
kind: null,
|
||||||
kindOther: "",
|
kindOther: "",
|
||||||
mentorEmail: "",
|
mentorEmail: "",
|
||||||
@ -71,7 +69,8 @@ const emptyInternshipValues: InternshipFormState = {
|
|||||||
office: null,
|
office: null,
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
startDate: null,
|
startDate: null,
|
||||||
student: null
|
student: sampleStudent,
|
||||||
|
workingHours: 40,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType } & HTMLProps<any>) => {
|
export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType } & HTMLProps<any>) => {
|
||||||
@ -85,89 +84,94 @@ export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionProps) => {
|
const InternshipProgramForm = () => {
|
||||||
const fieldProps = formFieldProps(internship, onChange);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { values, handleBlur, setFieldValue, errors } = useFormikContext<InternshipFormValues>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
<Autocomplete renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.kind") } fullWidth/> }
|
<Autocomplete renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.kind") } fullWidth error={ !!errors.kind } helperText={ errors.kind }/> }
|
||||||
getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label }
|
getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label }
|
||||||
renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> }
|
renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> }
|
||||||
options={ Object.values(InternshipType) as InternshipType[] }
|
options={ Object.values(InternshipType) as InternshipType[] }
|
||||||
disableClearable
|
disableClearable
|
||||||
{ ...fieldProps("type", (event, value) => value) as any }
|
value={ values.kind || undefined }
|
||||||
|
onChange={ (_, value) => setFieldValue("kind", value) }
|
||||||
|
onBlur={ handleBlur }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 8 }>
|
<Grid item md={ 8 }>
|
||||||
{ internship.type === InternshipType.Other && <TextField label={ t("forms.internship.fields.kind") } fullWidth/> }
|
{
|
||||||
|
values.kind === InternshipType.Other &&
|
||||||
|
<Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } />
|
||||||
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
{/*<Grid item>*/ }
|
|
||||||
{/* <FormGroup>*/ }
|
|
||||||
{/* <FormLabel component="legend" className="subsection-header">Realizowane punkty programu praktyk (minimum 3)</FormLabel>*/ }
|
|
||||||
{/* { course.possibleProgramEntries.map(entry => {*/ }
|
|
||||||
{/* return (*/ }
|
|
||||||
{/* <FormControlLabel label={ entry.description } key={ entry.id }*/ }
|
|
||||||
{/* control={ <Checkbox /> }*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* )*/ }
|
|
||||||
{/* }) }*/ }
|
|
||||||
{/* </FormGroup>*/ }
|
|
||||||
{/*</Grid>*/ }
|
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionProps) => {
|
const InternshipDurationForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
const [startDate, setStartDate] = useProxyState<Moment | null>(internship.startDate, value => onChange({ ...internship, startDate: value }));
|
values: { startDate, endDate, workingHours },
|
||||||
const [endDate, setEndDate] = useProxyState<Moment | null>(internship.endDate, value => onChange({ ...internship, endDate: value }));
|
errors,
|
||||||
|
touched,
|
||||||
|
setFieldTouched,
|
||||||
|
setFieldValue
|
||||||
|
} = useFormikContext<InternshipFormValues>();
|
||||||
|
|
||||||
const [overrideHours, setHoursOverride] = useState<number | null>(null)
|
const [overrideHours, setHoursOverride] = useState<number | null>(null)
|
||||||
const [workingHours, setWorkingHours] = useState<number>(40)
|
|
||||||
|
|
||||||
const computedHours = useMemo(() => startDate && endDate && computeWorkingHours(startDate, endDate, workingHours / 5), [startDate, endDate, workingHours]);
|
const computedHours = useMemo(() => startDate && endDate && computeWorkingHours(startDate, endDate, workingHours / 5), [startDate, endDate, workingHours]);
|
||||||
|
|
||||||
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
|
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
|
||||||
const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [hours]);
|
const weeks = useMemo(() => hours !== null ? Math.floor(hours / workingHours) : null, [ hours ]);
|
||||||
|
|
||||||
useEffect(() => onChange({ ...internship, hours }), [hours])
|
useUpdateEffect(() => {
|
||||||
|
setFieldTouched("hours", true);
|
||||||
|
setFieldValue("hours", hours, true);
|
||||||
|
}, [ hours ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item md={ 6 }>
|
<Grid item md={ 6 }>
|
||||||
<DatePicker value={ startDate } onChange={ setStartDate }
|
<DatePicker value={ startDate } onChange={ value => setFieldValue("startDate", value) }
|
||||||
format="DD MMMM yyyy"
|
format="DD MMMM yyyy"
|
||||||
clearable disableToolbar fullWidth
|
disableToolbar fullWidth
|
||||||
variant="inline" label={ t("forms.internship.fields.start-date") }
|
variant="inline" label={ t("forms.internship.fields.start-date") }
|
||||||
minDate={ moment() }
|
minDate={ moment() }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 6 }>
|
<Grid item md={ 6 }>
|
||||||
<DatePicker value={ endDate } onChange={ setEndDate }
|
<DatePicker value={ endDate } onChange={ value => setFieldValue("endDate", value) }
|
||||||
format="DD MMMM yyyy"
|
format="DD MMMM yyyy"
|
||||||
clearable disableToolbar fullWidth
|
disableToolbar fullWidth
|
||||||
variant="inline" label={ t("forms.internship.fields.end-date") }
|
variant="inline" label={ t("forms.internship.fields.end-date") }
|
||||||
minDate={ startDate || moment() }
|
minDate={ startDate || moment() }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
<TextField fullWidth label={ t("forms.internship.fields.working-hours") }
|
<Field component={ TextFieldFormik }
|
||||||
value={ workingHours } onChange={ ev => setWorkingHours(parseInt(ev.target.value) || 0) }
|
name="workingHours"
|
||||||
|
label={ t("forms.internship.fields.working-hours") }
|
||||||
helperText={ t("forms.internship.help.working-hours") }
|
helperText={ t("forms.internship.help.working-hours") }
|
||||||
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
<TextField fullWidth label={ t("forms.internship.fields.total-hours") }
|
<TextField fullWidth
|
||||||
value={ hours } onChange={ ev => setHoursOverride(parseInt(ev.target.value) || 0) }
|
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>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
<TextField fullWidth label={ t("forms.internship.fields.weeks") }
|
<TextField fullWidth label={ t("forms.internship.fields.weeks") }
|
||||||
value={ weeks } disabled
|
value={ weeks || "" }
|
||||||
|
disabled
|
||||||
helperText={ t("forms.internship.help.weeks") }
|
helperText={ t("forms.internship.help.weeks") }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -175,36 +179,86 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => {
|
type InternshipConverterContext = {
|
||||||
const initialInternshipState = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || {
|
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,
|
...emptyInternship,
|
||||||
|
office: null,
|
||||||
|
company: null,
|
||||||
|
mentor: null,
|
||||||
intern: sampleStudent
|
intern: sampleStudent
|
||||||
});
|
});
|
||||||
|
|
||||||
const [internship, setInternship] = useState<Nullable<Internship>>(initialInternshipState)
|
const edition = useSelector<AppState, Edition>(state => state.edition as Edition);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const validationSchema = Yup.object<Partial<InternshipFormValues>>({
|
||||||
setConfirmDialogOpen(false);
|
|
||||||
|
|
||||||
dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship });
|
|
||||||
history.push(route("home"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitConfirmation = () => {
|
|
||||||
setConfirmDialogOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setConfirmDialogOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationSchema = Yup.object<Partial<InternshipFormState>>({
|
|
||||||
mentorFirstName: Yup.string().required(t("validation.required")),
|
mentorFirstName: Yup.string().required(t("validation.required")),
|
||||||
mentorLastName: Yup.string().required(t("validation.required")),
|
mentorLastName: Yup.string().required(t("validation.required")),
|
||||||
mentorEmail: Yup.string()
|
mentorEmail: Yup.string()
|
||||||
@ -214,23 +268,68 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
|
|||||||
.required(t("validation.required"))
|
.required(t("validation.required"))
|
||||||
.matches(/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/, t("validation.phone")),
|
.matches(/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/, t("validation.phone")),
|
||||||
hours: Yup.number()
|
hours: Yup.number()
|
||||||
.min(40, t("validation.internship.minimum-hours")) // todo: take it from edition
|
.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"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
const values = converter.transform(initialInternship);
|
||||||
<Formik initialValues={ emptyInternshipValues } onSubmit={ values => console.log(values) }
|
|
||||||
validationSchema={ validationSchema } validateOnChange={ false } validateOnBlur={ true }>
|
const handleSubmit = (values: InternshipFormValues) => {
|
||||||
{ formik => <Form>
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setConfirmDialogOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Form>
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
|
||||||
<StudentForm student={ sampleStudent }/>
|
<StudentForm />
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.kind' )}</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.kind' )}</Typography>
|
||||||
<InternshipProgramForm internship={ internship } onChange={ setInternship }/>
|
<InternshipProgramForm />
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography>
|
||||||
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
|
<InternshipDurationForm />
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.place') }</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.place') }</Typography>
|
||||||
<CompanyForm internship={ internship } onChange={ setInternship }/>
|
<CompanyForm />
|
||||||
<Actions>
|
<Actions>
|
||||||
<Button variant="contained" color="primary" onClick={ () => formik.validateForm(formik.values) }>{ t("confirm") }</Button>
|
<Button variant="contained" color="primary" onClick={ handleSubmitConfirmation }>{ t("confirm") }</Button>
|
||||||
|
|
||||||
<Button component={ RouterLink } to={ route("home") }>
|
<Button component={ RouterLink } to={ route("home") }>
|
||||||
{ t('go-back') }
|
{ t('go-back') }
|
||||||
@ -243,10 +342,20 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
|
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
|
||||||
<Button color="primary" autoFocus onClick={ formik.submitForm }>{ t('confirm') }</Button>
|
<Button color="primary" autoFocus onClick={ submitForm }>{ t('confirm') }</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Form> }
|
</Form>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik initialValues={ values }
|
||||||
|
onSubmit={ handleSubmit }
|
||||||
|
validationSchema={ validationSchema }
|
||||||
|
validateOnChange={ false }
|
||||||
|
validateOnBlur={ true }
|
||||||
|
>
|
||||||
|
<InnerForm />
|
||||||
</Formik>
|
</Formik>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { Course, Student } from "@/data";
|
import { Course } from "@/data";
|
||||||
import { Button, Grid, TextField } from "@material-ui/core";
|
import { Button, Grid, TextField } from "@material-ui/core";
|
||||||
import { Alert, Autocomplete } from "@material-ui/lab";
|
import { Alert, Autocomplete } from "@material-ui/lab";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { sampleCourse } from "@/provider/dummy/student";
|
import { sampleCourse } from "@/provider/dummy/student";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFormikContext } from "formik";
|
||||||
|
import { InternshipFormValues } from "@/forms/internship";
|
||||||
|
|
||||||
type StudentFormProps = {
|
export const StudentForm = () => {
|
||||||
student: Student
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StudentForm = ({ student }: StudentFormProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { values: { student } } = useFormikContext<InternshipFormValues>();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from "./useProxyState"
|
export * from "./useProxyState"
|
||||||
|
export * from "./useUpdateEffect"
|
||||||
|
15
src/hooks/useUpdateEffect.ts
Normal file
15
src/hooks/useUpdateEffect.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { DependencyList, EffectCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export const useUpdateEffect = (effect: EffectCallback, dependencies: DependencyList) => {
|
||||||
|
const flag = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flag.current) {
|
||||||
|
effect();
|
||||||
|
} else {
|
||||||
|
flag.current = true;
|
||||||
|
}
|
||||||
|
}, dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdateEffect;
|
@ -4,5 +4,6 @@ import moment from "moment";
|
|||||||
export const sampleEdition: Edition = {
|
export const sampleEdition: Edition = {
|
||||||
startDate: moment("2020-07-01"),
|
startDate: moment("2020-07-01"),
|
||||||
endDate: moment("2020-09-30"),
|
endDate: moment("2020-09-30"),
|
||||||
proposalDeadline: moment("2020-07-31")
|
proposalDeadline: moment("2020-07-31"),
|
||||||
|
minimumInternshipHours: 40,
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,9 @@ type Simplify<T> = string |
|
|||||||
T extends Object ? Serializable<T> : any;
|
T extends Object ? Serializable<T> : any;
|
||||||
|
|
||||||
export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> }
|
export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> }
|
||||||
export type Transformer<TFrom, TResult> = {
|
export type Transformer<TFrom, TResult, TContext = never> = {
|
||||||
transform(subject: TFrom): TResult;
|
transform(subject: TFrom, context?: TContext): TResult;
|
||||||
reverseTransform(subject: TResult): TFrom;
|
reverseTransform(subject: TResult, context?: TContext): TFrom;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>
|
export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>
|
||||||
|
@ -163,5 +163,7 @@ validation:
|
|||||||
required: "To pole jest wymagane"
|
required: "To pole jest wymagane"
|
||||||
email: "Wprowadź poprawny adres e-mail"
|
email: "Wprowadź poprawny adres e-mail"
|
||||||
phone: "Wprowadź poprawny numer telefonu"
|
phone: "Wprowadź poprawny numer telefonu"
|
||||||
|
internship:
|
||||||
|
minimum-hours: "Minimalna liczba godzin wynosi {{ hours }}"
|
||||||
|
|
||||||
contact-coordinator: "Skontaktuj się z koordynatorem"
|
contact-coordinator: "Skontaktuj się z koordynatorem"
|
||||||
|
Loading…
Reference in New Issue
Block a user