feature_validation #10

Manually merged
system-praktyk merged 5 commits from feature_validation into master 2020-08-17 23:27:29 +02:00
11 changed files with 347 additions and 191 deletions
Showing only changes of commit 1857785c84 - Show all commits

View File

@ -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,
} }

View File

@ -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 = {

View File

@ -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 {

View File

@ -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>
@ -102,42 +123,50 @@ export const MentorForm = () => {
return ( return (
<Grid container> <Grid container>
<Grid item md={6}> <Grid item md={ 6 }>
<Field name="mentorFirstName" label={ t("forms.internship.fields.first-name") } fullWidth component={ TextFieldFormik } /> <Field name="mentorFirstName" label={ t("forms.internship.fields.first-name") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
<Grid item md={6}> <Grid item md={ 6 }>
<Field name="mentorLastName" label={ t("forms.internship.fields.last-name") } fullWidth component={ TextFieldFormik } /> <Field name="mentorLastName" label={ t("forms.internship.fields.last-name") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
<Grid item md={8}> <Grid item md={ 8 }>
<Field name="mentorEmail" label={ t("forms.internship.fields.e-mail") } fullWidth component={ TextFieldFormik } /> <Field name="mentorEmail" label={ t("forms.internship.fields.e-mail") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
<Grid item md={4}> <Grid item md={ 4 }>
<Field name="mentorPhone" label={ t("forms.internship.fields.phone") } fullWidth component={ TextFieldFormik } /> <Field name="mentorPhone" label={ t("forms.internship.fields.phone") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
</Grid> </Grid>
); );
} }
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/>
</> </>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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>

View File

@ -1 +1,2 @@
export * from "./useProxyState" export * from "./useProxyState"
export * from "./useUpdateEffect"

View 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;

View File

@ -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,
} }

View File

@ -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>

View File

@ -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"