From 1d08617d3446c059404a2f389be9fa3c34bec366 Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Sun, 2 Aug 2020 16:00:12 +0200
Subject: [PATCH] Save proposal form in state

---
 package.json                                 |  1 +
 src/components/index.tsx                     |  2 +
 src/components/step.tsx                      | 46 +++++++++++
 src/data/internship.ts                       |  5 +-
 src/forms/company.tsx                        | 39 ++++-----
 src/forms/helpers.ts                         |  2 +-
 src/forms/{Internship.tsx => internship.tsx} | 73 +++++++++--------
 src/helpers.ts                               |  5 ++
 src/hooks/index.ts                           |  1 +
 src/hooks/useProxyState.ts                   |  9 +++
 src/pages/index.ts                           |  1 -
 src/pages/internship/proposal.tsx            |  2 +-
 src/pages/main.tsx                           | 84 ++++++++++++--------
 src/serialization/index.ts                   |  3 +
 src/serialization/internship.ts              | 17 ++++
 src/serialization/moment.ts                  |  7 ++
 src/serialization/types.ts                   | 18 +++++
 src/state/actions/proposal.ts                |  2 +-
 src/state/reducer/proposal.ts                | 38 ++++++++-
 src/state/store.ts                           |  4 +-
 translations/pl.yaml                         |  7 ++
 webpack.config.js                            |  1 +
 yarn.lock                                    |  5 ++
 23 files changed, 274 insertions(+), 98 deletions(-)
 create mode 100644 src/components/index.tsx
 create mode 100644 src/components/step.tsx
 rename src/forms/{Internship.tsx => internship.tsx} (71%)
 create mode 100644 src/hooks/index.ts
 create mode 100644 src/hooks/useProxyState.ts
 create mode 100644 src/serialization/index.ts
 create mode 100644 src/serialization/internship.ts
 create mode 100644 src/serialization/moment.ts
 create mode 100644 src/serialization/types.ts

diff --git a/package.json b/package.json
index 93cb25f..1379ef1 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "i18next": "^19.6.0",
     "i18next-browser-languagedetector": "^5.0.0",
     "material-ui-dropzone": "^3.3.0",
+    "mdi-material-ui": "^6.17.0",
     "moment": "^2.26.0",
     "node-sass": "^4.14.1",
     "optimize-css-assets-webpack-plugin": "5.0.3",
diff --git a/src/components/index.tsx b/src/components/index.tsx
new file mode 100644
index 0000000..9feeb59
--- /dev/null
+++ b/src/components/index.tsx
@@ -0,0 +1,2 @@
+export * from "./actions"
+export * from "./step"
diff --git a/src/components/step.tsx b/src/components/step.tsx
new file mode 100644
index 0000000..0cf9cd6
--- /dev/null
+++ b/src/components/step.tsx
@@ -0,0 +1,46 @@
+import moment, { Moment } from "moment";
+import { Box, Step as StepperStep, StepContent, StepLabel, StepProps as StepperStepProps, Typography } from "@material-ui/core";
+import { useTranslation } from "react-i18next";
+import React, { ReactChild, useMemo } from "react";
+
+type StepProps = StepperStepProps & {
+    until?: Moment;
+    completedOn?: Moment;
+    label: string;
+    state?: ReactChild | null;
+
+    /** this roughly translates into completed */
+    accepted?: boolean;
+
+    /** this roughly translates into error */
+    declined?: boolean;
+    sent?: boolean;
+}
+
+const now = moment();
+
+export const Step = ({ until, label, completedOn, children, accepted = false, declined = false, completed = false, state = null, ...props }: StepProps) => {
+    const { t } = useTranslation();
+
+    const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
+    const left = useMemo(() => moment.duration(now.diff(until)), [until]);
+
+    return <StepperStep { ...props } completed={ completed }>
+        <StepLabel error={ declined }>
+            { label }
+            { until && <Box>
+                { state && <>
+                    <Typography variant="subtitle2" display="inline">{ state }</Typography>
+                    <Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography>
+                </> }
+                <Typography variant="subtitle2" color="textSecondary" display="inline">
+                    { t('until', { date: until }) }
+                    { isLate && <Typography color="error" display="inline"
+                                            variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
+                    { !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
+                </Typography>
+            </Box> }
+        </StepLabel>
+        { children && <StepContent>{ children }</StepContent> }
+    </StepperStep>
+}
diff --git a/src/data/internship.ts b/src/data/internship.ts
index 4961e27..bc82578 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 { Company } from "@/data/company";
+import { BranchOffice, Company } from "@/data/company";
 
 export enum InternshipType {
     FreeInternship = "FreeInternship",
@@ -61,8 +61,10 @@ export interface Internship extends Identifiable {
     endDate: Moment;
     isAccepted: boolean;
     lengthInWeeks: number;
+    hours: number;
     mentor: Mentor;
     company: Company;
+    office: BranchOffice;
 }
 
 export interface Mentor {
@@ -71,3 +73,4 @@ export interface Mentor {
     email: string;
     phone: string | null;
 }
+
diff --git a/src/forms/company.tsx b/src/forms/company.tsx
index c3fd10f..a728935 100644
--- a/src/forms/company.tsx
+++ b/src/forms/company.tsx
@@ -1,19 +1,19 @@
-import React, { HTMLProps, useEffect, useMemo, useState } from "react";
-import { BranchOffice, Company, Course, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data";
+import React, { HTMLProps, useMemo } from "react";
+import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data";
 import { sampleCompanies } from "@/provider/dummy";
-import { Alert, Autocomplete } from "@material-ui/lab";
-import { Button, Grid, TextField, Typography } from "@material-ui/core";
+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 { sampleCourse } from "@/provider/dummy/student";
+import { InternshipFormSectionProps } from "@/forms/internship";
 import { emptyMentor } from "@/provider/dummy/internship";
+import { useProxyState } from "@/hooks";
 
 export type CompanyFormProps = {} & InternshipFormSectionProps;
 
 export type BranchOfficeProps = {
-    company: Company,
-    disabled?: boolean
-}
+    disabled?: boolean;
+    offices?: BranchOffice[];
+} & BoundProperty<BranchOffice, "onChange", "value">
 
 export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => (
     <div className="company-item" { ...props }>
@@ -29,11 +29,8 @@ export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTML
     </div>
 )
 
-export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = false }) => {
-    const [office, setOffice] = useState<BranchOffice>(emptyBranchOffice)
-
+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 }))
 
     const handleCityChange = (event: any, value: BranchOffice | string | null) => {
@@ -63,13 +60,11 @@ export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = fa
         })
     }
 
-    useEffect(() => void (office.id && setOffice(emptyBranchOffice)), [company])
-
     return (
         <div>
             <Grid container>
                 <Grid item md={ 7 }>
-                    <Autocomplete options={ company?.offices || [] }
+                    <Autocomplete options={ offices || [] }
                                   disabled={ disabled }
                                   getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
                                   renderOption={ office => <OfficeItem office={ office }/> }
@@ -122,14 +117,12 @@ export const MentorForm = ({ mentor, onMentorChange }: BoundProperty<Mentor, 'on
 }
 
 export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ internship, onChange }) => {
-    const [company, setCompany] = useState<Company>(emptyCompany);
-    const [mentor, setMentor] = useState<Mentor>(emptyMentor);
+    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 }));
 
     const canEdit = useMemo(() => !company.id, [company.id]);
 
-    useEffect(() => onChange({ ...internship, mentor }), [ mentor ]);
-    useEffect(() => onChange({ ...internship, company }), [ company ]);
-
     const fieldProps = formFieldProps(company, setCompany)
 
     const handleCompanyChange = (event: any, value: Company | string | null) => {
@@ -153,7 +146,7 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns
                                   getOptionLabel={ option => option.name }
                                   renderOption={ company => <CompanyItem company={ company }/> }
                                   renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> }
-                                  onChange={ handleCompanyChange }
+                                  onChange={ handleCompanyChange } value={ company }
                                   freeSolo
                     />
                 </Grid>
@@ -167,7 +160,7 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns
             <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 company={ company }/>
+            <BranchForm value={ office } onChange={ setOffice } offices={ company.offices } />
         </>
     )
 }
diff --git a/src/forms/helpers.ts b/src/forms/helpers.ts
index 96fdbde..5eabad7 100644
--- a/src/forms/helpers.ts
+++ b/src/forms/helpers.ts
@@ -15,7 +15,7 @@ export function formFieldProps<T>(subject: T, update: (value: T) => void, option
     return <P extends keyof T, TArgs extends any[]>(
         field: P,
         extractor: (...args: TArgs) => T[P] = ((event: DOMEvent<HTMLInputElement>) => event.target.value as unknown as T[P]) as any
-    ) => ({
+    ): any => ({
         [property]: subject[field],
         [event]: (...args: TArgs) => update({
             ...subject,
diff --git a/src/forms/Internship.tsx b/src/forms/internship.tsx
similarity index 71%
rename from src/forms/Internship.tsx
rename to src/forms/internship.tsx
index 967b1b6..abd58f4 100644
--- a/src/forms/Internship.tsx
+++ b/src/forms/internship.tsx
@@ -1,18 +1,5 @@
-import React, { HTMLProps, useMemo, useState } from "react";
-import {
-    FormControl,
-    Grid,
-    Input,
-    InputLabel,
-    Typography,
-    FormHelperText,
-    TextField,
-    FormGroup,
-    FormControlLabel,
-    Checkbox,
-    FormLabel,
-    Button
-} from "@material-ui/core";
+import React, { HTMLProps, useEffect, useMemo, useState } from "react";
+import { Button, 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";
@@ -24,6 +11,14 @@ 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";
+import { useSelector } from "react-redux";
+import { AppState } from "@/state/reducer";
+import { useHistory } from "react-router-dom";
+import { route } from "@/routing";
+import { useProxyState } from "@/hooks";
+import { getInternshipProposal } from "@/state/reducer/proposal";
 
 export type InternshipFormProps = {}
 
@@ -62,25 +57,26 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr
             <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>
     )
 }
 
-const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => {
-    const [startDate, setStartDate] = useState<Moment | null>(internship.startDate);
-    const [endDate, setEndDate] = useState<Moment | null>(internship.endDate);
+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 [overrideHours, setHoursOverride] = useState<number | null>(null)
     const [workingHours, setWorkingHours] = useState<number>(40)
 
@@ -89,6 +85,8 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => {
     const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
     const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [ hours ]);
 
+    useEffect(() => onChange({ ...internship, hours }), [hours])
+
     return (
         <Grid container>
             <Grid item md={ 6 }>
@@ -140,7 +138,18 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => {
 }
 
 export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => {
-    const [internship, setInternship] = useState<Nullable<Internship>>({ ...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();
+
+    const dispatch = useDispatch();
+    const history = useHistory();
+
+    const handleSubmit = () => {
+        dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship });
+        history.push(route("home"))
+    }
 
     return (
         <div className="internship-form">
@@ -152,7 +161,7 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
             <InternshipDurationForm internship={ internship } onChange={ setInternship }/>
             <Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography>
             <CompanyForm internship={ internship } onChange={ setInternship }/>
-            <Button variant="contained" color="primary">Wyślij</Button>
+            <Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
         </div>
     )
 }
diff --git a/src/helpers.ts b/src/helpers.ts
index e128e6c..b24217f 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,5 +1,10 @@
 export type Nullable<T> = { [P in keyof T]: T[P] | null }
 
+export type Partial<T> = { [K in keyof T]?: T[K] }
+export type Dictionary<T> = { [key: string]: T };
+
+export type Index = string | symbol | number;
+
 export interface DOMEvent<TTarget extends EventTarget> extends Event {
     target: TTarget;
 }
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..283d0e1
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./useProxyState"
diff --git a/src/hooks/useProxyState.ts b/src/hooks/useProxyState.ts
new file mode 100644
index 0000000..3dd51fa
--- /dev/null
+++ b/src/hooks/useProxyState.ts
@@ -0,0 +1,9 @@
+import { Dispatch, SetStateAction, useEffect, 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];
+}
diff --git a/src/pages/index.ts b/src/pages/index.ts
index 538a1bd..c5f71a3 100644
--- a/src/pages/index.ts
+++ b/src/pages/index.ts
@@ -1,4 +1,3 @@
 export * from "./internship/proposal";
 export * from "./errors/not-found"
 export * from "./main"
-export { Actions } from "@/components/actions";
diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx
index 00e233c..a9ef954 100644
--- a/src/pages/internship/proposal.tsx
+++ b/src/pages/internship/proposal.tsx
@@ -2,7 +2,7 @@ import { Page } from "@/pages/base";
 import { Container, Link, Typography } from "@material-ui/core";
 import { Link as RouterLink } from "react-router-dom";
 import { route } from "@/routing";
-import { InternshipForm } from "@/forms/Internship";
+import { InternshipForm } from "@/forms/internship";
 import React from "react";
 
 export const InternshipProposalPage = () => {
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index c48e059..46a1eaa 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -1,45 +1,69 @@
 import React, { useMemo } from "react";
 import { Page } from "@/pages/base";
-import { Box, Button, Container, Step as StepperStep, StepContent, StepLabel, Stepper, StepProps as StepperStepProps, Typography } from "@material-ui/core";
+import { Button, Container, Stepper, StepProps, Theme, Typography } from "@material-ui/core";
 import { Link as RouterLink } from "react-router-dom";
 import { route } from "@/routing";
 import { useTranslation } from "react-i18next";
-import moment, { Moment } from "moment";
 import { useSelector } from "react-redux";
 import { AppState } from "@/state/reducer";
 import { getMissingStudentData, Student } from "@/data";
 import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
 import { Description as DescriptionIcon } from "@material-ui/icons"
-import { Actions } from "@/components/actions";
+import { Actions, Step } from "@/components";
+import { getInternshipProposalStatus, InternshipProposalState, InternshipProposalStatus } from "@/state/reducer/proposal";
+import { createStyles, makeStyles } from "@material-ui/core/styles";
 
-type StepProps = StepperStepProps & {
-    until?: Moment;
-    completedOn?: Moment;
-    label: string;
+
+const getColorByStatus = (status: InternshipProposalStatus, theme: Theme) => {
+    switch (status) {
+        case "awaiting":
+            return theme.palette.info.dark;
+        case "accepted":
+            return theme.palette.success.dark;
+        case "declined":
+            return theme.palette.error.dark;
+        case "draft":
+            return theme.palette.grey["800"];
+        default:
+            return "textPrimary";
+    }
 }
 
-const now = moment();
+const useStatusStyles = makeStyles((theme: Theme) => {
+    const colorByStatusGetter = ({ status }: any) => getColorByStatus(status, theme);
+
+    return createStyles({
+        foreground: {
+            color: colorByStatusGetter
+        },
+        background: {
+            backgroundColor: colorByStatusGetter
+        }
+    })
+})
+
+const ProposalStatus = () => {
+    const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal))
+    const classes = useStatusStyles({ status });
 
-const Step = ({ until, label, completedOn, children, completed, ...props }: StepProps) => {
     const { t } = useTranslation();
 
-    const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
-    const left = useMemo(() => moment.duration(now.diff(until)), [until]);
+    return <span className={ classes.foreground }>{ t(`proposal.status.${status}`) }</span>;
+}
 
-    return <StepperStep { ...props } completed={ completed || !!completedOn }>
-        <StepLabel>
-            { label }
-            { until && <Box>
-                <Typography variant="subtitle2" color="textSecondary">
-                    { t('until', { date: until }) }
-                    { isLate && <Typography color="error" display="inline"
-                                            variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
-                    { !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
-                </Typography>
-            </Box> }
-        </StepLabel>
-        { children && <StepContent>{ children }</StepContent> }
-    </StepperStep>
+const ProposalStep = (props: StepProps) => {
+    const { t } = useTranslation();
+
+    const { sent } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
+    const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
+
+    return <Step { ...props } label={ t('steps.internship-proposal.header') } active={ true } completed={ sent } until={ deadlines.proposal } state={ <ProposalStatus /> }>
+        <p>{ t('steps.internship-proposal.info') }</p>
+
+        <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
+            { t('steps.internship-proposal.form') }
+        </Button>
+    </Step>;
 }
 
 export const MainPage = () => {
@@ -67,13 +91,7 @@ export const MainPage = () => {
                         </Button>
                     </> }
                 </Step>
-                <Step label={ t('steps.internship-proposal.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
-                    <p>{ t('steps.internship-proposal.info') }</p>
-
-                    <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
-                        { t('steps.internship-proposal.form') }
-                    </Button>
-                </Step>
+                <ProposalStep />
                 <Step label={ t('steps.plan.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
                     <p>{ t('steps.plan.info') }</p>
 
@@ -81,7 +99,7 @@ export const MainPage = () => {
                         <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 /> }>
+                        <Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon/> }>
                             { t('steps.plan.template') }
                         </Button>
                     </Actions>
diff --git a/src/serialization/index.ts b/src/serialization/index.ts
new file mode 100644
index 0000000..e6efcb2
--- /dev/null
+++ b/src/serialization/index.ts
@@ -0,0 +1,3 @@
+export * from "./internship"
+export * from "./moment"
+export * from "./types"
diff --git a/src/serialization/internship.ts b/src/serialization/internship.ts
new file mode 100644
index 0000000..be27ddf
--- /dev/null
+++ b/src/serialization/internship.ts
@@ -0,0 +1,17 @@
+import { Internship, InternshipType } from "@/data";
+import { Serializable, SerializationTransformer } from "@/serialization/types";
+import { momentSerializationTransformer } from "@/serialization/moment";
+
+export const internshipSerializationTransformer: SerializationTransformer<Internship> = {
+    transform: (internship: Internship): Serializable<Internship> => ({
+        ...internship,
+        startDate: momentSerializationTransformer.transform(internship.startDate),
+        endDate: momentSerializationTransformer.transform(internship.endDate),
+    }),
+    reverseTransform: (serialized: Serializable<Internship>): Internship => ({
+        ...serialized,
+        startDate: momentSerializationTransformer.reverseTransform(serialized.startDate),
+        endDate: momentSerializationTransformer.reverseTransform(serialized.endDate),
+        type: serialized.type as InternshipType,
+    }),
+}
diff --git a/src/serialization/moment.ts b/src/serialization/moment.ts
new file mode 100644
index 0000000..19acae1
--- /dev/null
+++ b/src/serialization/moment.ts
@@ -0,0 +1,7 @@
+import { SerializationTransformer } from "@/serialization/types";
+import moment, { Moment } from "moment";
+
+export const momentSerializationTransformer: SerializationTransformer<Moment, string> = {
+    transform: (subject: Moment) => subject.toISOString(),
+    reverseTransform: (subject: string) => moment(subject),
+}
diff --git a/src/serialization/types.ts b/src/serialization/types.ts
new file mode 100644
index 0000000..94e443d
--- /dev/null
+++ b/src/serialization/types.ts
@@ -0,0 +1,18 @@
+import { Moment } from "moment";
+
+type Simplify<T> = string |
+    T extends string ? string :
+    T extends number ? number :
+    T extends boolean ? boolean :
+    T extends Moment ? string :
+    T extends Array<infer K> ? Array<Simplify<K>> :
+    T extends (infer K)[] ? Simplify<K>[] :
+    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 SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>
diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts
index a7876c4..8189ec3 100644
--- a/src/state/actions/proposal.ts
+++ b/src/state/actions/proposal.ts
@@ -18,7 +18,7 @@ export interface ReceiveProposalApproveAction extends Action<InternshipProposalA
 }
 
 export interface ReceiveProposalDeclineAction extends Action<InternshipProposalActions.Decline> {
-
+    comment: string;
 }
 
 export interface ReceiveProposalUpdateAction extends Action<InternshipProposalActions.Receive> {
diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts
index c779e70..1ccb187 100644
--- a/src/state/reducer/proposal.ts
+++ b/src/state/reducer/proposal.ts
@@ -1,28 +1,33 @@
 import { DeanApproval } from "@/data/deanApproval";
 import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions";
 import { Internship } from "@/data";
+import moment from "moment";
+import { Serializable } from "@/serialization/types";
+import { internshipSerializationTransformer, momentSerializationTransformer } from "@/serialization";
 
-export type ProposalStatus = "draft" | "awaiting" | "accepted" | "declined";
+export type InternshipProposalStatus = "draft" | "awaiting" | "accepted" | "declined";
 
 export type InternshipProposalState = {
     accepted: boolean;
     sent: boolean;
+    sentOn: string | null;
     declined: boolean;
     requiredDeanApprovals: DeanApproval[];
-    proposal: Internship | null;
+    proposal: Serializable<Internship> | null;
     comment: string | null;
 }
 
 const defaultInternshipProposalState: InternshipProposalState = {
     accepted: false,
     declined: false,
+    sentOn: null,
     proposal: null,
     requiredDeanApprovals: [],
     sent: false,
     comment: null
 }
 
-const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): ProposalStatus => {
+export const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): InternshipProposalStatus => {
     switch (true) {
         case !sent:
             return "draft";
@@ -37,13 +42,38 @@ const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipPro
     }
 }
 
+export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
+    proposal && internshipSerializationTransformer.reverseTransform(proposal);
+
 const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => {
     switch (action.type) {
+        case InternshipProposalActions.Approve:
+            return {
+                ...state,
+                accepted: true,
+                declined: false,
+                comment: ""
+            }
+        case InternshipProposalActions.Decline:
+            return {
+                ...state,
+                accepted: false,
+                declined: true,
+                comment: action.comment
+            }
         case InternshipProposalActions.Save:
+            return {
+                ...state,
+                proposal: internshipSerializationTransformer.transform(action.internship),
+            }
         case InternshipProposalActions.Send:
             return {
                 ...state,
-                proposal: action.internship
+                proposal: internshipSerializationTransformer.transform(action.internship),
+                sent: true,
+                sentOn: momentSerializationTransformer.transform(moment()),
+                accepted: false,
+                declined: false,
             }
         default:
             return state;
diff --git a/src/state/store.ts b/src/state/store.ts
index f35ec6d..d3f3095 100644
--- a/src/state/store.ts
+++ b/src/state/store.ts
@@ -16,6 +16,8 @@ const store = createStore(
     devToolsEnhancer({})
 );
 
-export const persistor = persistStore(store)
+export const persistor = persistStore(store);
+
+(window as any)._store = store;
 
 export default store;
diff --git a/translations/pl.yaml b/translations/pl.yaml
index e2c6ab2..7fd75de 100644
--- a/translations/pl.yaml
+++ b/translations/pl.yaml
@@ -32,6 +32,13 @@ student:
   email: adres e-mail
   albumNumber: numer albumu
 
+proposal:
+  status:
+    awaiting: "wysłano, oczekuje na weryfikacje"
+    accepted: "zaakceptowano"
+    declined: "do poprawy"
+    draft: "wersja robocza"
+
 steps:
   personal-data:
     header: "Uzupełnienie informacji"
diff --git a/webpack.config.js b/webpack.config.js
index 6aeedda..dae0296 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -53,6 +53,7 @@ const config = {
         host: 'system-praktyk-front.localhost',
         disableHostCheck: true,
         historyApiFallback: true,
+        overlay: true,
     },
     optimization: {
         usedExports: true
diff --git a/yarn.lock b/yarn.lock
index 9b75961..697aea8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5521,6 +5521,11 @@ md5.js@^1.3.4:
     inherits "^2.0.1"
     safe-buffer "^5.1.2"
 
+mdi-material-ui@^6.17.0:
+  version "6.17.0"
+  resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.17.0.tgz#da69f0b7d7c6fc2255e6007ed8b8ca858c1aede7"
+  integrity sha512-eOprRu31lklPIS1WGe3cM0G/8glKl1WKRvewxjDrgXH2Ryxxg7uQ+uwDUwUEONtLku0p2ZOLzgXUIy2uRy5rLg==
+
 mdn-data@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"