diff --git a/.build/deploy.sh b/.build/deploy.sh index 36f34bb..89c7bba 100755 --- a/.build/deploy.sh +++ b/.build/deploy.sh @@ -2,5 +2,6 @@ BUILD_PATH=$1 DEPLOY_PATH=$2 # copy all dist files to deploy path +rsync -avz $BUILD_PATH/public/* $DEPLOY_PATH rsync -avz $BUILD_PATH/build/* $DEPLOY_PATH diff --git a/src/components/actions.tsx b/src/components/actions.tsx index 5fdb03d..34357c3 100644 --- a/src/components/actions.tsx +++ b/src/components/actions.tsx @@ -1,14 +1,8 @@ import React, { HTMLProps } from "react"; -import { makeStyles } from "@material-ui/core/styles"; +import { useHorizontalSpacing } from "@/styles"; export const Actions = (props: HTMLProps<HTMLDivElement>) => { - const classes = makeStyles(theme => ({ - root: { - "& > *": { - marginRight: theme.spacing(1) - } - } - }))(); + const classes = useHorizontalSpacing(1); return <div className={ classes.root } { ...props }/> } diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx new file mode 100644 index 0000000..d0b8870 --- /dev/null +++ b/src/components/proposalPreview.tsx @@ -0,0 +1,95 @@ +import { Internship, internshipTypeLabels } from "@/data"; +import React from "react"; +import { Button, 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"; + +export type ProposalPreviewProps = { + proposal: Internship; +} + +const Label = ({ children }: TypographyProps) => { + return <Typography variant="subtitle2" className="proposal__header">{ children }</Typography> +} + +const useSectionStyles = makeStyles(theme => createStyles({ + root: { + padding: theme.spacing(2), + } +})) + +const Section = ({ children, ...props }: PaperProps) => { + const classes = useSectionStyles(); + + return <Paper {...props} className={ classNames(classes.root, props.className ) }> + { children } + </Paper> +} + +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) }> + <div> + <Typography className="proposal__primary">{ proposal.intern.name } { proposal.intern.surname }</Typography> + <Typography className="proposal__secondary"> + { t('internship.intern.semester', { semester: proposal.intern.semester }) } + { ", " } + { t('internship.intern.album', { album: proposal.intern.albumNumber }) } + </Typography> + </div> + + <Section> + <Label>{ t('internship.sections.place') }</Label> + <Typography className="proposal__primary"> + { proposal.company.name } + </Typography> + <Typography className="proposal__secondary"> + NIP: { proposal.company.nip } + </Typography> + + <Label>{ t('internship.office') }</Label> + <Typography className="proposal__primary">{ t('internship.address.city', proposal.office.address) }</Typography> + <Typography className="proposal__secondary">{ t('internship.address.street', proposal.office.address) }</Typography> + </Section> + + <Section> + <Label>{ t('internship.sections.kind') }</Label> + <Typography className="proposal__primary">{ internshipTypeLabels[proposal.type].label }</Typography> + </Section> + + <Section> + <Label>{ t('internship.sections.duration') }</Label> + <Typography className="proposal__primary"> + { t('internship.date-range', { start: proposal.startDate, end: proposal.endDate }) } + </Typography> + <Typography className="proposal__secondary"> + { t('internship.duration', { duration }) } + { ", " } + { t('internship.hours', { hours: proposal.hours }) } + </Typography> + </Section> + + <Section> + <Label>{ t('internship.sections.mentor') }</Label> + <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/edition.ts b/src/data/edition.ts index 8a29c94..7fc5438 100644 --- a/src/data/edition.ts +++ b/src/data/edition.ts @@ -11,6 +11,7 @@ export type Deadlines = { proposal?: Moment; personalPlan?: Moment; report?: Moment; + insurance?: Moment; } export function getEditionDeadlines(edition: Edition): Deadlines { diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 01b8689..f4d2324 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -1,5 +1,18 @@ import React, { HTMLProps, useEffect, useMemo, useState } from "react"; -import { Button, FormControl, FormHelperText, Grid, Input, InputLabel, TextField, Typography } from "@material-ui/core"; +import { + 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 { CompanyForm } from "@/forms/company"; import { StudentForm } from "@/forms/student"; @@ -46,30 +59,30 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr return ( <Grid container> - <Grid item md={4}> + <Grid item md={ 4 }> <Autocomplete renderInput={ props => <TextField { ...props } label="Rodzaj praktyki/umowy" fullWidth/> } getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label } - renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option } /> } + renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> } options={ Object.values(InternshipType) as InternshipType[] } disableClearable { ...fieldProps("type", (event, value) => value) as any } /> </Grid> - <Grid item md={8}> - { internship.type === InternshipType.Other && <TextField label={"Inny - Wprowadź"} fullWidth/> } + <Grid item md={ 8 }> + { internship.type === InternshipType.Other && <TextField label={ "Inny - Wprowadź" } fullWidth/> } </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 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> ) } @@ -84,7 +97,7 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP 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 / 40) : null, [hours]); useEffect(() => onChange({ ...internship, hours }), [hours]) @@ -139,7 +152,10 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP } export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => { - const initialInternshipState = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, intern: sampleStudent }); + const initialInternshipState = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || { + ...emptyInternship, + intern: sampleStudent + }); const [internship, setInternship] = useState<Nullable<Internship>>(initialInternshipState) const { t } = useTranslation(); @@ -147,28 +163,50 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop const dispatch = useDispatch(); const history = useHistory(); + const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false); + const handleSubmit = () => { + setConfirmDialogOpen(false); + dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship }); history.push(route("home")) } + const handleSubmitConfirmation = () => { + setConfirmDialogOpen(true); + } + + const handleCancel = () => { + setConfirmDialogOpen(false); + } + return ( <div className="internship-form"> - <Typography variant="h3" className="section-header">Dane osoby odbywającej praktykę</Typography> + <Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography> <StudentForm student={ sampleStudent }/> - <Typography variant="h3" className="section-header">Rodzaj i program praktyki</Typography> + <Typography variant="h3" className="section-header">{ t('internship.sections.kind' )}</Typography> <InternshipProgramForm internship={ internship } onChange={ setInternship }/> - <Typography variant="h3" className="section-header">Czas trwania praktyki</Typography> + <Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography> <InternshipDurationForm internship={ internship } onChange={ setInternship }/> - <Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography> + <Typography variant="h3" className="section-header">{ t('internship.sections.place') }</Typography> <CompanyForm internship={ internship } onChange={ setInternship }/> <Actions> - <Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button> + <Button variant="contained" color="primary" onClick={ handleSubmitConfirmation }>{ t("confirm") }</Button> <Button component={ RouterLink } to={ route("home") }> { t('go-back') } </Button> </Actions> + + <Dialog open={ confirmDialogOpen } onClose={ handleCancel }> + <DialogContent> + <DialogContentText>{ t('forms.internship.send-confirmation') }</DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={ handleCancel }>{ t('cancel') }</Button> + <Button color="primary" autoFocus onClick={ handleSubmit }>{ t('confirm') }</Button> + </DialogActions> + </Dialog> </div> ) } diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 24a8fa7..0c96203 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -32,7 +32,7 @@ export const PlanForm = () => { </Button> </Grid> <Grid item> - <DropzoneArea acceptedFiles={["image/*", "application/x-pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/> + <DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/> <FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText> </Grid> <Grid item> diff --git a/src/hooks/useProxyState.ts b/src/hooks/useProxyState.ts index 3dd51fa..87b7945 100644 --- a/src/hooks/useProxyState.ts +++ b/src/hooks/useProxyState.ts @@ -1,9 +1,10 @@ -import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; export function useProxyState<T>(initial: T, setter: (value: T) => void): [T, Dispatch<SetStateAction<T>>] { const [value, proxy] = useState<T>(initial); - useEffect(() => setter(value), [ value ]); - - return [value, proxy]; + return [value, (newValue: SetStateAction<T>) => { + proxy(newValue); + setter(typeof newValue === "function" ? (newValue as any)(value) : newValue); + }]; } diff --git a/src/i18n.ts b/src/i18n.ts index eb71721..c01bc57 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -5,6 +5,7 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector"; import "moment/locale/pl" import "moment/locale/en-gb" import moment, { isDuration, isMoment } from "moment"; +import { convertToRoman } from "@/utils/numbers"; const resources = { en: { @@ -24,6 +25,10 @@ i18n interpolation: { escapeValue: false, format: (value, format, lng) => { + if (typeof value === "number" && format == "roman") { + return convertToRoman(value); + } + if (isMoment(value)) { return value.locale(lng || "pl").format(format || "DD MMM YYYY"); } diff --git a/src/pages/index.ts b/src/pages/index.ts index bb1a4b5..c5f71a3 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,7 +1,3 @@ export * from "./internship/proposal"; export * from "./errors/not-found" export * from "./main" -export { ProposalStep } from "@/pages/steps/proposal"; -export { ProposalComment } from "@/pages/steps/proposal"; -export { ProposalActions } from "@/pages/steps/proposal"; -export { ProposalStatus } from "@/pages/steps/proposal"; diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index e45b4cb..09b8ca8 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -4,9 +4,15 @@ import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { InternshipForm } from "@/forms/internship"; import React from "react"; -import { ProposalComment } from "@/pages"; +import { ProposalComment } from "@/pages/steps/proposal"; +import { useTranslation } from "react-i18next"; +import { ProposalPreview } from "@/components/proposalPreview"; +import { useSelector } from "react-redux"; +import { Internship } from "@/data"; +import { AppState } from "@/state/reducer"; +import { internshipSerializationTransformer } from "@/serialization"; -export const InternshipProposalPage = () => { +export const InternshipProposalFormPage = () => { return <Page title="Zgłoszenie praktyki"> <Page.Header maxWidth="md"> <Page.Breadcrumbs> @@ -22,4 +28,23 @@ export const InternshipProposalPage = () => { </Page> } -export default InternshipProposalPage; +export const InternshipProposalPreviewPage = () => { + const { t } = useTranslation(); + const proposal = useSelector<AppState, Internship | null>(state => state.proposal.proposal && internshipSerializationTransformer.reverseTransform(state.proposal.proposal)); + + return <Page title={ t("") }> + <Page.Header maxWidth="md"> + <Page.Breadcrumbs> + <Link component={ RouterLink } to={ route("home") }>Moja praktyka</Link> + <Typography color="textPrimary">Podgląd zgłoszenia</Typography> + </Page.Breadcrumbs> + <Page.Title>Moje zgłoszenie</Page.Title> + </Page.Header> + <Container maxWidth={ "md" }> + <ProposalComment /> + { proposal && <ProposalPreview proposal={ proposal } /> } + </Container> + </Page> +} + +export default InternshipProposalFormPage; diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 3ddcfef..5d2da88 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -11,12 +11,15 @@ import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; import { Step } from "@/components"; import { ProposalStep } from "@/pages/steps/proposal"; import { PlanStep } from "@/pages/steps/plan"; +import { InsuranceStep } from "@/pages/steps/insurance"; +import { InsuranceState } from "@/state/reducer/insurance"; export const MainPage = () => { const { t } = useTranslation(); const student = useSelector<AppState, Student | null>(state => state.student); const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + const insurance = useSelector<AppState, InsuranceState>(root => root.insurance); const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); @@ -39,7 +42,7 @@ export const MainPage = () => { </Step> <ProposalStep /> <PlanStep /> - <Step label={ t('steps.insurance.header') }/> + { insurance.required && <InsuranceStep /> } <Step label={ t('steps.report.header') } until={ deadlines.report }/> <Step label={ t('steps.grade.header') }/> </Stepper> diff --git a/src/pages/steps/common.tsx b/src/pages/steps/common.tsx index 261ba52..58fdb8e 100644 --- a/src/pages/steps/common.tsx +++ b/src/pages/steps/common.tsx @@ -1,8 +1,9 @@ import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission"; -import { Theme } from "@material-ui/core"; +import { Button, ButtonProps, Theme } from "@material-ui/core"; import { createStyles, makeStyles } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import React from "react"; +import { CommentQuestion } from "mdi-material-ui/index"; export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => { switch (status) { @@ -44,3 +45,9 @@ export const Status = ({ submission } : SubmissionStatusProps) => { return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>; } + +export const ContactAction = (props: ButtonProps) => { + const { t } = useTranslation(); + + return <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button> +} diff --git a/src/pages/steps/insurance.tsx b/src/pages/steps/insurance.tsx new file mode 100644 index 0000000..2245c53 --- /dev/null +++ b/src/pages/steps/insurance.tsx @@ -0,0 +1,27 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { InsuranceState } from "@/state/reducer/insurance"; +import { Actions, Step } from "@/components"; +import { useTranslation } from "react-i18next"; +import React from "react"; +import { Edition, getEditionDeadlines } from "@/data/edition"; +import { Moment } from "moment"; +import { ContactAction } from "@/pages/steps/common"; + +export const InsuranceStep = () => { + const insurance = useSelector<AppState, InsuranceState>(root => root.insurance); + const deadline = useSelector<AppState, Moment | undefined>(state => getEditionDeadlines(state.edition as Edition).insurance); // edition cannot be null at this point + const { t } = useTranslation(); + + // we don't want to show this step unless it's required + if (!insurance.required) { + return null; + } + + return <Step label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }> + <p>{ t(`steps.insurance.instructions`) }</p> + <Actions> + <ContactAction /> + </Actions> + </Step> +} diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index 197c0cb..7a94c7f 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -3,14 +3,14 @@ import { AppState } from "@/state/reducer"; import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission"; import { useTranslation } from "react-i18next"; import { Box, Button, ButtonProps, StepProps } from "@material-ui/core"; -import { CommentQuestion, FileFind } from "mdi-material-ui/index"; +import { FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index"; import { route } from "@/routing"; import { Link as RouterLink } from "react-router-dom"; import { Actions, Step } from "@/components"; import React, { HTMLProps } from "react"; import { Alert, AlertTitle } from "@material-ui/lab"; import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; -import { Status } from "@/pages/steps/common"; +import { ContactAction, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; const PlanActions = () => { @@ -18,20 +18,22 @@ const PlanActions = () => { const { t } = useTranslation(); const ReviewAction = (props: ButtonProps) => - <Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</Button> + <Button startIcon={ <FileDownloadOutline /> } color="primary" { ...props }>{ t('steps.plan.download') }</Button> const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) => - <Button to={ route("plan") } variant="contained" color="primary" component={ RouterLink } { ...props as any }> - { children } + <Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink } startIcon={ <FileUploadOutline /> }> + { t('steps.plan.submit') } </Button> - const ContactAction = (props: ButtonProps) => - <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button> + const TemplateAction = (props: ButtonProps) => + <Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon/> } { ...props }> + { t('steps.plan.template') } + </Button> switch (status) { case "awaiting": return <Actions> - <ReviewAction/> + <ReviewAction /> </Actions> case "accepted": return <Actions> @@ -41,16 +43,14 @@ const PlanActions = () => { case "declined": return <Actions> <FormAction>{ t('fix-errors') }</FormAction> + <ReviewAction /> + <TemplateAction /> <ContactAction/> </Actions> case "draft": return <Actions> - <Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink }> - { t('steps.plan.submit') } - </Button> - <Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon/> }> - { t('steps.plan.template') } - </Button> + <FormAction /> + <TemplateAction /> </Actions> default: diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx index 5565cdc..f7646d9 100644 --- a/src/pages/steps/proposal.tsx +++ b/src/pages/steps/proposal.tsx @@ -10,15 +10,19 @@ import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; import { Actions, Step } from "@/components"; import { route } from "@/routing"; import { Link as RouterLink } from "react-router-dom"; -import { ClipboardEditOutline, CommentQuestion, FileFind } from "mdi-material-ui/index"; -import { Status } from "@/pages/steps/common"; +import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index"; +import { ContactAction, Status } from "@/pages/steps/common"; const ProposalActions = () => { const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal)); const { t } = useTranslation(); const ReviewAction = (props: ButtonProps) => - <Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</Button> + <Button startIcon={ <FileFind/> } + component={ RouterLink } to={ route("internship_proposal_preview") } + { ...props as any }> + { t('review') } + </Button> const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) => <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink } @@ -26,9 +30,6 @@ const ProposalActions = () => { { children } </Button> - const ContactAction = (props: ButtonProps) => - <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button> - switch (status) { case "awaiting": return <Actions> @@ -42,7 +43,7 @@ const ProposalActions = () => { case "declined": return <Actions> <FormAction>{ t('fix-errors') }</FormAction> - <ContactAction/> + <ContactAction /> </Actions> case "draft": return <Actions> diff --git a/src/routing.tsx b/src/routing.tsx index adb533a..c0e3e2e 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -1,7 +1,7 @@ import React, { ReactComponentElement } from "react"; import { MainPage } from "@/pages/main"; import { RouteProps } from "react-router-dom"; -import { InternshipProposalPage } from "@/pages/internship/proposal"; +import { InternshipProposalFormPage, InternshipProposalPreviewPage } from "@/pages/internship/proposal"; import { NotFoundPage } from "@/pages/errors/not-found"; import SubmitPlanPage from "@/pages/internship/plan"; @@ -13,7 +13,8 @@ type Route = { export const routes: Route[] = [ { name: "home", path: "/", exact: true, content: () => <MainPage/> }, - { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalPage/> }, + { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/> }, + { name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/> }, { name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/> }, // fallback route for 404 pages diff --git a/src/serialization/internship.ts b/src/serialization/internship.ts index be27ddf..9a9f628 100644 --- a/src/serialization/internship.ts +++ b/src/serialization/internship.ts @@ -1,6 +1,7 @@ import { Internship, InternshipType } from "@/data"; import { Serializable, SerializationTransformer } from "@/serialization/types"; import { momentSerializationTransformer } from "@/serialization/moment"; +import { Moment } from "moment"; export const internshipSerializationTransformer: SerializationTransformer<Internship> = { transform: (internship: Internship): Serializable<Internship> => ({ @@ -10,8 +11,8 @@ export const internshipSerializationTransformer: SerializationTransformer<Intern }), reverseTransform: (serialized: Serializable<Internship>): Internship => ({ ...serialized, - startDate: momentSerializationTransformer.reverseTransform(serialized.startDate), - endDate: momentSerializationTransformer.reverseTransform(serialized.endDate), + startDate: momentSerializationTransformer.reverseTransform(serialized.startDate) as Moment, + endDate: momentSerializationTransformer.reverseTransform(serialized.endDate) as Moment, type: serialized.type as InternshipType, }), } diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index f5bcdfb..54ac996 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -6,6 +6,7 @@ import { Dispatch } from "react"; import { useDispatch as useReduxDispatch } from "react-redux"; import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions/plan"; +import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance"; export * from "./base" export * from "./edition" @@ -14,9 +15,9 @@ export * from "./student" export * from "./proposal" export * from "./plan" -export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction; +export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction | InsuranceAction; -export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions } +export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions, ...InsuranceActions } export type Actions = typeof Actions; export const useDispatch = () => useReduxDispatch<Dispatch<Action>>() diff --git a/src/state/actions/insurance.ts b/src/state/actions/insurance.ts new file mode 100644 index 0000000..721d7d6 --- /dev/null +++ b/src/state/actions/insurance.ts @@ -0,0 +1,12 @@ +import { Action } from "@/state/actions/base"; +import { InsuranceState } from "@/state/reducer/insurance"; + +export enum InsuranceActions { + Signed = "RECEIVE_INSURANCE_SIGN", + Update = "RECEIVE_INSURANCE_UPDATE", +} + +export type InsuranceSigned = Action<InsuranceActions.Signed>; +export type InsuranceUpdate = Action<InsuranceActions.Update> & Partial<InsuranceState>; + +export type InsuranceAction = InsuranceSigned | InsuranceUpdate; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 3d139e2..ca5d238 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -5,6 +5,7 @@ import editionReducer from "@/state/reducer/edition"; import settingsReducer from "@/state/reducer/settings"; import internshipProposalReducer from "@/state/reducer/proposal"; import internshipPlanReducer from "@/state/reducer/plan"; +import { insuranceReducer } from "@/state/reducer/insurance"; const rootReducer = combineReducers({ student: studentReducer, @@ -12,6 +13,7 @@ const rootReducer = combineReducers({ settings: settingsReducer, proposal: internshipProposalReducer, plan: internshipPlanReducer, + insurance: insuranceReducer, }) export type AppState = ReturnType<typeof rootReducer>; diff --git a/src/state/reducer/insurance.ts b/src/state/reducer/insurance.ts new file mode 100644 index 0000000..d5aaa4f --- /dev/null +++ b/src/state/reducer/insurance.ts @@ -0,0 +1,26 @@ +import { Reducer } from "react"; +import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance"; + +export type InsuranceState = { + required: boolean; + signed: boolean; + /// other data? +} + +const initialInsuranceState: InsuranceState = { + required: false, + signed: false, +} + +export const insuranceReducer: Reducer<InsuranceState, InsuranceAction> = (state = initialInsuranceState, action) => { + const { type, ...payload } = action; + + switch (action.type) { + case InsuranceActions.Signed: + return { ...state, signed: true } + case InsuranceActions.Update: + return { ...state, ...payload } + default: + return state; + } +} diff --git a/src/styles/index.ts b/src/styles/index.ts new file mode 100644 index 0000000..c7b6bdf --- /dev/null +++ b/src/styles/index.ts @@ -0,0 +1 @@ +export * from "./spacing" diff --git a/src/styles/page.scss b/src/styles/page.scss index 8b41597..f6a1b68 100644 --- a/src/styles/page.scss +++ b/src/styles/page.scss @@ -12,3 +12,11 @@ font-weight: 400; line-height: 1.5; } + +.proposal__primary { + font-size: 1.675rem; +} + +.proposal__header:not(:first-child) { + margin-top: 1rem; +} diff --git a/src/styles/spacing.ts b/src/styles/spacing.ts new file mode 100644 index 0000000..dc8863d --- /dev/null +++ b/src/styles/spacing.ts @@ -0,0 +1,19 @@ +import { createStyles, makeStyles } from "@material-ui/core/styles"; + +const defaultSpacing: number = 3; + +export const useVerticalSpacing = makeStyles(theme => createStyles({ + root: { + "& > *:not(:last-child)": { + marginBottom: (spacing: number = defaultSpacing) => theme.spacing(spacing) + } + } +})) + +export const useHorizontalSpacing = makeStyles(theme => createStyles({ + root: { + "& > *:not(:last-child)": { + marginRight: (spacing: number = defaultSpacing) => theme.spacing(spacing) + } + } +})) diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts new file mode 100644 index 0000000..090761b --- /dev/null +++ b/src/utils/numbers.ts @@ -0,0 +1,32 @@ +const roman = { + M: 1000, + CM: 900, + D: 500, + CD: 400, + C: 100, + XC: 90, + L: 50, + XL: 40, + X: 10, + IX: 9, + V: 5, + IV: 4, + I: 1 +}; + +type RomanLiteral = keyof typeof roman; + +// shamefully stolen from https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript +export function convertToRoman(number: number) { + let result = ''; + + for (const i in roman) { + const q = Math.floor(number / roman[i as RomanLiteral]); + + number -= q * roman[i as RomanLiteral]; + + result += i.repeat(q); + } + + return result; +} diff --git a/translations/pl.yaml b/translations/pl.yaml index 6c9bfc3..c52c43d 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -18,6 +18,8 @@ fix-errors: popraw uwagi contact: skontaktuj się z pełnomocnikiem comments: Zgłoszone uwagi send-again: wyślij ponownie +cancel: anuluj + dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać" @@ -26,9 +28,13 @@ sections: header: "Moja praktyka" forms: + internship: + 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 Plan Praktyk a następnie wyślij go z pomocą tego formularza. <więcej informacji> + 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: @@ -46,18 +52,37 @@ submission: declined: "do poprawy" draft: "wersja robocza" +internship: + intern: + semester: semestr {{ semester, roman }} + album: "numer albumu {{ album }}" + date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}" + duration: "{{ duration, humanize }}" + hours: "{{ hours }} godzin" + office: "Oddział / adres" + address: + city: "{{ city }}, {{ country }}" + street: "{{ postalCode }}, {{ street }} {{ building }}" + sections: + intern-info: "Dane osoby odbywającej praktykę" + duration: "Czas trwania praktyki" + place: "Miejsce odbywania praktyki" + kind: "Rodzaj i program praktyki" + mentor: "Zakładowy opiekun praktyki" + + steps: personal-data: header: "Uzupełnienie informacji" info: > Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz - problem z uzupełnieniem tych informacji - skontaktuj się z koordynatorem praktyk dla Twojego kierunku. + problem z uzupełnieniem tych informacji - skontaktuj się z pełnomocnikiem ds. praktyk dla Twojego kierunku. form: "Uzupełnij dane" internship-proposal: header: "Zgłoszenie praktyki" info: draft: > - Przed podjęciem praktyki należy ją zgłosić. + 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. @@ -74,18 +99,23 @@ steps: draft: > TODO awaiting: > - TODO + Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i + zatwierdzony. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian. accepted: > - TODO + Twój indywidualny program praktyki został zweryfikowany i zaakceptowany. declined: > - TODO + Twój indywidualny program praktyki został zweryfikowany i odrzucony. Popraw zgłoszone uwagi i wyślij nowy program. W + razie pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku. template: "Pobierz szablon" - submit: "Wyślij Indywidualny Plan Praktyki" + submit: "Wyślij Indywidualny Program Praktyki" + download: Twój indywidualny program praktyki report: header: "Raport z praktyki" grade: header: "Ocena z praktyki" insurance: header: "Ubezpieczenie NNW" + instructions: > + papierki do podpisania... contact-coordinator: "Skontaktuj się z koordynatorem"