From 5cc9f51584e22c82ca023b7364279ccd8ea62dd0 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 4 Aug 2020 20:20:58 +0200 Subject: [PATCH] Add IPP state management --- src/forms/plan.tsx | 50 +++++++++++++ src/pages/index.ts | 4 ++ src/pages/internship/plan.tsx | 32 +-------- src/pages/main.tsx | 132 ++-------------------------------- src/pages/steps/common.tsx | 46 ++++++++++++ src/pages/steps/plan.tsx | 92 ++++++++++++++++++++++++ src/pages/steps/proposal.tsx | 84 ++++++++++++++++++++++ src/state/actions/index.ts | 6 +- src/state/actions/plan.ts | 40 +++++++++++ src/state/actions/proposal.ts | 3 - src/state/reducer/index.ts | 2 + src/state/reducer/plan.ts | 49 +++++++++++++ translations/pl.yaml | 13 +++- 13 files changed, 391 insertions(+), 162 deletions(-) create mode 100644 src/forms/plan.tsx create mode 100644 src/pages/steps/common.tsx create mode 100644 src/pages/steps/plan.tsx create mode 100644 src/pages/steps/proposal.tsx create mode 100644 src/state/actions/plan.ts create mode 100644 src/state/reducer/plan.ts diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx new file mode 100644 index 0000000..24a8fa7 --- /dev/null +++ b/src/forms/plan.tsx @@ -0,0 +1,50 @@ +import { Button, FormHelperText, Grid, Typography } from "@material-ui/core"; +import { Description as DescriptionIcon } from "@material-ui/icons"; +import { DropzoneArea } from "material-ui-dropzone"; +import { Actions } from "@/components"; +import { Link as RouterLink, useHistory } from "react-router-dom"; +import { route } from "@/routing"; +import React, { useState } from "react"; +import { Plan } from "@/data"; +import { useTranslation } from "react-i18next"; +import { InternshipPlanActions, useDispatch } from "@/state/actions"; + +export const PlanForm = () => { + const { t } = useTranslation(); + + const [plan, setPlan] = useState({}); + + const dispatch = useDispatch(); + const history = useHistory(); + + const handleSubmit = () => { + dispatch({ type: InternshipPlanActions.Send, plan }); + history.push(route("home")) + } + + return + + { t('forms.plan.instructions') } + + + + + + + { t('forms.plan.dropzone-help') } + + + + + + + + + +} diff --git a/src/pages/index.ts b/src/pages/index.ts index c5f71a3..bb1a4b5 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,3 +1,7 @@ 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/plan.tsx b/src/pages/internship/plan.tsx index 992fd5c..e09a214 100644 --- a/src/pages/internship/plan.tsx +++ b/src/pages/internship/plan.tsx @@ -1,12 +1,10 @@ import { Page } from "@/pages/base"; -import { Button, Container, FormHelperText, Grid, Link, Typography } from "@material-ui/core"; +import { Container, Link, Typography } from "@material-ui/core"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import React from "react"; import { useTranslation } from "react-i18next"; -import { DropzoneArea } from "material-ui-dropzone"; -import { Description as DescriptionIcon } from "@material-ui/icons"; -import { Actions } from "@/components/actions"; +import { PlanForm } from "@/forms/plan"; export const SubmitPlanPage = () => { const { t } = useTranslation(); @@ -20,31 +18,7 @@ export const SubmitPlanPage = () => { { t("steps.plan.submit") } - - - { t('forms.plan.instructions') } - - - - - - - { t('forms.plan.dropzone-help') } - - - - - - - - - + } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 21df4f3..3ddcfef 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,6 +1,6 @@ -import React, { HTMLProps, useMemo } from "react"; +import React, { useMemo } from "react"; import { Page } from "@/pages/base"; -import { Box, Button, ButtonProps, Container, Stepper, StepProps, Theme, Typography } from "@material-ui/core"; +import { Button, Container, Stepper, Typography } from "@material-ui/core"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; @@ -8,118 +8,9 @@ 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, Step } from "@/components"; -import { InternshipProposalState } from "@/state/reducer/proposal"; -import { createStyles, makeStyles } from "@material-ui/core/styles"; -import { ClipboardEditOutline, CommentQuestion, FileFind } from "mdi-material-ui"; -import { Alert, AlertTitle } from "@material-ui/lab"; -import { getSubmissionStatus, SubmissionStatus } from "@/state/reducer/submission"; - - -const getColorByStatus = (status: SubmissionStatus, 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 useStatusStyles = makeStyles((theme: Theme) => { - const colorByStatusGetter = ({ status }: any) => getColorByStatus(status, theme); - - return createStyles({ - foreground: { - color: colorByStatusGetter - }, - background: { - backgroundColor: colorByStatusGetter - } - }) -}) - -const ProposalStatus = () => { - const status = useSelector(state => getSubmissionStatus(state.proposal)) - const classes = useStatusStyles({ status }); - - const { t } = useTranslation(); - - return { t(`proposal.status.${status}`) }; -} - -const ProposalActions = () => { - const status = useSelector(state => getSubmissionStatus(state.proposal)); - const { t } = useTranslation(); - - const ReviewAction = (props: ButtonProps) => - - - const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) => - - - const ContactAction = (props: ButtonProps) => - - - switch (status) { - case "awaiting": - return - - - case "accepted": - return - - { t('make-changes') } - - case "declined": - return - { t('fix-errors') } - - - case "draft": - return - { t('steps.internship-proposal.action') } - - default: - return - } -} - -export const ProposalComment = (props: HTMLProps) => { - const { comment, declined } = useSelector(state => state.proposal); - const { t } = useTranslation(); - - return comment ? - { t('comments') } - { comment } - : null -} - -const ProposalStep = (props: StepProps) => { - const { t } = useTranslation(); - - const { sent, comment, declined } = useSelector(state => state.proposal); - const status = useSelector(state => getSubmissionStatus(state.proposal)); - const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point - - return }> -

{ t(`steps.internship-proposal.info.${status}`) }

- { comment && } - -
; -} +import { Step } from "@/components"; +import { ProposalStep } from "@/pages/steps/proposal"; +import { PlanStep } from "@/pages/steps/plan"; export const MainPage = () => { const { t } = useTranslation(); @@ -147,18 +38,7 @@ export const MainPage = () => { } - -

{ t('steps.plan.info') }

- - - - - -
+ diff --git a/src/pages/steps/common.tsx b/src/pages/steps/common.tsx new file mode 100644 index 0000000..261ba52 --- /dev/null +++ b/src/pages/steps/common.tsx @@ -0,0 +1,46 @@ +import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission"; +import { Theme } from "@material-ui/core"; +import { createStyles, makeStyles } from "@material-ui/core/styles"; +import { useTranslation } from "react-i18next"; +import React from "react"; + +export const getColorByStatus = (status: SubmissionStatus, 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"; + } +} + +export const useStatusStyles = makeStyles((theme: Theme) => { + const colorByStatusGetter = ({ status }: any) => getColorByStatus(status, theme); + + return createStyles({ + foreground: { + color: colorByStatusGetter + }, + background: { + backgroundColor: colorByStatusGetter + } + }) +}) + +export type SubmissionStatusProps = { + submission: SubmissionState, +} + +export const Status = ({ submission } : SubmissionStatusProps) => { + const status = getSubmissionStatus(submission); + const classes = useStatusStyles({ status }); + + const { t } = useTranslation(); + + return { t(`submission.status.${ status }`) }; +} diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx new file mode 100644 index 0000000..197c0cb --- /dev/null +++ b/src/pages/steps/plan.tsx @@ -0,0 +1,92 @@ +import { useSelector } from "react-redux"; +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 { 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 { Description as DescriptionIcon } from "@material-ui/icons"; + +const PlanActions = () => { + const status = useSelector(state => getSubmissionStatus(state.plan)); + const { t } = useTranslation(); + + const ReviewAction = (props: ButtonProps) => + + + const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) => + + + const ContactAction = (props: ButtonProps) => + + + switch (status) { + case "awaiting": + return + + + case "accepted": + return + + { t('send-again') } + + case "declined": + return + { t('fix-errors') } + + + case "draft": + return + + + + + default: + return + } +} + +export const PlanComment = (props: HTMLProps) => { + const { comment, declined } = useSelector(state => state.plan); + const { t } = useTranslation(); + + return comment ? + { t('comments') } + { comment } + : null +} + +export const PlanStep = (props: StepProps) => { + const { t } = useTranslation(); + + const submission = useSelector(state => state.plan); + + const status = getSubmissionStatus(submission); + const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + + const { sent, declined, comment } = submission; + + return }> +

{ t(`steps.plan.info.${ status }`) }

+ + { comment && } + + +
; +} diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx new file mode 100644 index 0000000..5565cdc --- /dev/null +++ b/src/pages/steps/proposal.tsx @@ -0,0 +1,84 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission"; +import { useTranslation } from "react-i18next"; +import React, { HTMLProps } from "react"; +import { InternshipProposalState } from "@/state/reducer/proposal"; +import { Alert, AlertTitle } from "@material-ui/lab"; +import { Box, Button, ButtonProps, StepProps } from "@material-ui/core"; +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"; + +const ProposalActions = () => { + const status = useSelector(state => getSubmissionStatus(state.proposal)); + const { t } = useTranslation(); + + const ReviewAction = (props: ButtonProps) => + + + const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) => + + + const ContactAction = (props: ButtonProps) => + + + switch (status) { + case "awaiting": + return + + + case "accepted": + return + + { t('make-changes') } + + case "declined": + return + { t('fix-errors') } + + + case "draft": + return + { t('steps.internship-proposal.action') } + + default: + return + } +} + +export const ProposalComment = (props: HTMLProps) => { + const { comment, declined } = useSelector(state => state.proposal); + const { t } = useTranslation(); + + return comment ? + { t('comments') } + { comment } + : null +} + +export const ProposalStep = (props: StepProps) => { + const { t } = useTranslation(); + + const submission = useSelector(state => state.proposal); + const status = useSelector(state => getSubmissionStatus(state.proposal)); + const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + + const { sent, declined, comment } = submission; + + return }> +

{ t(`steps.internship-proposal.info.${ status }`) }

+ { comment && } + +
; +} diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 39de422..f5bcdfb 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -5,16 +5,18 @@ import { InternshipProposalAction, InternshipProposalActions } from "@/state/act import { Dispatch } from "react"; import { useDispatch as useReduxDispatch } from "react-redux"; +import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions/plan"; export * from "./base" export * from "./edition" export * from "./settings" export * from "./student" export * from "./proposal" +export * from "./plan" -export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction; +export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction; -export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions } +export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions } export type Actions = typeof Actions; export const useDispatch = () => useReduxDispatch>() diff --git a/src/state/actions/plan.ts b/src/state/actions/plan.ts new file mode 100644 index 0000000..fcabfbe --- /dev/null +++ b/src/state/actions/plan.ts @@ -0,0 +1,40 @@ +import { Plan } from "@/data"; +import { + ReceiveSubmissionApproveAction, + ReceiveSubmissionDeclineAction, + ReceiveSubmissionUpdateAction, + SaveSubmissionAction, + SendSubmissionAction +} from "@/state/actions/submission"; + +export enum InternshipPlanActions { + Send = "SEND_PLAN", + Save = "SAVE_PLAN", + Approve = "RECEIVE_PLAN_APPROVE", + Decline = "RECEIVE_PLAN_DECLINE", + Receive = "RECEIVE_PLAN_STATE", +} + +export interface SendPlanAction extends SendSubmissionAction { + plan: Plan; +} + +export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction { +} + +export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction { +} + +export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction { +} + +export interface SavePlanAction extends SaveSubmissionAction { + plan: Plan; +} + +export type InternshipPlanAction + = SendPlanAction + | SavePlanAction + | ReceivePlanApproveAction + | ReceivePlanDeclineAction + | ReceivePlanUpdateAction; diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index 20f4af6..ad8102a 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -20,15 +20,12 @@ export interface SendProposalAction extends SendSubmissionAction { - comment: string | null; } export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction { - comment: string; } export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction { - } export interface SaveProposalAction extends SaveSubmissionAction { diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 3b9c780..3d139e2 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -4,12 +4,14 @@ import studentReducer from "@/state/reducer/student" import editionReducer from "@/state/reducer/edition"; import settingsReducer from "@/state/reducer/settings"; import internshipProposalReducer from "@/state/reducer/proposal"; +import internshipPlanReducer from "@/state/reducer/plan"; const rootReducer = combineReducers({ student: studentReducer, edition: editionReducer, settings: settingsReducer, proposal: internshipProposalReducer, + plan: internshipPlanReducer, }) export type AppState = ReturnType; diff --git a/src/state/reducer/plan.ts b/src/state/reducer/plan.ts new file mode 100644 index 0000000..d092583 --- /dev/null +++ b/src/state/reducer/plan.ts @@ -0,0 +1,49 @@ +import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions"; +import { Plan } from "@/data"; +import { Serializable } from "@/serialization/types"; +import { + createSubmissionReducer, + defaultDeanApprovalsState, + defaultSubmissionState, + MayRequireDeanApproval, + SubmissionState +} from "@/state/reducer/submission"; +import { Reducer } from "react"; +import { SubmissionAction } from "@/state/actions/submission"; + +export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & { + plan: Serializable | null; +} + +const defaultInternshipPlanState: InternshipPlanState = { + ...defaultDeanApprovalsState, + ...defaultSubmissionState, + plan: null, +} + +export const getInternshipPlan = ({ plan }: InternshipPlanState): Plan | null => plan; + +const internshipPlanSubmissionReducer: Reducer = createSubmissionReducer({ + [InternshipPlanActions.Approve]: SubmissionAction.Approve, + [InternshipPlanActions.Decline]: SubmissionAction.Decline, + [InternshipPlanActions.Receive]: SubmissionAction.Receive, + [InternshipPlanActions.Save]: SubmissionAction.Save, + [InternshipPlanActions.Send]: SubmissionAction.Send, +}) + +const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPlanState, action: InternshipPlanAction): InternshipPlanState => { + state = internshipPlanSubmissionReducer(state, action); + + switch (action.type) { + case InternshipPlanActions.Save: + case InternshipPlanActions.Send: + return { + ...state, + plan: action.plan, + } + default: + return state; + } +} + +export default internshipPlanReducer; diff --git a/translations/pl.yaml b/translations/pl.yaml index e1564df..6c9bfc3 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -17,6 +17,7 @@ review: podgląd fix-errors: popraw uwagi contact: skontaktuj się z pełnomocnikiem comments: Zgłoszone uwagi +send-again: wyślij ponownie dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać" @@ -38,7 +39,7 @@ student: email: adres e-mail albumNumber: numer albumu -proposal: +submission: status: awaiting: "wysłano, oczekuje na weryfikacje" accepted: "zaakceptowano" @@ -69,7 +70,15 @@ steps: action: "zgłoś praktykę" plan: header: "Indywidualny Program Praktyki" - info: "" + info: + draft: > + TODO + awaiting: > + TODO + accepted: > + TODO + declined: > + TODO template: "Pobierz szablon" submit: "Wyślij Indywidualny Plan Praktyki" report: