Refactor submission state management so it is reusable
This commit is contained in:
		
							parent
							
								
									a3475c5f30
								
							
						
					
					
						commit
						886153afb5
					
				@ -67,6 +67,10 @@ export interface Internship extends Identifiable {
 | 
			
		||||
    office: BranchOffice;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Plan extends Identifiable {
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Mentor {
 | 
			
		||||
    name: string;
 | 
			
		||||
    surname: string;
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,11 @@ 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 { Link as RouterLink, useHistory } from "react-router-dom";
 | 
			
		||||
import { route } from "@/routing";
 | 
			
		||||
import { useProxyState } from "@/hooks";
 | 
			
		||||
import { getInternshipProposal } from "@/state/reducer/proposal";
 | 
			
		||||
import { Actions } from "@/components";
 | 
			
		||||
 | 
			
		||||
export type InternshipFormProps = {}
 | 
			
		||||
 | 
			
		||||
@ -161,7 +162,13 @@ 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 }/>
 | 
			
		||||
            <Actions>
 | 
			
		||||
                <Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
 | 
			
		||||
 | 
			
		||||
                <Button component={ RouterLink } to={ route("home") }>
 | 
			
		||||
                    { t('go-back') }
 | 
			
		||||
                </Button>
 | 
			
		||||
            </Actions>
 | 
			
		||||
        </div>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,13 +10,14 @@ 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 { getInternshipProposalStatus, InternshipProposalState, InternshipProposalStatus } from "@/state/reducer/proposal";
 | 
			
		||||
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: InternshipProposalStatus, theme: Theme) => {
 | 
			
		||||
const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
        case "awaiting":
 | 
			
		||||
            return theme.palette.info.dark;
 | 
			
		||||
@ -45,7 +46,7 @@ const useStatusStyles = makeStyles((theme: Theme) => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const ProposalStatus = () => {
 | 
			
		||||
    const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal))
 | 
			
		||||
    const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal))
 | 
			
		||||
    const classes = useStatusStyles({ status });
 | 
			
		||||
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
@ -54,7 +55,7 @@ const ProposalStatus = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProposalActions = () => {
 | 
			
		||||
    const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal));
 | 
			
		||||
    const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    const ReviewAction = (props: ButtonProps) =>
 | 
			
		||||
@ -96,17 +97,17 @@ export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
 | 
			
		||||
    const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    return <Alert severity={declined ? "error" : "warning"} {...props as any}>
 | 
			
		||||
    return comment ? <Alert severity={declined ? "error" : "warning"} {...props as any}>
 | 
			
		||||
        <AlertTitle>{ t('comments') }</AlertTitle>
 | 
			
		||||
        { comment }
 | 
			
		||||
    </Alert>
 | 
			
		||||
    </Alert> : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProposalStep = (props: StepProps) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    const { sent, comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
 | 
			
		||||
    const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(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 }
 | 
			
		||||
 | 
			
		||||
@ -1,7 +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),
 | 
			
		||||
export const momentSerializationTransformer: SerializationTransformer<Moment | null, string> = {
 | 
			
		||||
    transform: (subject: Moment) => subject && subject.toISOString(),
 | 
			
		||||
    reverseTransform: (subject: string) => subject ? moment(subject) : null,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,11 @@
 | 
			
		||||
import { Action } from "@/state/actions/base";
 | 
			
		||||
import { Internship } from "@/data";
 | 
			
		||||
import {
 | 
			
		||||
    ReceiveSubmissionApproveAction,
 | 
			
		||||
    ReceiveSubmissionDeclineAction,
 | 
			
		||||
    ReceiveSubmissionUpdateAction,
 | 
			
		||||
    SaveSubmissionAction,
 | 
			
		||||
    SendSubmissionAction
 | 
			
		||||
} from "@/state/actions/submission";
 | 
			
		||||
 | 
			
		||||
export enum InternshipProposalActions {
 | 
			
		||||
    Send = "SEND_PROPOSAL",
 | 
			
		||||
@ -9,23 +15,23 @@ export enum InternshipProposalActions {
 | 
			
		||||
    Receive = "RECEIVE_PROPOSAL_STATE",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SendProposalAction extends Action<InternshipProposalActions.Send> {
 | 
			
		||||
export interface SendProposalAction extends SendSubmissionAction<InternshipProposalActions.Send> {
 | 
			
		||||
    internship: Internship;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReceiveProposalApproveAction extends Action<InternshipProposalActions.Approve> {
 | 
			
		||||
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
 | 
			
		||||
    comment: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReceiveProposalDeclineAction extends Action<InternshipProposalActions.Decline> {
 | 
			
		||||
export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction<InternshipProposalActions.Decline> {
 | 
			
		||||
    comment: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReceiveProposalUpdateAction extends Action<InternshipProposalActions.Receive> {
 | 
			
		||||
export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction<InternshipProposalActions.Receive> {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SaveProposalAction extends Action<InternshipProposalActions.Save> {
 | 
			
		||||
export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> {
 | 
			
		||||
    internship: Internship;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								src/state/actions/submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/state/actions/submission.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
import { Action } from "@/state/actions/base";
 | 
			
		||||
 | 
			
		||||
export enum SubmissionAction {
 | 
			
		||||
    Send = "SEND",
 | 
			
		||||
    Save = "SAVE",
 | 
			
		||||
    Approve = "RECEIVE_APPROVE",
 | 
			
		||||
    Decline = "RECEIVE_DECLINE",
 | 
			
		||||
    Receive = "RECEIVE_STATE",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SendSubmissionAction<T extends string> extends Action<T> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReceiveSubmissionApproveAction<T extends string> extends Action<T> {
 | 
			
		||||
    comment: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReceiveSubmissionDeclineAction<T extends string> extends Action<T> {
 | 
			
		||||
    comment: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReceiveSubmissionUpdateAction<T extends string> extends Action<T> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SaveSubmissionAction<T extends string> extends Action<T> {
 | 
			
		||||
}
 | 
			
		||||
@ -1,80 +1,47 @@
 | 
			
		||||
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";
 | 
			
		||||
import { internshipSerializationTransformer } from "@/serialization";
 | 
			
		||||
import {
 | 
			
		||||
    createSubmissionReducer,
 | 
			
		||||
    defaultDeanApprovalsState,
 | 
			
		||||
    defaultSubmissionState,
 | 
			
		||||
    MayRequireDeanApproval,
 | 
			
		||||
    SubmissionState
 | 
			
		||||
} from "@/state/reducer/submission";
 | 
			
		||||
import { Reducer } from "react";
 | 
			
		||||
import { SubmissionAction } from "@/state/actions/submission";
 | 
			
		||||
 | 
			
		||||
export type InternshipProposalStatus = "draft" | "awaiting" | "accepted" | "declined";
 | 
			
		||||
 | 
			
		||||
export type InternshipProposalState = {
 | 
			
		||||
    accepted: boolean;
 | 
			
		||||
    sent: boolean;
 | 
			
		||||
    sentOn: string | null;
 | 
			
		||||
    declined: boolean;
 | 
			
		||||
    requiredDeanApprovals: DeanApproval[];
 | 
			
		||||
export type InternshipProposalState = SubmissionState & MayRequireDeanApproval & {
 | 
			
		||||
    proposal: Serializable<Internship> | null;
 | 
			
		||||
    comment: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultInternshipProposalState: InternshipProposalState = {
 | 
			
		||||
    accepted: false,
 | 
			
		||||
    declined: false,
 | 
			
		||||
    sentOn: null,
 | 
			
		||||
    ...defaultDeanApprovalsState,
 | 
			
		||||
    ...defaultSubmissionState,
 | 
			
		||||
    proposal: null,
 | 
			
		||||
    requiredDeanApprovals: [],
 | 
			
		||||
    sent: false,
 | 
			
		||||
    comment: null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): InternshipProposalStatus => {
 | 
			
		||||
    switch (true) {
 | 
			
		||||
        case !sent:
 | 
			
		||||
            return "draft";
 | 
			
		||||
        case sent && accepted:
 | 
			
		||||
            return "accepted"
 | 
			
		||||
        case sent && declined:
 | 
			
		||||
            return "declined"
 | 
			
		||||
        case sent && (!accepted && !declined):
 | 
			
		||||
            return "awaiting"
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error("Invalid proposal state " + JSON.stringify({ accepted, declined, sent }));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
 | 
			
		||||
    proposal && internshipSerializationTransformer.reverseTransform(proposal);
 | 
			
		||||
 | 
			
		||||
const internshipProposalSubmissionReducer: Reducer<InternshipProposalState, InternshipProposalAction> = createSubmissionReducer({
 | 
			
		||||
    [InternshipProposalActions.Approve]: SubmissionAction.Approve,
 | 
			
		||||
    [InternshipProposalActions.Decline]: SubmissionAction.Decline,
 | 
			
		||||
    [InternshipProposalActions.Receive]: SubmissionAction.Receive,
 | 
			
		||||
    [InternshipProposalActions.Save]: SubmissionAction.Save,
 | 
			
		||||
    [InternshipProposalActions.Send]: SubmissionAction.Send,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => {
 | 
			
		||||
    state = internshipProposalSubmissionReducer(state, action);
 | 
			
		||||
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
        case InternshipProposalActions.Approve:
 | 
			
		||||
            return {
 | 
			
		||||
                ...state,
 | 
			
		||||
                accepted: true,
 | 
			
		||||
                declined: false,
 | 
			
		||||
                comment: action.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: internshipSerializationTransformer.transform(action.internship),
 | 
			
		||||
                sent: true,
 | 
			
		||||
                sentOn: momentSerializationTransformer.transform(moment()),
 | 
			
		||||
                accepted: false,
 | 
			
		||||
                declined: false,
 | 
			
		||||
                comment: null,
 | 
			
		||||
            }
 | 
			
		||||
        default:
 | 
			
		||||
            return state;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										80
									
								
								src/state/reducer/submission.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/state/reducer/submission.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
import { DeanApproval } from "@/data/deanApproval";
 | 
			
		||||
import { Action } from "@/state/actions";
 | 
			
		||||
import { momentSerializationTransformer } from "@/serialization";
 | 
			
		||||
import moment from "moment";
 | 
			
		||||
import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, SubmissionAction } from "@/state/actions/submission";
 | 
			
		||||
 | 
			
		||||
export type SubmissionStatus = "draft" | "awaiting" | "accepted" | "declined";
 | 
			
		||||
 | 
			
		||||
export type SubmissionState = {
 | 
			
		||||
    accepted: boolean;
 | 
			
		||||
    sent: boolean;
 | 
			
		||||
    sentOn: string | null;
 | 
			
		||||
    declined: boolean;
 | 
			
		||||
    comment: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type MayRequireDeanApproval = {
 | 
			
		||||
    requiredDeanApprovals: DeanApproval[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const defaultSubmissionState: SubmissionState = {
 | 
			
		||||
    accepted: false,
 | 
			
		||||
    sent: false,
 | 
			
		||||
    sentOn: null,
 | 
			
		||||
    declined: false,
 | 
			
		||||
    comment: null,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const defaultDeanApprovalsState: MayRequireDeanApproval = {
 | 
			
		||||
    requiredDeanApprovals: [],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getSubmissionStatus = ({ accepted, declined, sent }: SubmissionState): SubmissionStatus => {
 | 
			
		||||
    switch (true) {
 | 
			
		||||
        case !sent:
 | 
			
		||||
            return "draft";
 | 
			
		||||
        case sent && accepted:
 | 
			
		||||
            return "accepted"
 | 
			
		||||
        case sent && declined:
 | 
			
		||||
            return "declined"
 | 
			
		||||
        case sent && (!accepted && !declined):
 | 
			
		||||
            return "awaiting"
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error("Invalid submission state " + JSON.stringify({ accepted, declined, sent }));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createSubmissionReducer<TState, TActionType, TAction extends Action>(mapping: { [TAction in keyof TActionType]: SubmissionAction }) {
 | 
			
		||||
    return (state: TState, action: TAction) => {
 | 
			
		||||
        const mappedAction = mapping[action.type as keyof TActionType];
 | 
			
		||||
 | 
			
		||||
        switch (mappedAction) {
 | 
			
		||||
            case SubmissionAction.Approve:
 | 
			
		||||
                return {
 | 
			
		||||
                    ...state,
 | 
			
		||||
                    accepted: true,
 | 
			
		||||
                    declined: false,
 | 
			
		||||
                    comment: (action as ReceiveSubmissionApproveAction<any>).comment,
 | 
			
		||||
                }
 | 
			
		||||
            case SubmissionAction.Decline:
 | 
			
		||||
                return {
 | 
			
		||||
                    ...state,
 | 
			
		||||
                    accepted: false,
 | 
			
		||||
                    declined: true,
 | 
			
		||||
                    comment: (action as ReceiveSubmissionDeclineAction<any>).comment,
 | 
			
		||||
                }
 | 
			
		||||
            case SubmissionAction.Send:
 | 
			
		||||
                return {
 | 
			
		||||
                    ...state,
 | 
			
		||||
                    sent: true,
 | 
			
		||||
                    sentOn: momentSerializationTransformer.transform(moment()),
 | 
			
		||||
                    accepted: false,
 | 
			
		||||
                    declined: false,
 | 
			
		||||
                    comment: null,
 | 
			
		||||
                }
 | 
			
		||||
            default:
 | 
			
		||||
                return state;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user