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

View File

@ -2,7 +2,7 @@ import React, { HTMLProps } from "react";
import { useHorizontalSpacing } from "@/styles"; import { useHorizontalSpacing } from "@/styles";
export const Actions = (props: HTMLProps<HTMLDivElement>) => { 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 { Internship, internshipTypeLabels } from "@/data";
import React from "react"; 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 { useTranslation } from "react-i18next";
import { createStyles, makeStyles } from "@material-ui/core/styles"; import { createStyles, makeStyles } from "@material-ui/core/styles";
import classNames from "classnames"; 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 { useVerticalSpacing } from "@/styles";
import moment from "moment"; import moment from "moment";
@ -36,7 +33,6 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const classes = useVerticalSpacing(3); const classes = useVerticalSpacing(3);
const duration = moment.duration(proposal.endDate.diff(proposal.startDate)); const duration = moment.duration(proposal.endDate.diff(proposal.startDate));
return <div className={ classNames("proposal", classes.root) }> 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 }) } { t('internship.date-range', { start: proposal.startDate, end: proposal.endDate }) }
</Typography> </Typography>
<Typography className="proposal__secondary"> <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> </Typography>
</Section> </Section>
@ -85,11 +81,5 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
<Typography className="proposal__primary">{ proposal.mentor.name } { proposal.mentor.surname }</Typography> <Typography className="proposal__primary">{ proposal.mentor.name } { proposal.mentor.surname }</Typography>
<Typography className="proposal__secondary">{ proposal.mentor.email }, { proposal.mentor.phone }</Typography> <Typography className="proposal__secondary">{ proposal.mentor.email }, { proposal.mentor.phone }</Typography>
</Section> </Section>
<Actions>
<Button component={ RouterLink } to={ route("home") } variant="contained" color="primary">
{ t('go-back') }
</Button>
</Actions>
</div> </div>
} }

View File

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

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,19 +1,12 @@
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 { useTranslation } from "react-i18next";
import { emptyMentor } from "@/provider/dummy/internship"; import { Field, useFormikContext } from "formik";
import { useProxyState } from "@/hooks"; 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 }>
@ -22,42 +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 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 (
@ -68,73 +84,89 @@ 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={ "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 } 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={ "Kod pocztowy" } 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={ "Kraj" } 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={ "Ulica" } 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={ "Nr Budynku" } 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>
) )
} }
export const MentorForm = ({ mentor, onMentorChange }: BoundProperty<Mentor, 'onMentorChange', 'mentor'>) => { export const MentorForm = () => {
const fieldProps = formFieldProps(mentor, onMentorChange) const { t } = useTranslation();
return ( return (
<>
<Grid container> <Grid container>
<Grid item md={6}> <Grid item md={ 6 }>
<TextField label="Imię" fullWidth { ...fieldProps("name") }/> <Field name="mentorFirstName" label={ t("forms.internship.fields.first-name") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
<Grid item md={6}> <Grid item md={ 6 }>
<TextField label="Nazwisko" value={ mentor.surname } fullWidth { ...fieldProps("surname") }/> <Field name="mentorLastName" label={ t("forms.internship.fields.last-name") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
<Grid item md={8}> <Grid item md={ 8 }>
<TextField label="E-mail" value={ mentor.email } fullWidth { ...fieldProps("email") }/> <Field name="mentorEmail" label={ t("forms.internship.fields.e-mail") } fullWidth component={ TextFieldFormik }/>
</Grid> </Grid>
<Grid item md={4}> <Grid item md={ 4 }>
<TextField label="Nr telefonu" value={ mentor.phone } fullWidth { ...fieldProps("phone") }/> <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 { t } = useTranslation();
const [office, setOffice] = useProxyState<BranchOffice>(internship.office || emptyBranchOffice, office => onChange({ ...internship, office }));
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);
} }
} }
@ -143,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={ "Nazwa firmy" } 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={ "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">Zakładowy opiekun praktyki</Typography> <Typography variant="subtitle1" className="subsection-header">{ t("internship.mentor") }</Typography>
<MentorForm mentor={ mentor } onMentorChange={ setMentor }/> <MentorForm/>
<Typography variant="subtitle1" className="subsection-header">Oddział</Typography> <Typography variant="subtitle1" className="subsection-header">{ t("internship.office") }</Typography>
<BranchForm value={ office } onChange={ setOffice } offices={ company.offices } /> <BranchForm/>
</> </>
) )
} }

View File

@ -1,28 +1,14 @@
import React, { HTMLProps, useEffect, useMemo, useState } from "react"; import React, { HTMLProps, useMemo, useState } from "react";
import { import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Grid, TextField, Typography } from "@material-ui/core";
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
FormControl,
FormHelperText,
Grid,
Input,
InputLabel,
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 { Course, Internship, InternshipType, internshipTypeLabels } 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";
@ -30,15 +16,61 @@ 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 { 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 = { // relations
internship: Nullable<Internship>, kind: InternshipType | null;
onChange: (internship: Nullable<Internship>) => void, 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>) => { 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 InternshipProgramForm = () => {
const fieldProps = formFieldProps(internship, onChange); const { t } = useTranslation();
const { values, handleBlur, setFieldValue, errors } = useFormikContext<InternshipFormValues>();
const course = internship.intern?.course as Course;
return ( return (
<Grid container> <Grid container>
<Grid item md={ 4 }> <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 } 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={ "Inny - Wprowadź" } 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 [startDate, setStartDate] = useProxyState<Moment | null>(internship.startDate, value => onChange({ ...internship, startDate: value })); const { t } = useTranslation();
const [endDate, setEndDate] = useProxyState<Moment | null>(internship.endDate, value => onChange({ ...internship, endDate: value })); const {
values: { startDate, endDate, workingHours },
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={ "Data rozpoczęcia praktyki" } variant="inline" label={ t("forms.internship.fields.start-date") }
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={ "Data zakończenia praktyki" } 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 }>
<FormControl fullWidth> <Field component={ TextFieldFormik }
<InputLabel>Wymiar etatu</InputLabel> name="workingHours"
<Input value={ workingHours } label={ t("forms.internship.fields.working-hours") }
onChange={ ev => setWorkingHours(parseInt(ev.target.value) || 0) } helperText={ t("forms.internship.help.working-hours") }
fullWidth fullWidth
/> />
<FormHelperText>Liczba godzin w tygodniu roboczym</FormHelperText>
</FormControl>
</Grid> </Grid>
<Grid item md={ 4 }> <Grid item md={ 4 }>
<FormControl fullWidth> <TextField fullWidth
<InputLabel>Łączna liczba godzin</InputLabel> label={ t("forms.internship.fields.total-hours") }
<Input value={ hours || "" } error={ !!errors.hours && touched.hours }
helperText={ touched.hours && errors.hours }
value={ hours || "" }
onChange={ ev => setHoursOverride(parseInt(ev.target.value) || 0) } onChange={ ev => setHoursOverride(parseInt(ev.target.value) || 0) }
fullWidth
/> />
</FormControl>
</Grid> </Grid>
<Grid item md={ 4 }> <Grid item md={ 4 }>
<FormControl fullWidth> <TextField fullWidth label={ t("forms.internship.fields.weeks") }
<InputLabel>Liczba tygodni</InputLabel> value={ weeks || "" }
<Input value={ weeks || "" }
disabled disabled
fullWidth helperText={ t("forms.internship.help.weeks") }
/> />
<FormHelperText>Wyliczona automatycznie</FormHelperText>
</FormControl>
</Grid> </Grid>
</Grid> </Grid>
); );
} }
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>>({
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); 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")) history.push(route("home"))
} }
const handleSubmitConfirmation = () => { const InnerForm = () => {
const { submitForm, validateForm } = useFormikContext();
const handleSubmitConfirmation = async () => {
const errors = await validateForm();
if (Object.keys(errors).length == 0) {
setConfirmDialogOpen(true); setConfirmDialogOpen(true);
} }
}
const handleCancel = () => { const handleCancel = () => {
setConfirmDialogOpen(false); setConfirmDialogOpen(false);
} }
return ( return <Form>
<div className="internship-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={ handleSubmitConfirmation }>{ t("confirm") }</Button> <Button variant="contained" color="primary" onClick={ handleSubmitConfirmation }>{ t("confirm") }</Button>
@ -204,10 +342,21 @@ 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={ handleSubmit }>{ t('confirm') }</Button> <Button color="primary" autoFocus onClick={ submitForm }>{ t('confirm') }</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</div> </Form>
}
return (
<Formik initialValues={ values }
onSubmit={ handleSubmit }
validationSchema={ validationSchema }
validateOnChange={ false }
validateOnBlur={ true }
>
<InnerForm />
</Formik>
) )
} }

View File

@ -1,37 +1,38 @@
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 { useFormikContext } from "formik";
import { InternshipFormValues } from "@/forms/internship";
type StudentFormProps = { export const StudentForm = () => {
student: Student const { t } = useTranslation();
} const { values: { student } } = useFormikContext<InternshipFormValues>();
export const StudentForm = ({ student }: StudentFormProps) => { return <>
return (
<>
<Grid container> <Grid container>
<Grid item md={4}> <Grid item md={4}>
<TextField label="Imię" value={ student.name } disabled fullWidth/> <TextField label={ t("forms.internship.fields.first-name") } value={ student.name } disabled fullWidth/>
</Grid> </Grid>
<Grid item md={4}> <Grid item md={4}>
<TextField label="Nazwisko" value={ student.surname } disabled fullWidth/> <TextField label={ t("forms.internship.fields.last-name") } value={ student.surname } disabled fullWidth/>
</Grid> </Grid>
<Grid item md={4}> <Grid item md={4}>
<TextField label="Nr Indeksu" value={ student.albumNumber } disabled fullWidth/> <TextField label={ t("forms.internship.fields.album") } value={ student.albumNumber } disabled fullWidth/>
</Grid> </Grid>
<Grid item md={9}> <Grid item md={9}>
<Autocomplete <Autocomplete
getOptionLabel={ (course: Course) => course.name } getOptionLabel={ (course: Course) => course.name }
renderInput={ props => <TextField { ...props } label={ "Kierunek" } fullWidth/> } renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.course") } fullWidth/> }
options={[ sampleCourse ]} options={[ sampleCourse ]}
value={ student.course } value={ student.course }
disabled disabled
/> />
</Grid> </Grid>
<Grid item md={3}> <Grid item md={3}>
<TextField label="Semestr" value={ student.semester } disabled fullWidth/> <TextField label={ t("forms.internship.fields.semester") } value={ student.semester } disabled fullWidth/>
</Grid> </Grid>
<Grid item> <Grid item>
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }> <Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
@ -39,6 +40,5 @@ export const StudentForm = ({ student }: StudentFormProps) => {
</Alert> </Alert>
</Grid> </Grid>
</Grid> </Grid>
</> </>;
);
} }

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

View File

@ -12,7 +12,7 @@ export const SubmitPlanPage = () => {
return <Page title={ t("steps.plan.submit") }> return <Page title={ t("steps.plan.submit") }>
<Page.Header maxWidth="md"> <Page.Header maxWidth="md">
<Page.Breadcrumbs> <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> <Typography color="textPrimary">{ t("steps.plan.submit") }</Typography>
</Page.Breadcrumbs> </Page.Breadcrumbs>
<Page.Title>{ t("steps.plan.submit") }</Page.Title> <Page.Title>{ t("steps.plan.submit") }</Page.Title>

View File

@ -1,9 +1,22 @@
import { Page } from "@/pages/base"; import { Page } from "@/pages/base";
import { Container, Link, Typography } from "@material-ui/core"; import {
import { Link as RouterLink } from "react-router-dom"; 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 { route } from "@/routing";
import { InternshipForm } from "@/forms/internship"; import { InternshipForm } from "@/forms/internship";
import React from "react"; import React, { useState } from "react";
import { ProposalComment } from "@/pages/steps/proposal"; import { ProposalComment } from "@/pages/steps/proposal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ProposalPreview } from "@/components/proposalPreview"; import { ProposalPreview } from "@/components/proposalPreview";
@ -11,15 +24,21 @@ import { useSelector } from "react-redux";
import { Internship } from "@/data"; import { Internship } from "@/data";
import { AppState } from "@/state/reducer"; import { AppState } from "@/state/reducer";
import { internshipSerializationTransformer } from "@/serialization"; 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 = () => { 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.Header maxWidth="md">
<Page.Breadcrumbs> <Page.Breadcrumbs>
<Link component={ RouterLink } to={ route("home") }>Moja praktyka</Link> <Link component={ RouterLink } to={ route("home") }>{ t("pages.my-internship.header") }</Link>
<Typography color="textPrimary">Zgłoszenie praktyki</Typography> <Typography color="textPrimary">{ t("pages.proposal-form.header") }</Typography>
</Page.Breadcrumbs> </Page.Breadcrumbs>
<Page.Title>Zgłoszenie praktyki</Page.Title> <Page.Title>{ t("pages.proposal-form.header") }</Page.Title>
</Page.Header> </Page.Header>
<Container maxWidth={ "md" }> <Container maxWidth={ "md" }>
<ProposalComment /> <ProposalComment />
@ -32,6 +51,57 @@ export const InternshipProposalPreviewPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const proposal = useSelector<AppState, Internship | null>(state => state.proposal.proposal && internshipSerializationTransformer.reverseTransform(state.proposal.proposal)); 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("") }> return <Page title={ t("") }>
<Page.Header maxWidth="md"> <Page.Header maxWidth="md">
<Page.Breadcrumbs> <Page.Breadcrumbs>
@ -40,10 +110,64 @@ export const InternshipProposalPreviewPage = () => {
</Page.Breadcrumbs> </Page.Breadcrumbs>
<Page.Title>Moje zgłoszenie</Page.Title> <Page.Title>Moje zgłoszenie</Page.Title>
</Page.Header> </Page.Header>
<Container maxWidth={ "md" }> <Container maxWidth={ "md" } className={ classes.root }>
<ProposalComment /> <ProposalComment />
{ proposal && <ProposalPreview proposal={ proposal } /> } { 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> </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> </Page>
} }

View File

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

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

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

View File

@ -11,6 +11,41 @@ left: '{{ left, humanize }} left'
dropzone: "Drag and drop a file here or click to choose" 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: student:
name: first name name: first name
surname: last name surname: last name
@ -19,7 +54,41 @@ student:
email: e-mail email: e-mail
albumNumber: album number 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: my-internship:
header: "My internship" header: "My internship"
@ -29,10 +98,22 @@ steps:
info: > info: >
Your profile is incomplete. In order to continue your internship you have to supply information given below. In 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. case of problem with providing those information - please contact with your internship coordinator of your course.
form: "Add missing data"
internship-proposal: internship-proposal:
header: "Internship proposal" header: "Internship proposal"
form: "Internship proposal form" 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: plan:
header: "Individual Internship Plan" header: "Individual Internship Plan"
info: "" info: ""

View File

@ -20,15 +20,46 @@ comments: Zgłoszone uwagi
send-again: wyślij ponownie send-again: wyślij ponownie
cancel: anuluj 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ć" dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
sections: pages:
my-internship: my-internship:
header: "Moja praktyka" header: "Moja praktyka"
proposal-form:
header: "Propose internship"
forms: forms:
internship: 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: > send-confirmation: >
Po wysłaniu zgłoszenia nie będzie możliwości jego zmiany do czasu zweryfikowania go przez pełnomocnika ds. Twojego 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? kierunku. Czy na pewno chcesz wysłać zgłoszenie praktyki w tej formie?
@ -57,9 +88,14 @@ internship:
semester: semestr {{ semester, roman }} semester: semestr {{ semester, roman }}
album: "numer albumu {{ album }}" album: "numer albumu {{ album }}"
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}" date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
duration: "{{ duration, humanize }}" duration_2: "{{ duration, weeks }} tygodni"
hours: "{{ hours }} godzin" 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" office: "Oddział / adres"
mentor: "Zakładowy opiekun praktyki"
address: address:
city: "{{ city }}, {{ country }}" city: "{{ city }}, {{ country }}"
street: "{{ postalCode }}, {{ street }} {{ building }}" street: "{{ postalCode }}, {{ street }} {{ building }}"
@ -69,7 +105,12 @@ internship:
place: "Miejsce odbywania praktyki" place: "Miejsce odbywania praktyki"
kind: "Rodzaj i program praktyki" kind: "Rodzaj i program praktyki"
mentor: "Zakładowy opiekun 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: steps:
personal-data: personal-data:
@ -118,4 +159,11 @@ steps:
instructions: > instructions: >
papierki do podpisania... 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" contact-coordinator: "Skontaktuj się z koordynatorem"

View File

@ -933,6 +933,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" 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": "@babel/template@^7.10.1", "@babel/template@^7.8.6":
version "7.10.1" version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
@ -1311,6 +1318,11 @@
"@types/webpack-sources" "*" "@types/webpack-sources" "*"
source-map "^0.6.0" 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": "@typescript-eslint/eslint-plugin@^2.10.0":
version "2.34.0" version "2.34.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" 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" object-keys "^1.1.1"
regexp.prototype.flags "^1.2.0" 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: default-gateway@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" 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" inherits "^2.0.3"
readable-stream "^2.3.6" 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: follow-redirects@^1.0.0:
version "1.11.0" version "1.11.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb" 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" combined-stream "^1.0.6"
mime-types "^2.1.12" 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: forwarded@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5364,6 +5405,11 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" 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: lodash._reinterpolate@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" 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" object-assign "^4.1.1"
react-is "^16.8.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: proxy-addr@~2.0.5:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" 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" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA== 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: react-i18next@^11.7.0:
version "11.7.0" version "11.7.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.7.0.tgz#f27c4c237a274e007a48ac1210db83e33719908b" 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" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== 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: scheduler@^0.19.1:
version "0.19.1" version "0.19.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" 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" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== 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: tapable@^1.0.0, tapable@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" 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" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 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: tough-cookie@~2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" 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" which-module "^2.0.0"
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^13.1.2" 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"