feature_validation #10

Manually merged
system-praktyk merged 5 commits from feature_validation into master 2020-08-17 23:27:29 +02:00
22 changed files with 798 additions and 256 deletions

View File

@ -18,6 +18,7 @@
"@types/react-router-dom": "^5.1.5",
"@types/redux": "^3.6.0",
"@types/redux-persist": "^4.3.1",
"@types/yup": "^0.29.4",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"babel-core": "^6.26.3",
@ -29,6 +30,8 @@
"css-loader": "3.4.2",
"date-holidays": "^1.5.3",
"file-loader": "4.3.0",
"formik": "^2.1.5",
"formik-material-ui": "^3.0.0-alpha.0",
"html-webpack-plugin": "4.0.0-beta.11",
"i18next": "^19.6.0",
"i18next-browser-languagedetector": "^5.0.0",
@ -62,7 +65,8 @@
"webpack-cli": "^3.3.11",
"webpack-dev-server": "3.10.3",
"workbox-webpack-plugin": "4.3.1",
"yaml-loader": "^0.6.0"
"yaml-loader": "^0.6.0",
"yup": "^0.29.3"
},
"scripts": {
"serve": "webpack-dev-server --mode development",

View File

@ -2,7 +2,7 @@ import React, { HTMLProps } from "react";
import { useHorizontalSpacing } from "@/styles";
export const Actions = (props: HTMLProps<HTMLDivElement>) => {
const classes = useHorizontalSpacing(1);
const classes = useHorizontalSpacing(2);
return <div className={ classes.root } { ...props }/>
return <div className={ classes.root } { ...props } style={{ display: "flex", alignItems: "center" }}/>
}

View File

@ -1,12 +1,9 @@
import { Internship, internshipTypeLabels } from "@/data";
import React from "react";
import { Button, Paper, PaperProps, Typography, TypographyProps } from "@material-ui/core";
import { Paper, PaperProps, Typography, TypographyProps } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import { createStyles, makeStyles } from "@material-ui/core/styles";
import classNames from "classnames";
import { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import { Actions } from "@/components/actions";
import { useVerticalSpacing } from "@/styles";
import moment from "moment";
@ -36,7 +33,6 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
const { t } = useTranslation();
const classes = useVerticalSpacing(3);
const duration = moment.duration(proposal.endDate.diff(proposal.startDate));
return <div className={ classNames("proposal", classes.root) }>
@ -74,9 +70,9 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
{ t('internship.date-range', { start: proposal.startDate, end: proposal.endDate }) }
</Typography>
<Typography className="proposal__secondary">
{ t('internship.duration', { duration }) }
{ t('internship.duration', { duration, count: Math.floor(duration.asWeeks()) }) }
{ ", " }
{ t('internship.hours', { hours: proposal.hours }) }
{ t('internship.hours', { hours: proposal.hours, count: proposal.hours }) }
</Typography>
</Section>
@ -85,11 +81,5 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
<Typography className="proposal__primary">{ proposal.mentor.name } { proposal.mentor.surname }</Typography>
<Typography className="proposal__secondary">{ proposal.mentor.email }, { proposal.mentor.phone }</Typography>
</Section>
<Actions>
<Button component={ RouterLink } to={ route("home") } variant="contained" color="primary">
{ t('go-back') }
</Button>
</Actions>
</div>
}

View File

@ -1,3 +1,5 @@
export type Identifier = string;
export interface Identifiable {
id?: string
id?: Identifier
}

View File

@ -12,10 +12,10 @@ export interface Company extends Identifiable {
name: string;
url?: string;
nip: string;
offices: BranchOffice[];
offices: Office[];
}
export interface BranchOffice extends Identifiable {
export interface Office extends Identifiable {
address: Address;
}
@ -34,7 +34,7 @@ export const emptyAddress: Address = {
building: ""
}
export const emptyBranchOffice: BranchOffice = {
export const emptyBranchOffice: Office = {
address: emptyAddress,
}

View File

@ -4,6 +4,8 @@ export type Edition = {
startDate: Moment;
endDate: Moment;
proposalDeadline: Moment;
minimumInternshipHours: number;
maximumInternshipHours?: number;
}
export type Deadlines = {

View File

@ -1,7 +1,7 @@
import { Moment } from "moment";
import { Identifiable } from "./common";
import { Student } from "@/data/student";
import { BranchOffice, Company } from "@/data/company";
import { Company, Office } from "@/data/company";
export enum InternshipType {
FreeInternship = "FreeInternship",
@ -64,7 +64,7 @@ export interface Internship extends Identifiable {
hours: number;
mentor: Mentor;
company: Company;
office: BranchOffice;
office: Office;
}
export interface Plan extends Identifiable {

View File

@ -1,19 +1,12 @@
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 { Autocomplete } from "@material-ui/lab";
import { Grid, TextField, Typography } from "@material-ui/core";
import { BoundProperty, formFieldProps } from "./helpers";
import { InternshipFormSectionProps } from "@/forms/internship";
import { emptyMentor } from "@/provider/dummy/internship";
import { useProxyState } from "@/hooks";
export type CompanyFormProps = {} & InternshipFormSectionProps;
export type BranchOfficeProps = {
disabled?: boolean;
offices?: BranchOffice[];
} & BoundProperty<BranchOffice, "onChange", "value">
import { InternshipFormValues } from "@/forms/internship";
import { useTranslation } from "react-i18next";
import { Field, useFormikContext } from "formik";
import { TextField as TextFieldFormik } from "formik-material-ui"
export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => (
<div className="company-item" { ...props }>
@ -22,42 +15,65 @@ export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLPr
</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>{ office.address.city }</div>
<Typography variant="caption">{ formatAddress(office.address) }</Typography>
</div>
)
export const BranchForm: React.FC<BranchOfficeProps> = ({ value: office, onChange: setOffice, offices = [], disabled = false }) => {
const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]);
const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address }))
export const BranchForm: React.FC = () => {
const { values, errors, setValues, touched, setFieldTouched } = useFormikContext<InternshipFormValues>();
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") {
setOffice({
...emptyBranchOffice,
address: {
...emptyAddress,
city: value,
}
});
setValues({
...values,
office: null,
city: value,
}, true);
} else if (typeof value === "object" && value !== null) {
setOffice(value);
} else {
setOffice(emptyBranchOffice);
const office = value as Office;
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 base = office.id ? emptyBranchOffice : office;
setOffice({
...base,
address: {
...base.address,
city: value,
}
})
setValues( {
...values,
office: null,
...(values.office ? {
country: "",
street: "",
building: "",
postalCode: "",
} : { }),
city: value,
}, true);
}
return (
@ -68,73 +84,89 @@ export const BranchForm: React.FC<BranchOfficeProps> = ({ value: office, onChang
disabled={ disabled }
getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
renderOption={ office => <OfficeItem office={ office }/> }
renderInput={ props => <TextField { ...props } label={ "Miasto" } fullWidth/> }
renderInput={
props =>
<TextField { ...props }
label={ t("forms.internship.fields.city") }
fullWidth
error={ touched.city && !!errors.city }
helperText={ touched.city && errors.city }
/>
}
onChange={ handleCityChange }
onInputChange={ handleCityInput }
inputValue={ office.address.city }
value={ office.id ? office : null }
onBlur={ ev => setFieldTouched("city", true) }
inputValue={ values.city }
value={ values.office ? values.office : null }
freeSolo
/>
</Grid>
<Grid item md={ 2 }>
<TextField label={ "Kod pocztowy" } fullWidth disabled={ !canEdit } { ...fieldProps("postalCode") }/>
<Field label={ t("forms.internship.fields.postal-code") } name="postalCode" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
</Grid>
<Grid item md={ 3 }>
<TextField label={ "Kraj" } fullWidth disabled={ !canEdit } { ...fieldProps("country") }/>
<Field label={ t("forms.internship.fields.country") } name="country" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
</Grid>
<Grid item md={ 10 }>
<TextField label={ "Ulica" } fullWidth disabled={ !canEdit } { ...fieldProps("street") }/>
<Field label={ t("forms.internship.fields.street") } name="street" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
</Grid>
<Grid item md={ 2 }>
<TextField label={ "Nr Budynku" } fullWidth disabled={ !canEdit } { ...fieldProps("building") }/>
<Field label={ t("forms.internship.fields.building") } name="building" fullWidth disabled={ !canEdit } component={ TextFieldFormik }/>
</Grid>
</Grid>
</div>
)
}
export const MentorForm = ({ mentor, onMentorChange }: BoundProperty<Mentor, 'onMentorChange', 'mentor'>) => {
const fieldProps = formFieldProps(mentor, onMentorChange)
export const MentorForm = () => {
const { t } = useTranslation();
return (
<>
<Grid container>
<Grid item md={6}>
<TextField label="Imię" fullWidth { ...fieldProps("name") }/>
</Grid>
<Grid item md={6}>
<TextField label="Nazwisko" value={ mentor.surname } fullWidth { ...fieldProps("surname") }/>
</Grid>
<Grid item md={8}>
<TextField label="E-mail" value={ mentor.email } fullWidth { ...fieldProps("email") }/>
</Grid>
<Grid item md={4}>
<TextField label="Nr telefonu" value={ mentor.phone } fullWidth { ...fieldProps("phone") }/>
</Grid>
<Grid container>
<Grid item md={ 6 }>
<Field name="mentorFirstName" label={ t("forms.internship.fields.first-name") } fullWidth component={ TextFieldFormik }/>
</Grid>
</>
<Grid item md={ 6 }>
<Field name="mentorLastName" label={ t("forms.internship.fields.last-name") } fullWidth component={ TextFieldFormik }/>
</Grid>
<Grid item md={ 8 }>
<Field name="mentorEmail" label={ t("forms.internship.fields.e-mail") } fullWidth component={ TextFieldFormik }/>
</Grid>
<Grid item md={ 4 }>
<Field name="mentorPhone" label={ t("forms.internship.fields.phone") } fullWidth component={ TextFieldFormik }/>
</Grid>
</Grid>
);
}
export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ internship, onChange }) => {
const [company, setCompany] = useProxyState<Company>(internship.company || emptyCompany, company => onChange({ ...internship, company }));
const [mentor, setMentor] = useProxyState<Mentor>(internship.mentor || emptyMentor, mentor => onChange({ ...internship, mentor }));
const [office, setOffice] = useProxyState<BranchOffice>(internship.office || emptyBranchOffice, office => onChange({ ...internship, office }));
export const CompanyForm: React.FunctionComponent = () => {
const { values, setValues, errors, touched, setFieldTouched } = useFormikContext<InternshipFormValues>();
const { t } = useTranslation();
const canEdit = useMemo(() => !company.id, [company.id]);
const fieldProps = formFieldProps(company, setCompany)
const canEdit = useMemo(() => !values.company, [values.company]);
const handleCompanyChange = (event: any, value: Company | string | null) => {
setFieldTouched("companyName", true);
if (typeof value === "string") {
setCompany({
...emptyCompany,
name: value,
});
setValues({
...values,
company: null,
companyName: value
}, true)
} else if (typeof value === "object" && value !== null) {
setCompany(value);
setValues({
...values,
company: value as Company,
companyName: value.name,
companyNip: value.nip,
}, true)
} else {
setCompany(emptyCompany);
setValues({
...values,
company: null,
companyName: "",
}, true);
}
}
@ -143,24 +175,22 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns
<Grid container>
<Grid item>
<Autocomplete options={ sampleCompanies }
getOptionLabel={ option => option.name }
getOptionLabel={ option => typeof option === "string" ? option : option.name }
renderOption={ company => <CompanyItem company={ company }/> }
renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> }
onChange={ handleCompanyChange } value={ company }
renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.company-name") } fullWidth
error={ touched.companyName && !!errors.companyName } helperText={ touched.companyName && errors.companyName }/> }
onChange={ handleCompanyChange } value={ values.company || values.companyName }
freeSolo
/>
</Grid>
<Grid item md={ 4 }>
<TextField label={ "NIP" } fullWidth { ...fieldProps("nip") } disabled={ !canEdit }/>
<Field label={ t("forms.internship.fields.nip") } fullWidth name="companyNip" disabled={ !canEdit } component={ TextFieldFormik }/>
</Grid>
{/*<Grid item md={ 8 }>*/}
{/* <TextField label={ "Url" } fullWidth { ...fieldProps("url") } disabled={ !canEdit }/>*/}
{/*</Grid>*/}
</Grid>
<Typography variant="subtitle1" className="subsection-header">Zakładowy opiekun praktyki</Typography>
<MentorForm mentor={ mentor } onMentorChange={ setMentor }/>
<Typography variant="subtitle1" className="subsection-header">Oddział</Typography>
<BranchForm value={ office } onChange={ setOffice } offices={ company.offices } />
<Typography variant="subtitle1" className="subsection-header">{ t("internship.mentor") }</Typography>
<MentorForm/>
<Typography variant="subtitle1" className="subsection-header">{ t("internship.office") }</Typography>
<BranchForm/>
</>
)
}

View File

@ -1,28 +1,14 @@
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
FormControl,
FormHelperText,
Grid,
Input,
InputLabel,
TextField,
Typography
} from "@material-ui/core";
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 { Course, Internship, InternshipType, internshipTypeLabels } from "@/data";
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 { formFieldProps } from "@/forms/helpers";
import { emptyInternship } from "@/provider/dummy/internship";
import { InternshipProposalActions, useDispatch } from "@/state/actions";
import { useTranslation } from "react-i18next";
@ -30,15 +16,61 @@ import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { route } from "@/routing";
import { useProxyState } from "@/hooks";
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 InternshipFormProps = {}
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;
export type InternshipFormSectionProps = {
internship: Nullable<Internship>,
onChange: (internship: Nullable<Internship>) => void,
// 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>) => {
@ -52,144 +84,250 @@ export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType }
)
}
const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionProps) => {
const fieldProps = formFieldProps(internship, onChange);
const course = internship.intern?.course as Course;
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="Rodzaj praktyki/umowy" 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 }
renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> }
options={ Object.values(InternshipType) as InternshipType[] }
disableClearable
{ ...fieldProps("type", (event, value) => value) as any }
value={ values.kind || undefined }
onChange={ (_, value) => setFieldValue("kind", value) }
onBlur={ handleBlur }
/>
</Grid>
<Grid item md={ 8 }>
{ internship.type === InternshipType.Other && <TextField label={ "Inny - Wprowadź" } fullWidth/> }
{
values.kind === InternshipType.Other &&
<Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } />
}
</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>
)
}
const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionProps) => {
const [startDate, setStartDate] = useProxyState<Moment | null>(internship.startDate, value => onChange({ ...internship, startDate: value }));
const [endDate, setEndDate] = useProxyState<Moment | null>(internship.endDate, value => onChange({ ...internship, endDate: value }));
const InternshipDurationForm = () => {
const { t } = useTranslation();
const {
values: { startDate, endDate, workingHours },
errors,
touched,
setFieldTouched,
setFieldValue
} = useFormikContext<InternshipFormValues>();
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 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 (
<Grid container>
<Grid item md={ 6 }>
<DatePicker value={ startDate } onChange={ setStartDate }
<DatePicker value={ startDate } onChange={ value => setFieldValue("startDate", value) }
format="DD MMMM yyyy"
clearable disableToolbar fullWidth
variant="inline" label={ "Data rozpoczęcia praktyki" }
disableToolbar fullWidth
variant="inline" label={ t("forms.internship.fields.start-date") }
minDate={ moment() }
/>
</Grid>
<Grid item md={ 6 }>
<DatePicker value={ endDate } onChange={ setEndDate }
<DatePicker value={ endDate } onChange={ value => setFieldValue("endDate", value) }
format="DD MMMM yyyy"
clearable disableToolbar fullWidth
variant="inline" label={ "Data zakończenia praktyki" }
disableToolbar fullWidth
variant="inline" label={ t("forms.internship.fields.end-date") }
minDate={ startDate || moment() }
/>
</Grid>
<Grid item md={ 4 }>
<FormControl fullWidth>
<InputLabel>Wymiar etatu</InputLabel>
<Input value={ workingHours }
onChange={ ev => setWorkingHours(parseInt(ev.target.value) || 0) }
fullWidth
/>
<FormHelperText>Liczba godzin w tygodniu roboczym</FormHelperText>
</FormControl>
<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 }>
<FormControl fullWidth>
<InputLabel>Łączna liczba godzin</InputLabel>
<Input value={ hours || "" }
<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) }
fullWidth
/>
</FormControl>
/>
</Grid>
<Grid item md={ 4 }>
<FormControl fullWidth>
<InputLabel>Liczba tygodni</InputLabel>
<Input value={ weeks || "" }
<TextField fullWidth label={ t("forms.internship.fields.weeks") }
value={ weeks || "" }
disabled
fullWidth
/>
<FormHelperText>Wyliczona automatycznie</FormHelperText>
</FormControl>
helperText={ t("forms.internship.help.weeks") }
/>
</Grid>
</Grid>
);
}
export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => {
const initialInternshipState = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || {
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 [internship, setInternship] = useState<Nullable<Internship>>(initialInternshipState)
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 handleSubmit = () => {
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: internship as Internship });
dispatch({
type: InternshipProposalActions.Send,
internship: converter.reverseTransform(values, {
internship: initialInternship as Internship,
}) as Internship
});
history.push(route("home"))
}
const handleSubmitConfirmation = () => {
setConfirmDialogOpen(true);
}
const InnerForm = () => {
const { submitForm, validateForm } = useFormikContext();
const handleCancel = () => {
setConfirmDialogOpen(false);
}
const handleSubmitConfirmation = async () => {
const errors = await validateForm();
return (
<div className="internship-form">
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>
<StudentForm student={ sampleStudent }/>
<StudentForm />
<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>
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
<InternshipDurationForm />
<Typography variant="h3" className="section-header">{ t('internship.sections.place') }</Typography>
<CompanyForm internship={ internship } onChange={ setInternship }/>
<CompanyForm />
<Actions>
<Button variant="contained" color="primary" onClick={ handleSubmitConfirmation }>{ t("confirm") }</Button>
@ -204,10 +342,21 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
</DialogContent>
<DialogActions>
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
<Button color="primary" autoFocus onClick={ handleSubmit }>{ t('confirm') }</Button>
<Button color="primary" autoFocus onClick={ submitForm }>{ t('confirm') }</Button>
</DialogActions>
</Dialog>
</div>
</Form>
}
return (
<Formik initialValues={ values }
onSubmit={ handleSubmit }
validationSchema={ validationSchema }
validateOnChange={ false }
validateOnBlur={ true }
>
<InnerForm />
</Formik>
)
}

View File

@ -1,44 +1,44 @@
import { Course, Student } from "@/data";
import { Course } from "@/data";
import { Button, Grid, TextField } from "@material-ui/core";
import { Alert, Autocomplete } from "@material-ui/lab";
import React from "react";
import { sampleCourse } from "@/provider/dummy/student";
import { useTranslation } from "react-i18next";
import { useFormikContext } from "formik";
import { InternshipFormValues } from "@/forms/internship";
type StudentFormProps = {
student: Student
}
export const StudentForm = () => {
const { t } = useTranslation();
const { values: { student } } = useFormikContext<InternshipFormValues>();
export const StudentForm = ({ student }: StudentFormProps) => {
return (
<>
<Grid container>
<Grid item md={4}>
<TextField label="Imię" value={ student.name } disabled fullWidth/>
</Grid>
<Grid item md={4}>
<TextField label="Nazwisko" value={ student.surname } disabled fullWidth/>
</Grid>
<Grid item md={4}>
<TextField label="Nr Indeksu" value={ student.albumNumber } disabled fullWidth/>
</Grid>
<Grid item md={9}>
<Autocomplete
getOptionLabel={ (course: Course) => course.name }
renderInput={ props => <TextField { ...props } label={ "Kierunek" } fullWidth/> }
options={[ sampleCourse ]}
value={ student.course }
disabled
/>
</Grid>
<Grid item md={3}>
<TextField label="Semestr" value={ student.semester } disabled fullWidth/>
</Grid>
<Grid item>
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
Powyższe dane nie poprawne?
</Alert>
</Grid>
return <>
<Grid container>
<Grid item md={4}>
<TextField label={ t("forms.internship.fields.first-name") } value={ student.name } disabled fullWidth/>
</Grid>
</>
);
<Grid item md={4}>
<TextField label={ t("forms.internship.fields.last-name") } value={ student.surname } disabled fullWidth/>
</Grid>
<Grid item md={4}>
<TextField label={ t("forms.internship.fields.album") } value={ student.albumNumber } disabled fullWidth/>
</Grid>
<Grid item md={9}>
<Autocomplete
getOptionLabel={ (course: Course) => course.name }
renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.course") } fullWidth/> }
options={[ sampleCourse ]}
value={ student.course }
disabled
/>
</Grid>
<Grid item md={3}>
<TextField label={ t("forms.internship.fields.semester") } value={ student.semester } disabled fullWidth/>
</Grid>
<Grid item>
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
Powyższe dane nie poprawne?
</Alert>
</Grid>
</Grid>
</>;
}

View File

@ -1 +1,2 @@
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,7 +4,7 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
import "moment/locale/pl"
import "moment/locale/en-gb"
import moment, { isDuration, isMoment } from "moment";
import moment, { isDuration, isMoment, unitOfTime } from "moment";
import { convertToRoman } from "@/utils/numbers";
const resources = {
@ -22,6 +22,7 @@ i18n
.init({
resources,
fallbackLng: "pl",
compatibilityJSON: "v3",
interpolation: {
escapeValue: false,
format: (value, format, lng) => {
@ -34,7 +35,11 @@ i18n
}
if (isDuration(value)) {
return value.locale(lng || "pl").humanize();
if (format === "humanize") {
return value.locale(lng || "pl").humanize();
} else {
return Math.floor(value.locale(lng || "pl").as(format as unitOfTime.Base));
}
}
return value;

View File

@ -12,7 +12,7 @@ export const SubmitPlanPage = () => {
return <Page title={ t("steps.plan.submit") }>
<Page.Header maxWidth="md">
<Page.Breadcrumbs>
<Link component={ RouterLink } to={ route("home") }>{ t('sections.my-internship.header') }</Link>
<Link component={ RouterLink } to={ route("home") }>{ t('pages.my-internship.header') }</Link>
<Typography color="textPrimary">{ t("steps.plan.submit") }</Typography>
</Page.Breadcrumbs>
<Page.Title>{ t("steps.plan.submit") }</Page.Title>

View File

@ -1,9 +1,22 @@
import { Page } from "@/pages/base";
import { Container, Link, Typography } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import {
Button,
ButtonGroup,
Container,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Link,
Menu,
MenuItem,
TextField,
Typography
} from "@material-ui/core";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { route } from "@/routing";
import { InternshipForm } from "@/forms/internship";
import React from "react";
import React, { useState } from "react";
import { ProposalComment } from "@/pages/steps/proposal";
import { useTranslation } from "react-i18next";
import { ProposalPreview } from "@/components/proposalPreview";
@ -11,15 +24,21 @@ import { useSelector } from "react-redux";
import { Internship } from "@/data";
import { AppState } from "@/state/reducer";
import { internshipSerializationTransformer } from "@/serialization";
import { Actions } from "@/components";
import { InternshipProposalActions, useDispatch } from "@/state/actions";
import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui/index";
import { useVerticalSpacing } from "@/styles";
export const InternshipProposalFormPage = () => {
return <Page title="Zgłoszenie praktyki">
const { t } = useTranslation();
return <Page title={ t("pages.proposal-form.header") }>
<Page.Header maxWidth="md">
<Page.Breadcrumbs>
<Link component={ RouterLink } to={ route("home") }>Moja praktyka</Link>
<Typography color="textPrimary">Zgłoszenie praktyki</Typography>
<Link component={ RouterLink } to={ route("home") }>{ t("pages.my-internship.header") }</Link>
<Typography color="textPrimary">{ t("pages.proposal-form.header") }</Typography>
</Page.Breadcrumbs>
<Page.Title>Zgłoszenie praktyki</Page.Title>
<Page.Title>{ t("pages.proposal-form.header") }</Page.Title>
</Page.Header>
<Container maxWidth={ "md" }>
<ProposalComment />
@ -32,6 +51,57 @@ export const InternshipProposalPreviewPage = () => {
const { t } = useTranslation();
const proposal = useSelector<AppState, Internship | null>(state => state.proposal.proposal && internshipSerializationTransformer.reverseTransform(state.proposal.proposal));
const dispatch = useDispatch();
const history = useHistory();
const [isDiscardModalOpen, setDiscardModelOpen] = useState<boolean>(false);
const [isAcceptModalOpen, setAcceptModelOpen] = useState<boolean>(false);
const [comment, setComment] = useState<string>("");
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
const handleAccept = () => {
dispatch({ type: InternshipProposalActions.Approve, comment });
history.push(route("home"));
}
const handleDiscard = () => {
dispatch({ type: InternshipProposalActions.Decline, comment });
history.push(route("home"));
}
const handleAcceptModalClose = () => {
setAcceptModelOpen(false);
}
const handleDiscardModalClose = () => {
setDiscardModelOpen(false);
}
const handleDiscardAction = () => {
setDiscardModelOpen(true);
}
const handleAcceptMenuOpen = (ev: React.MouseEvent<HTMLElement>) => {
setMenuAnchor(ev.currentTarget);
}
const handleAcceptMenuClose = () => {
setMenuAnchor(null);
}
const handleAcceptWithComment = () => {
setAcceptModelOpen(true);
setMenuAnchor(null);
}
const handleAcceptWithoutComment = () => {
dispatch({ type: InternshipProposalActions.Approve, comment: null });
history.push(route("home"));
}
const classes = useVerticalSpacing(3);
return <Page title={ t("") }>
<Page.Header maxWidth="md">
<Page.Breadcrumbs>
@ -40,10 +110,64 @@ export const InternshipProposalPreviewPage = () => {
</Page.Breadcrumbs>
<Page.Title>Moje zgłoszenie</Page.Title>
</Page.Header>
<Container maxWidth={ "md" }>
<Container maxWidth={ "md" } className={ classes.root }>
<ProposalComment />
{ proposal && <ProposalPreview proposal={ proposal } /> }
<Actions>
<Button component={ RouterLink } to={ route("home") } variant="contained" color="primary">
{ t('go-back') }
</Button>
<ButtonGroup color="primary" variant="contained">
<Button onClick={ handleAcceptWithoutComment } startIcon={ <StickerCheckOutline /> }>
{ t('accept-without-comments') }
</Button>
<Button size="small" onClick={ handleAcceptMenuOpen }><MenuDown /></Button>
</ButtonGroup>
<Menu open={ !!menuAnchor } anchorEl={ menuAnchor } onClose={ handleAcceptMenuClose }>
<MenuItem onClick={ handleAcceptWithoutComment }>{ t("accept-without-comments") }</MenuItem>
<MenuItem onClick={ handleAcceptWithComment }>{ t("accept-with-comments") }</MenuItem>
</Menu>
<Button onClick={ handleDiscardAction } color="secondary" startIcon={ <StickerRemoveOutline /> }>
{ t('discard') }
</Button>
</Actions>
</Container>
<Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md">
<DialogTitle>{ t("internship.discard.title") }</DialogTitle>
<DialogContent className={ classes.root }>
<Typography variant="body1">{ t("internship.discard.info") }</Typography>
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
<DialogActions>
<Button onClick={ handleDiscardModalClose }>
{ t('cancel') }
</Button>
<Button onClick={ handleDiscard } color="primary" variant="contained">
{ t('confirm') }
</Button>
</DialogActions>
</DialogContent>
</Dialog>
<Dialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md">
<DialogTitle>{ t("internship.accept.title") }</DialogTitle>
<DialogContent className={ classes.root }>
<Typography variant="body1">{ t("internship.accept.info") }</Typography>
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") }/>
<DialogActions>
<Button onClick={ handleAcceptModalClose }>
{ t('cancel') }
</Button>
<Button onClick={ handleAccept } color="primary" variant="contained">
{ t('confirm') }
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</Page>
}

View File

@ -25,7 +25,7 @@ export const MainPage = () => {
return <Page my={ 6 }>
<Container>
<Typography variant="h2">{ t("sections.my-internship.header") }</Typography>
<Typography variant="h2">{ t("pages.my-internship.header") }</Typography>
<Stepper orientation="vertical" nonLinear>
<Step label={ t('steps.personal-data.header') } completed={ missingStudentData.length === 0 } until={ deadlines.personalData }>
{ missingStudentData.length > 0 && <>

View File

@ -4,5 +4,6 @@ import moment from "moment";
export const sampleEdition: Edition = {
startDate: moment("2020-07-01"),
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;
export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> }
export type Transformer<TFrom, TResult> = {
transform(subject: TFrom): TResult;
reverseTransform(subject: TResult): TFrom;
export type Transformer<TFrom, TResult, TContext = never> = {
transform(subject: TFrom, context?: TContext): TResult;
reverseTransform(subject: TResult, context?: TContext): TFrom;
}
export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>

View File

@ -8,6 +8,9 @@ export const studentTheme = responsiveFontSizes(createMuiTheme({
},
MuiContainer: {
maxWidth: "md"
},
MuiTextField: {
variant: "outlined",
}
},
palette: {

View File

@ -11,6 +11,41 @@ left: '{{ left, humanize }} left'
dropzone: "Drag and drop a file here or click to choose"
forms:
internship:
fields:
start-date: Internship start date
end-date: Internship end date
working-hours: Working time
total-hours: Total hours
weeks: Total weeks
first-name: First name
last-name: Last name
album: Album number
course: Course
semester: Semester
kind: Contract type
kind-other: Other - please fill
company-name: Company name
nip: NIP
e-mail: e-mail address
phone: Phone number
city: City
postal-code: Postal code
country: Country
street: Street
building: Building
help:
weeks: Calculated automatically
working-hours: Total working hours in working week
send-confirmation: >
Po wysłaniu zgłoszenia nie będzie możliwości jego zmiany do czasu zweryfikowania go przez pełnomocnika ds. Twojego
kierunku. Czy na pewno chcesz wysłać zgłoszenie praktyki w tej formie?
plan:
instructions: >
Wypełnij i zeskanuj Indywidualny program Praktyk a następnie wyślij go z pomocą tego formularza. <więcej informacji>
dropzone-help: Skan dokumentu w formacie PDF
student:
name: first name
surname: last name
@ -19,7 +54,41 @@ student:
email: e-mail
albumNumber: album number
sections:
internship:
intern:
semester: semesetr {{ semester, roman }}
album: "album number {{ album }}"
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
duration: "{{ duration, weeks }} week"
duration_plural: "{{ duration, weeks }} weeks"
hours: "{{ hours }} hour"
hours_plural: "{{ hours }} hours"
office: "Office / Address"
mentor: "Internship mentor"
address:
city: "{{ city }}, {{ country }}"
street: "{{ postalCode }}, {{ street }} {{ building }}"
sections:
intern-info: "Intern personal data"
duration: "Internship duration"
place: "Internship place"
kind: "Contract and programme"
mentor: "Internship mentor"
discard:
title: "Discard internship proposal"
info: "This comments will be presented to student in order to fix errors."
accept:
title: "Accept internship proposal"
info: "This comments will be presented to student."
submission:
status:
awaiting: "sent, awaiting verification"
accepted: "accepted"
declined: "needs correction"
draft: "draft"
pages:
my-internship:
header: "My internship"
@ -29,10 +98,22 @@ steps:
info: >
Your profile is incomplete. In order to continue your internship you have to supply information given below. In
case of problem with providing those information - please contact with your internship coordinator of your course.
form: "Add missing data"
internship-proposal:
header: "Internship proposal"
form: "Internship proposal form"
info: ""
info:
draft: >
Przed podjęciem praktyki należy ją zgłosić. (TODO)
awaiting: >
Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o
akceptacji bądź konieczności wprowadzenia zmian.
accepted: >
Twoje zgłoszenie zostało zweryfikowane i zaakceptowane.
declined: >
Twoje zgłoszenie zostało zweryfikowane i odrzucone. Popraw zgłoszone uwagi i wyślij zgłoszenie ponownie. W razie
pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku.
action: "Send internship proposal"
plan:
header: "Individual Internship Plan"
info: ""

View File

@ -20,15 +20,46 @@ comments: Zgłoszone uwagi
send-again: wyślij ponownie
cancel: anuluj
accept: zaakceptuj
accept-with-comments: zaakceptuj z uwagami
accept-without-comments: zaakceptuj bez uwag
discard: zgłoś uwagi
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
sections:
pages:
my-internship:
header: "Moja praktyka"
proposal-form:
header: "Propose internship"
forms:
internship:
fields:
start-date: Data rozpoczęcia praktyki
end-date: Data zakończenia praktyki
working-hours: Wymiar etatu
total-hours: Łączna liczba godzin
weeks: Liczba tygodni
first-name: Imię
last-name: Nazwisko
album: Numer albumu
course: Kierunek
semester: Semestr
kind: Rodzaj praktyki/umowy
kind-other: Inny - wprowadź
company-name: Nazwa firmy
nip: NIP
e-mail: Kontaktowy adres e-mail
phone: Numer telefonu
city: Miasto
postal-code: Kod pocztowy
country: Kraj
street: Ulica
building: Nr budynku
help:
weeks: Wartość wyliczana automatycznie
working-hours: Liczba godzin w tygodniu roboczym
send-confirmation: >
Po wysłaniu zgłoszenia nie będzie możliwości jego zmiany do czasu zweryfikowania go przez pełnomocnika ds. Twojego
kierunku. Czy na pewno chcesz wysłać zgłoszenie praktyki w tej formie?
@ -57,9 +88,14 @@ internship:
semester: semestr {{ semester, roman }}
album: "numer albumu {{ album }}"
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
duration: "{{ duration, humanize }}"
hours: "{{ hours }} godzin"
duration_2: "{{ duration, weeks }} tygodni"
duration_0: "{{ duration, weeks }} tydzień"
duration_1: "{{ duration, weeks }} tygodnie"
hours_2: "{{ hours }} godzin"
hours_0: "{{ hours }} godzina"
hours_1: "{{ hours }} godziny"
office: "Oddział / adres"
mentor: "Zakładowy opiekun praktyki"
address:
city: "{{ city }}, {{ country }}"
street: "{{ postalCode }}, {{ street }} {{ building }}"
@ -69,7 +105,12 @@ internship:
place: "Miejsce odbywania praktyki"
kind: "Rodzaj i program praktyki"
mentor: "Zakładowy opiekun praktyki"
discard:
title: "Odrzuć zgłoszenie praktyki"
info: "Poniższa informacja zostanie przekazana praktykantowi w celu poprawy zgłoszenia."
accept:
title: "Zaakceptuj zgłoszenie praktyki"
info: "Poniższa informacja zostanie przekazana praktykantowi."
steps:
personal-data:
@ -118,4 +159,11 @@ steps:
instructions: >
papierki do podpisania...
validation:
required: "To pole jest wymagane"
email: "Wprowadź poprawny adres e-mail"
phone: "Wprowadź poprawny numer telefonu"
internship:
minimum-hours: "Minimalna liczba godzin wynosi {{ hours }}"
contact-coordinator: "Skontaktuj się z koordynatorem"

View File

@ -933,6 +933,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.1", "@babel/template@^7.8.6":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
@ -1311,6 +1318,11 @@
"@types/webpack-sources" "*"
source-map "^0.6.0"
"@types/yup@^0.29.4":
version "0.29.4"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.4.tgz#f7b1f9978180d5155663c1cd0ecdc41a72c23d81"
integrity sha512-OQ7gZRQb7eSbGu5h57tbK67sgX8UH5wbuqPORTFBG7qiBtOkEf1dXAr0QULyHIeRwaGLPYxPXiQru+40ClR6ng==
"@typescript-eslint/eslint-plugin@^2.10.0":
version "2.34.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9"
@ -3170,6 +3182,11 @@ deep-equal@^1.0.1:
object-keys "^1.1.1"
regexp.prototype.flags "^1.2.0"
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
default-gateway@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
@ -3914,6 +3931,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
fn-name@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
follow-redirects@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb"
@ -3954,6 +3976,25 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formik-material-ui@^3.0.0-alpha.0:
version "3.0.0-alpha.0"
resolved "https://registry.yarnpkg.com/formik-material-ui/-/formik-material-ui-3.0.0-alpha.0.tgz#4020b5cbd9e431406fb275a317cdce95ad398545"
integrity sha512-N9JcSngi4nWaKN67sN1M3ILXgz0fLCdoMhHHecrZC3NeR+C5lWWAUuAcjGZWNj+z87Qt7NW8VXlxSnGxGus8Uw==
formik@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.1.5.tgz#de5bbbe35543fa6d049fe96b8ee329d6cd6892b8"
integrity sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==
dependencies:
deepmerge "^2.1.1"
hoist-non-react-statics "^3.3.0"
lodash "^4.17.14"
lodash-es "^4.17.14"
react-fast-compare "^2.0.1"
scheduler "^0.18.0"
tiny-warning "^1.0.2"
tslib "^1.10.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5364,6 +5405,11 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.11, lodash-es@^4.17.14:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -7192,6 +7238,11 @@ prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
property-expr@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"
integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g==
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@ -7400,6 +7451,11 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-i18next@^11.7.0:
version "11.7.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.7.0.tgz#f27c4c237a274e007a48ac1210db83e33719908b"
@ -7888,6 +7944,14 @@ sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
@ -8554,6 +8618,11 @@ symbol-observable@^1.2.0:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
synchronous-promise@^2.0.13:
version "2.0.13"
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
tapable@^1.0.0, tapable@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
@ -8696,6 +8765,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@ -9469,3 +9543,16 @@ yargs@^13.3.2:
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.1.2"
yup@^0.29.3:
version "0.29.3"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"
integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==
dependencies:
"@babel/runtime" "^7.10.5"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.13"
toposort "^2.0.2"