Add IPP state management

This commit is contained in:
Kacper Donat 2020-08-04 20:20:58 +02:00 committed by Gitea
parent 886153afb5
commit 5cc9f51584
13 changed files with 391 additions and 162 deletions

50
src/forms/plan.tsx Normal file
View File

@ -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<Plan>({});
const dispatch = useDispatch();
const history = useHistory();
const handleSubmit = () => {
dispatch({ type: InternshipPlanActions.Send, plan });
history.push(route("home"))
}
return <Grid container>
<Grid item>
<Typography variant="body1" component="p">{ t('forms.plan.instructions') }</Typography>
</Grid>
<Grid item>
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }>
{ t('steps.plan.template') }
</Button>
</Grid>
<Grid item>
<DropzoneArea acceptedFiles={["image/*", "application/x-pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/>
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
</Grid>
<Grid item>
<Actions>
<Button variant="contained" color="primary" onClick={ handleSubmit }>
{ t('confirm') }
</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</Grid>
</Grid>
}

View File

@ -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";

View File

@ -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 = () => {
<Page.Title>{ t("steps.plan.submit") }</Page.Title>
</Page.Header>
<Container maxWidth={ "md" }>
<Grid container>
<Grid item>
<Typography variant="body1" component="p">{ t('forms.plan.instructions') }</Typography>
</Grid>
<Grid item>
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }>
{ t('steps.plan.template') }
</Button>
</Grid>
<Grid item>
<DropzoneArea acceptedFiles={["image/*", "application/x-pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/>
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
</Grid>
<Grid item>
<Actions>
<Button variant="contained" color="primary">
{ t('confirm') }
</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</Grid>
</Grid>
<PlanForm />
</Container>
</Page>
}

View File

@ -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<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal))
const classes = useStatusStyles({ status });
const { t } = useTranslation();
return <span className={ classes.foreground }>{ t(`proposal.status.${status}`) }</span>;
}
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>
const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) =>
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink } startIcon={ <ClipboardEditOutline /> } { ...props as any }>
{ children }
</Button>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion /> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction />
</Actions>
case "accepted":
return <Actions>
<ReviewAction />
<FormAction variant="outlined" color="secondary">{ t('make-changes') }</FormAction>
</Actions>
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<ContactAction/>
</Actions>
case "draft":
return <Actions>
<FormAction color="primary">{ t('steps.internship-proposal.action') }</FormAction>
</Actions>
default:
return <Actions />
}
}
export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
const { t } = useTranslation();
return comment ? <Alert severity={declined ? "error" : "warning"} {...props as any}>
<AlertTitle>{ t('comments') }</AlertTitle>
{ comment }
</Alert> : null
}
const ProposalStep = (props: StepProps) => {
const { t } = useTranslation();
const { sent, comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(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 } declined={ declined } waiting={ status == "awaiting" }
until={ deadlines.proposal }
state={ <ProposalStatus /> }>
<p>{ t(`steps.internship-proposal.info.${status}`) }</p>
{ comment && <Box pb={2}><ProposalComment /></Box> }
<ProposalActions />
</Step>;
}
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 = () => {
</> }
</Step>
<ProposalStep />
<Step label={ t('steps.plan.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
<p>{ t('steps.plan.info') }</p>
<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>
</Actions>
</Step>
<PlanStep />
<Step label={ t('steps.insurance.header') }/>
<Step label={ t('steps.report.header') } until={ deadlines.report }/>
<Step label={ t('steps.grade.header') }/>

View File

@ -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 <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>;
}

92
src/pages/steps/plan.tsx Normal file
View File

@ -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<AppState, SubmissionStatus>(state => getSubmissionStatus(state.plan));
const { t } = useTranslation();
const ReviewAction = (props: ButtonProps) =>
<Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</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>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction/>
</Actions>
case "accepted":
return <Actions>
<ReviewAction/>
<FormAction variant="outlined" color="secondary">{ t('send-again') }</FormAction>
</Actions>
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<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>
</Actions>
default:
return <Actions/>
}
}
export const PlanComment = (props: HTMLProps<HTMLDivElement>) => {
const { comment, declined } = useSelector<AppState, SubmissionState>(state => state.plan);
const { t } = useTranslation();
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
<AlertTitle>{ t('comments') }</AlertTitle>
{ comment }
</Alert> : null
}
export const PlanStep = (props: StepProps) => {
const { t } = useTranslation();
const submission = useSelector<AppState, SubmissionState>(state => state.plan);
const status = getSubmissionStatus(submission);
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
const { sent, declined, comment } = submission;
return <Step { ...props }
label={ t('steps.plan.header') }
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
until={ deadlines.proposal }
state={ <Status submission={ submission } /> }>
<p>{ t(`steps.plan.info.${ status }`) }</p>
{ comment && <Box pb={ 2 }><PlanComment/></Box> }
<PlanActions/>
</Step>;
}

View File

@ -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<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
const { t } = useTranslation();
const ReviewAction = (props: ButtonProps) =>
<Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</Button>
const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) =>
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }
startIcon={ <ClipboardEditOutline/> } { ...props as any }>
{ children }
</Button>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction/>
</Actions>
case "accepted":
return <Actions>
<ReviewAction/>
<FormAction variant="outlined" color="secondary">{ t('make-changes') }</FormAction>
</Actions>
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<ContactAction/>
</Actions>
case "draft":
return <Actions>
<FormAction color="primary">{ t('steps.internship-proposal.action') }</FormAction>
</Actions>
default:
return <Actions/>
}
}
export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
const { t } = useTranslation();
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
<AlertTitle>{ t('comments') }</AlertTitle>
{ comment }
</Alert> : null
}
export const ProposalStep = (props: StepProps) => {
const { t } = useTranslation();
const submission = useSelector<AppState, SubmissionState>(state => state.proposal);
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
const { sent, declined, comment } = submission;
return <Step { ...props }
label={ t('steps.internship-proposal.header') }
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
until={ deadlines.proposal }
state={ <Status submission={ submission } /> }>
<p>{ t(`steps.internship-proposal.info.${ status }`) }</p>
{ comment && <Box pb={ 2 }><ProposalComment/></Box> }
<ProposalActions/>
</Step>;
}

View File

@ -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<Dispatch<Action>>()

40
src/state/actions/plan.ts Normal file
View File

@ -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<InternshipPlanActions.Send> {
plan: Plan;
}
export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction<InternshipPlanActions.Approve> {
}
export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction<InternshipPlanActions.Decline> {
}
export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction<InternshipPlanActions.Receive> {
}
export interface SavePlanAction extends SaveSubmissionAction<InternshipPlanActions.Save> {
plan: Plan;
}
export type InternshipPlanAction
= SendPlanAction
| SavePlanAction
| ReceivePlanApproveAction
| ReceivePlanDeclineAction
| ReceivePlanUpdateAction;

View File

@ -20,15 +20,12 @@ export interface SendProposalAction extends SendSubmissionAction<InternshipPropo
}
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
comment: string | null;
}
export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction<InternshipProposalActions.Decline> {
comment: string;
}
export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction<InternshipProposalActions.Receive> {
}
export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> {

View File

@ -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<typeof rootReducer>;

49
src/state/reducer/plan.ts Normal file
View File

@ -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<Plan> | null;
}
const defaultInternshipPlanState: InternshipPlanState = {
...defaultDeanApprovalsState,
...defaultSubmissionState,
plan: null,
}
export const getInternshipPlan = ({ plan }: InternshipPlanState): Plan | null => plan;
const internshipPlanSubmissionReducer: Reducer<InternshipPlanState, InternshipPlanAction> = 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;

View File

@ -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: