diff --git a/package.json b/package.json index 1379ef1..c5eba09 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/actions.tsx b/src/components/actions.tsx index 34357c3..79ce5e2 100644 --- a/src/components/actions.tsx +++ b/src/components/actions.tsx @@ -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" }}/> } diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx index d0b8870..096779e 100644 --- a/src/components/proposalPreview.tsx +++ b/src/components/proposalPreview.tsx @@ -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> } diff --git a/src/data/common.ts b/src/data/common.ts index 62671b8..5a735c4 100644 --- a/src/data/common.ts +++ b/src/data/common.ts @@ -1,3 +1,5 @@ +export type Identifier = string; + export interface Identifiable { - id?: string + id?: Identifier } diff --git a/src/data/company.ts b/src/data/company.ts index 5e6f2db..c799e1b 100644 --- a/src/data/company.ts +++ b/src/data/company.ts @@ -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, } diff --git a/src/data/edition.ts b/src/data/edition.ts index 7fc5438..0de2052 100644 --- a/src/data/edition.ts +++ b/src/data/edition.ts @@ -4,6 +4,8 @@ export type Edition = { startDate: Moment; endDate: Moment; proposalDeadline: Moment; + minimumInternshipHours: number; + maximumInternshipHours?: number; } export type Deadlines = { diff --git a/src/data/internship.ts b/src/data/internship.ts index ace8c57..4cd82ea 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -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 { diff --git a/src/forms/company.tsx b/src/forms/company.tsx index a728935..74fbfdb 100644 --- a/src/forms/company.tsx +++ b/src/forms/company.tsx @@ -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/> </> ) } diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index f4d2324..7f0b103 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -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> ) } diff --git a/src/forms/student.tsx b/src/forms/student.tsx index da722e8..1c8ee26 100644 --- a/src/forms/student.tsx +++ b/src/forms/student.tsx @@ -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 są 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 są poprawne? + </Alert> + </Grid> + </Grid> + </>; } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 283d0e1..9e7f847 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from "./useProxyState" +export * from "./useUpdateEffect" diff --git a/src/hooks/useUpdateEffect.ts b/src/hooks/useUpdateEffect.ts new file mode 100644 index 0000000..939b1c0 --- /dev/null +++ b/src/hooks/useUpdateEffect.ts @@ -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; diff --git a/src/i18n.ts b/src/i18n.ts index c01bc57..84cc091 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -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; diff --git a/src/pages/internship/plan.tsx b/src/pages/internship/plan.tsx index e09a214..5706817 100644 --- a/src/pages/internship/plan.tsx +++ b/src/pages/internship/plan.tsx @@ -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> diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index 09b8ca8..939998b 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -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> } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 5d2da88..f03e9b3 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -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 && <> diff --git a/src/provider/dummy/edition.ts b/src/provider/dummy/edition.ts index 290a3ac..e53bd4e 100644 --- a/src/provider/dummy/edition.ts +++ b/src/provider/dummy/edition.ts @@ -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, } diff --git a/src/serialization/types.ts b/src/serialization/types.ts index 94e443d..b7528c6 100644 --- a/src/serialization/types.ts +++ b/src/serialization/types.ts @@ -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> diff --git a/src/ui/theme.ts b/src/ui/theme.ts index 8f770f0..cf65aad 100644 --- a/src/ui/theme.ts +++ b/src/ui/theme.ts @@ -8,6 +8,9 @@ export const studentTheme = responsiveFontSizes(createMuiTheme({ }, MuiContainer: { maxWidth: "md" + }, + MuiTextField: { + variant: "outlined", } }, palette: { diff --git a/translations/en.yaml b/translations/en.yaml index 902dbbd..1fc2920 100644 --- a/translations/en.yaml +++ b/translations/en.yaml @@ -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: "" diff --git a/translations/pl.yaml b/translations/pl.yaml index c52c43d..9bc83eb 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -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" diff --git a/yarn.lock b/yarn.lock index 697aea8..634fa14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"