Add reporting with custom fields support
This commit is contained in:
parent
3d827317f0
commit
7f71e6758c
@ -29,19 +29,18 @@ export const Step = (props: StepProps) => {
|
|||||||
{ label }
|
{ label }
|
||||||
<Box>
|
<Box>
|
||||||
{ state && <Typography variant="subtitle2" display="inline">{ state }</Typography> }
|
{ state && <Typography variant="subtitle2" display="inline">{ state }</Typography> }
|
||||||
|
{ (notBefore || until) && <Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography> }
|
||||||
{ notBefore &&
|
{ notBefore &&
|
||||||
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
||||||
{ t('not-before', { date: notBefore }) }
|
{ t('not-before', { date: notBefore }) }
|
||||||
</Typography> }
|
</Typography> }
|
||||||
{ until && <>
|
{ until &&
|
||||||
<Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography>
|
|
||||||
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
||||||
{ t('until', { date: until }) }
|
{ t('until', { date: until }) }
|
||||||
{ isLate && <Typography color="error" display="inline"
|
{ isLate && <Typography color="error" display="inline"
|
||||||
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
|
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
|
||||||
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
|
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
|
||||||
</Typography>
|
</Typography> }
|
||||||
</> }
|
|
||||||
</Box>
|
</Box>
|
||||||
</StepLabel>
|
</StepLabel>
|
||||||
{ children && <StepContent>{ children }</StepContent> }
|
{ children && <StepContent>{ children }</StepContent> }
|
||||||
|
37
src/data/report.ts
Normal file
37
src/data/report.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Multilingual } from "@/data/common";
|
||||||
|
|
||||||
|
interface PredefinedChoices {
|
||||||
|
choices: Multilingual<string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseField {
|
||||||
|
name: string;
|
||||||
|
description: Multilingual<string>;
|
||||||
|
label: Multilingual<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextField extends BaseField {
|
||||||
|
type: "text";
|
||||||
|
value: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiChoiceField extends BaseField, PredefinedChoices {
|
||||||
|
type: "multi-choice";
|
||||||
|
value: Multilingual<string>[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleChoiceField extends BaseField, PredefinedChoices {
|
||||||
|
type: "single-choice";
|
||||||
|
value: Multilingual<string> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectField extends BaseField, PredefinedChoices {
|
||||||
|
type: "select";
|
||||||
|
value: Multilingual<string> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReportField = TextField | MultiChoiceField | SingleChoiceField | SelectField;
|
||||||
|
|
||||||
|
export interface Report {
|
||||||
|
fields: ReportField[];
|
||||||
|
}
|
136
src/forms/report.tsx
Normal file
136
src/forms/report.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { emptyReport } from "@/provider/dummy/report";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
FormGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
Radio,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { route } from "@/routing";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MultiChoiceField, Report, ReportField, SelectField, SingleChoiceField } from "@/data/report";
|
||||||
|
import { TextField as TextFieldFormik } from "formik-material-ui";
|
||||||
|
import { Field, Form, Formik, useFormik, useFormikContext } from "formik";
|
||||||
|
import { Multilingual } from "@/data";
|
||||||
|
import { Transformer } from "@/serialization";
|
||||||
|
|
||||||
|
export type ReportFieldProps<TField = ReportField> = {
|
||||||
|
field: TField;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomField = ({ field, ...props }: ReportFieldProps) => {
|
||||||
|
switch (field.type) {
|
||||||
|
case "text":
|
||||||
|
return <CustomField.Text {...props} field={ field } />
|
||||||
|
case "single-choice":
|
||||||
|
case "multi-choice":
|
||||||
|
return <CustomField.Choice {...props} field={ field }/>
|
||||||
|
case "select":
|
||||||
|
return <CustomField.Select {...props} field={ field }/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomField.Text = ({ field }: ReportFieldProps) => {
|
||||||
|
return <>
|
||||||
|
<Field label={ field.label.pl } name={ field.name } fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomField.Select = ({ field }: ReportFieldProps<SelectField>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const id = `custom-field-${field.name}`;
|
||||||
|
const { values, setFieldValue } = useFormikContext<any>();
|
||||||
|
|
||||||
|
const value = values[field.name];
|
||||||
|
|
||||||
|
return <FormControl variant="outlined">
|
||||||
|
<InputLabel htmlFor={id}>{ field.label.pl }</InputLabel>
|
||||||
|
<Select label={ field.label.pl } name={ field.name } id={id} value={ value } onChange={ ({ target }) => setFieldValue(field.name, target.value, false) }>
|
||||||
|
{ field.choices.map(choice => <MenuItem value={ choice as any }>{ choice.pl }</MenuItem>) }
|
||||||
|
</Select>
|
||||||
|
<Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/>
|
||||||
|
</FormControl>
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomField.Choice = ({ field }: ReportFieldProps<SingleChoiceField | MultiChoiceField>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { values, setFieldValue } = useFormikContext<any>();
|
||||||
|
|
||||||
|
const value = values[field.name];
|
||||||
|
|
||||||
|
const isSelected = field.type == 'single-choice'
|
||||||
|
? (checked: Multilingual<string>) => value == checked
|
||||||
|
: (checked: Multilingual<string>) => (value || []).includes(checked)
|
||||||
|
|
||||||
|
const handleChange = field.type == 'single-choice'
|
||||||
|
? (choice: Multilingual<string>) => () => setFieldValue(field.name, choice, false)
|
||||||
|
: (choice: Multilingual<string>) => () => {
|
||||||
|
const current = value || [];
|
||||||
|
setFieldValue(field.name, !current.includes(choice) ? [ ...current, choice ] : current.filter((c: Multilingual<string>) => c != choice), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = field.type == 'single-choice' ? Radio : Checkbox;
|
||||||
|
|
||||||
|
return <FormControl component="fieldset">
|
||||||
|
<FormLabel component="legend">{ field.label.pl }</FormLabel>
|
||||||
|
<FormGroup>
|
||||||
|
{ field.choices.map(choice => <FormControlLabel
|
||||||
|
control={ <Component checked={ isSelected(choice) } onChange={ handleChange(choice) }/> }
|
||||||
|
label={ choice.pl }
|
||||||
|
/>) }
|
||||||
|
</FormGroup>
|
||||||
|
<Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/>
|
||||||
|
</FormControl>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReportFormValues = { [field: string]: any };
|
||||||
|
|
||||||
|
const reportToFormValuesTransformer: Transformer<Report, ReportFormValues, { report: Report }> = {
|
||||||
|
reverseTransform(subject: ReportFormValues, context: { report: Report }): Report {
|
||||||
|
return { ...context.report };
|
||||||
|
},
|
||||||
|
transform(subject: Report, context: undefined): ReportFormValues {
|
||||||
|
return Object.fromEntries(subject.fields.map(field => [ field.name, field.value ]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReportForm() {
|
||||||
|
const report = emptyReport;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleSubmit = async () => {};
|
||||||
|
|
||||||
|
return <Formik initialValues={ reportToFormValuesTransformer.transform(report) } onSubmit={ handleSubmit }>
|
||||||
|
{ ({ submitForm }) => <Form>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="body1" component="p">{ t('forms.report.instructions') }</Typography>
|
||||||
|
</Grid>
|
||||||
|
{ report.fields.map(field => <Grid item xs={12}><CustomField field={ field }/></Grid>) }
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Actions>
|
||||||
|
<Button variant="contained" color="primary" onClick={ submitForm }>
|
||||||
|
{ t('confirm') }
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button component={ RouterLink } to={ route("home") }>
|
||||||
|
{ t('go-back') }
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Form> }
|
||||||
|
</Formik>
|
||||||
|
}
|
||||||
|
|
26
src/pages/internship/report.tsx
Normal file
26
src/pages/internship/report.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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 React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ReportForm from "@/forms/report";
|
||||||
|
|
||||||
|
export const SubmitReportPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <Page title={ t("steps.report.submit") }>
|
||||||
|
<Page.Header maxWidth="md">
|
||||||
|
<Page.Breadcrumbs>
|
||||||
|
<Link component={ RouterLink } to={ route("home") }>{ t('pages.my-internship.header') }</Link>
|
||||||
|
<Typography color="textPrimary">{ t("steps.report.submit") }</Typography>
|
||||||
|
</Page.Breadcrumbs>
|
||||||
|
<Page.Title>{ t("steps.report.submit") }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
|
<Container maxWidth={ "md" }>
|
||||||
|
<ReportForm/>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubmitReportPage;
|
@ -18,6 +18,7 @@ import api from "@/api";
|
|||||||
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions";
|
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions";
|
||||||
import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration";
|
import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration";
|
||||||
import { UploadType } from "@/api/upload";
|
import { UploadType } from "@/api/upload";
|
||||||
|
import { ReportStep } from "@/pages/steps/report";
|
||||||
|
|
||||||
export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
||||||
const internship = await api.internship.get();
|
const internship = await api.internship.get();
|
||||||
@ -48,10 +49,8 @@ export const MainPage = () => {
|
|||||||
|
|
||||||
const student = useSelector<AppState, Student | null>(state => state.student);
|
const student = useSelector<AppState, Student | null>(state => state.student);
|
||||||
|
|
||||||
const deadlines = useDeadlines();
|
|
||||||
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
|
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const edition = useCurrentEdition();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(updateInternshipInfo);
|
dispatch(updateInternshipInfo);
|
||||||
@ -69,7 +68,7 @@ export const MainPage = () => {
|
|||||||
if (insurance.required)
|
if (insurance.required)
|
||||||
yield <InsuranceStep key="insurance"/>;
|
yield <InsuranceStep key="insurance"/>;
|
||||||
|
|
||||||
yield <Step label={ t('steps.report.header') } until={ deadlines.report } notBefore={ edition?.reportingStart } key="report"/>
|
yield <ReportStep key="report"/>;
|
||||||
yield <Step label={ t('steps.grade.header') } key="grade"/>
|
yield <Step label={ t('steps.grade.header') } key="grade"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
85
src/pages/steps/report.tsx
Normal file
85
src/pages/steps/report.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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 { FileUploadOutline } from "mdi-material-ui/index";
|
||||||
|
import { route } from "@/routing";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { Actions, Step } from "@/components";
|
||||||
|
import React, { HTMLProps } from "react";
|
||||||
|
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||||
|
import { ContactButton, Status } from "@/pages/steps/common";
|
||||||
|
import { useCurrentEdition, useDeadlines } from "@/hooks";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
|
||||||
|
const ReportActions = () => {
|
||||||
|
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.report));
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const FormAction = ({ children = t('steps.report.submit'), ...props }: ButtonProps) =>
|
||||||
|
<Button to={ route("internship_report") } variant="contained" color="primary" component={ RouterLink } startIcon={ <FileUploadOutline /> } { ...props as any }>
|
||||||
|
{ children }
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "awaiting":
|
||||||
|
return <Actions>
|
||||||
|
</Actions>
|
||||||
|
case "accepted":
|
||||||
|
return <Actions>
|
||||||
|
<FormAction variant="outlined" color="secondary">{ t('send-again') }</FormAction>
|
||||||
|
</Actions>
|
||||||
|
case "declined":
|
||||||
|
return <Actions>
|
||||||
|
<FormAction>{ t('send-again') }</FormAction>
|
||||||
|
<ContactButton/>
|
||||||
|
</Actions>
|
||||||
|
case "draft":
|
||||||
|
return <Actions>
|
||||||
|
<FormAction />
|
||||||
|
</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 ReportStep = (props: StepProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const submission = useSelector<AppState, SubmissionState>(state => state.report);
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
const edition = useCurrentEdition();
|
||||||
|
|
||||||
|
const status = getSubmissionStatus(submission);
|
||||||
|
const deadlines = useDeadlines();
|
||||||
|
|
||||||
|
const { sent, declined, comment } = submission;
|
||||||
|
|
||||||
|
return <Step { ...props }
|
||||||
|
label={ t('steps.report.header') }
|
||||||
|
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
|
||||||
|
until={ deadlines.report }
|
||||||
|
notBefore={ edition?.reportingStart }
|
||||||
|
state={ <Status submission={ submission } /> }>
|
||||||
|
<div className={ spacing.vertical }>
|
||||||
|
<p>{ t(`steps.report.info.${ status }`) }</p>
|
||||||
|
|
||||||
|
{ comment && <Box pb={ 2 }><PlanComment/></Box> }
|
||||||
|
|
||||||
|
<ReportActions/>
|
||||||
|
</div>
|
||||||
|
</Step>;
|
||||||
|
}
|
66
src/provider/dummy/report.ts
Normal file
66
src/provider/dummy/report.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Report } from "@/data/report";
|
||||||
|
|
||||||
|
const choices = [1, 2, 3, 4, 5].map(n => ({
|
||||||
|
pl: `Wybór ${n}`,
|
||||||
|
en: `Choice ${n}`
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const emptyReport: Report = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "text",
|
||||||
|
description: {
|
||||||
|
en: "Text field, with <strong>HTML</strong> description",
|
||||||
|
pl: "Pole tekstowe, z opisem w formacie <strong>HTML</strong>"
|
||||||
|
},
|
||||||
|
value: null,
|
||||||
|
label: {
|
||||||
|
en: "Text Field",
|
||||||
|
pl: "Pole tekstowe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "single-choice",
|
||||||
|
name: "single",
|
||||||
|
description: {
|
||||||
|
en: "single choice field, with <strong>HTML</strong> description",
|
||||||
|
pl: "Pole jednokrotnego wyboru, z opisem w formacie <strong>HTML</strong>"
|
||||||
|
},
|
||||||
|
value: null,
|
||||||
|
choices,
|
||||||
|
label: {
|
||||||
|
en: "Single choice field",
|
||||||
|
pl: "Pole jednokrotnego wyboru",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
name: "select",
|
||||||
|
description: {
|
||||||
|
en: "select field, with <strong>HTML</strong> description",
|
||||||
|
pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie <strong>HTML</strong>"
|
||||||
|
},
|
||||||
|
value: choices[2],
|
||||||
|
choices,
|
||||||
|
label: {
|
||||||
|
en: "Select field",
|
||||||
|
pl: "Pole jednokrotnego wyboru (selectbox)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi",
|
||||||
|
type: "multi-choice",
|
||||||
|
description: {
|
||||||
|
en: "Multiple choice field, with <strong>HTML</strong> description",
|
||||||
|
pl: "Pole wielokrotnego wyboru, z opisem w formacie <strong>HTML</strong>"
|
||||||
|
},
|
||||||
|
value: [ choices[0], choices[3] ],
|
||||||
|
choices,
|
||||||
|
label: {
|
||||||
|
en: "Multi choice field",
|
||||||
|
pl: "Pole wielokrotnego wyboru",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
@ -11,6 +11,7 @@ import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
|||||||
import UserFillPage from "@/pages/user/fill";
|
import UserFillPage from "@/pages/user/fill";
|
||||||
import UserProfilePage from "@/pages/user/profile";
|
import UserProfilePage from "@/pages/user/profile";
|
||||||
import { managementRoutes } from "@/management/routing";
|
import { managementRoutes } from "@/management/routing";
|
||||||
|
import SubmitReportPage from "@/pages/internship/report";
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -44,6 +45,7 @@ export const routes: Route[] = [
|
|||||||
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/>, middlewares: [ isReadyMiddleware ] },
|
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/>, middlewares: [ isReadyMiddleware ] },
|
||||||
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/>, middlewares: [ isReadyMiddleware ] },
|
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/>, middlewares: [ isReadyMiddleware ] },
|
||||||
{ name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/>, middlewares: [ isReadyMiddleware ] },
|
{ name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/>, middlewares: [ isReadyMiddleware ] },
|
||||||
|
{ name: "internship_report", path: "/internship/report", exact: true, content: () => <SubmitReportPage/>, middlewares: [ isReadyMiddleware ] },
|
||||||
|
|
||||||
// user
|
// user
|
||||||
{ name: "user_login", path: "/user/login", content: () => <UserLoginPage/> },
|
{ name: "user_login", path: "/user/login", content: () => <UserLoginPage/> },
|
||||||
|
4
src/serialization/report.ts
Normal file
4
src/serialization/report.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { identityTransformer, Serializable, Transformer } from "@/serialization/types";
|
||||||
|
import { Report } from "@/data/report";
|
||||||
|
|
||||||
|
export const reportSerializationTransformer: Transformer<Report, Serializable<Report>> = identityTransformer;
|
@ -9,6 +9,7 @@ import { UserAction, UserActions } from "@/state/actions/user";
|
|||||||
import { ThunkDispatch } from "redux-thunk";
|
import { ThunkDispatch } from "redux-thunk";
|
||||||
import { AppState } from "@/state/reducer";
|
import { AppState } from "@/state/reducer";
|
||||||
import { StudentAction, StudentActions } from "@/state/actions/student";
|
import { StudentAction, StudentActions } from "@/state/actions/student";
|
||||||
|
import { InternshipReportAction, InternshipReportActions } from "@/state/actions/report";
|
||||||
|
|
||||||
export * from "./base"
|
export * from "./base"
|
||||||
export * from "./edition"
|
export * from "./edition"
|
||||||
@ -17,6 +18,7 @@ export * from "./proposal"
|
|||||||
export * from "./plan"
|
export * from "./plan"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
export * from "./student"
|
export * from "./student"
|
||||||
|
export * from "./report"
|
||||||
|
|
||||||
export type Action
|
export type Action
|
||||||
= UserAction
|
= UserAction
|
||||||
@ -25,6 +27,7 @@ export type Action
|
|||||||
| InternshipProposalAction
|
| InternshipProposalAction
|
||||||
| StudentAction
|
| StudentAction
|
||||||
| InternshipPlanAction
|
| InternshipPlanAction
|
||||||
|
| InternshipReportAction
|
||||||
| InsuranceAction;
|
| InsuranceAction;
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
@ -35,6 +38,7 @@ export const Actions = {
|
|||||||
...InternshipPlanActions,
|
...InternshipPlanActions,
|
||||||
...InsuranceActions,
|
...InsuranceActions,
|
||||||
...StudentActions,
|
...StudentActions,
|
||||||
|
...InternshipReportActions,
|
||||||
}
|
}
|
||||||
export type Actions = typeof Actions;
|
export type Actions = typeof Actions;
|
||||||
export type AppDispatch = ThunkDispatch<AppState, any, Action>;
|
export type AppDispatch = ThunkDispatch<AppState, any, Action>;
|
||||||
|
43
src/state/actions/report.ts
Normal file
43
src/state/actions/report.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Internship } from "@/data";
|
||||||
|
import {
|
||||||
|
ReceiveSubmissionApproveAction,
|
||||||
|
ReceiveSubmissionDeclineAction,
|
||||||
|
ReceiveSubmissionUpdateAction,
|
||||||
|
SaveSubmissionAction,
|
||||||
|
SendSubmissionAction
|
||||||
|
} from "@/state/actions/submission";
|
||||||
|
import { SubmissionState } from "@/api/dto/internship-registration";
|
||||||
|
import { Report } from "@/data/report";
|
||||||
|
|
||||||
|
export enum InternshipReportActions {
|
||||||
|
Send = "SEND_REPORT",
|
||||||
|
Save = "SAVE_REPORT",
|
||||||
|
Approve = "RECEIVE_REPORT_APPROVE",
|
||||||
|
Decline = "RECEIVE_REPORT_DECLINE",
|
||||||
|
Receive = "RECEIVE_REPORT_STATE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendReportAction extends SendSubmissionAction<InternshipReportActions.Send> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiveReportApproveAction extends ReceiveSubmissionApproveAction<InternshipReportActions.Approve> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiveReportDeclineAction extends ReceiveSubmissionDeclineAction<InternshipReportActions.Decline> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiveReportUpdateAction extends ReceiveSubmissionUpdateAction<InternshipReportActions.Receive> {
|
||||||
|
report: Report;
|
||||||
|
state: SubmissionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveReportAction extends SaveSubmissionAction<InternshipReportActions.Save> {
|
||||||
|
report: Report;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InternshipReportAction
|
||||||
|
= SendReportAction
|
||||||
|
| SaveReportAction
|
||||||
|
| ReceiveReportApproveAction
|
||||||
|
| ReceiveReportDeclineAction
|
||||||
|
| ReceiveReportUpdateAction;
|
@ -7,6 +7,7 @@ import internshipProposalReducer from "@/state/reducer/proposal";
|
|||||||
import internshipPlanReducer from "@/state/reducer/plan";
|
import internshipPlanReducer from "@/state/reducer/plan";
|
||||||
import insuranceReducer from "@/state/reducer/insurance";
|
import insuranceReducer from "@/state/reducer/insurance";
|
||||||
import userReducer from "@/state/reducer/user";
|
import userReducer from "@/state/reducer/user";
|
||||||
|
import internshipReportReducer from "@/state/reducer/report";
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
student: studentReducer,
|
student: studentReducer,
|
||||||
@ -16,6 +17,7 @@ const rootReducer = combineReducers({
|
|||||||
plan: internshipPlanReducer,
|
plan: internshipPlanReducer,
|
||||||
insurance: insuranceReducer,
|
insurance: insuranceReducer,
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
|
report: internshipReportReducer,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AppState = ReturnType<typeof rootReducer>;
|
export type AppState = ReturnType<typeof rootReducer>;
|
||||||
|
66
src/state/reducer/report.ts
Normal file
66
src/state/reducer/report.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { InternshipReportAction, InternshipReportActions } from "@/state/actions";
|
||||||
|
import { Serializable } from "@/serialization/types";
|
||||||
|
import {
|
||||||
|
createSubmissionReducer,
|
||||||
|
defaultDeanApprovalsState,
|
||||||
|
defaultSubmissionState,
|
||||||
|
SubmissionState
|
||||||
|
} from "@/state/reducer/submission";
|
||||||
|
import { Reducer } from "react";
|
||||||
|
import { SubmissionAction } from "@/state/actions/submission";
|
||||||
|
import { SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
|
||||||
|
import { Report } from "@/data/report";
|
||||||
|
import { reportSerializationTransformer } from "@/serialization/report";
|
||||||
|
|
||||||
|
export type InternshipReportState = SubmissionState & {
|
||||||
|
report: Serializable<Report> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultInternshipReportState: InternshipReportState = {
|
||||||
|
...defaultDeanApprovalsState,
|
||||||
|
...defaultSubmissionState,
|
||||||
|
report: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getInternshipReport = ({ report }: InternshipReportState): Report | null =>
|
||||||
|
report && reportSerializationTransformer.reverseTransform(report);
|
||||||
|
|
||||||
|
const internshipReportSubmissionReducer: Reducer<InternshipReportState, InternshipReportAction> = createSubmissionReducer({
|
||||||
|
[InternshipReportActions.Approve]: SubmissionAction.Approve,
|
||||||
|
[InternshipReportActions.Decline]: SubmissionAction.Decline,
|
||||||
|
[InternshipReportActions.Receive]: SubmissionAction.Receive,
|
||||||
|
[InternshipReportActions.Save]: SubmissionAction.Save,
|
||||||
|
[InternshipReportActions.Send]: SubmissionAction.Send,
|
||||||
|
})
|
||||||
|
|
||||||
|
const internshipReportReducer = (state: InternshipReportState = defaultInternshipReportState, action: InternshipReportAction): InternshipReportState => {
|
||||||
|
state = internshipReportSubmissionReducer(state, action);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case InternshipReportActions.Save:
|
||||||
|
case InternshipReportActions.Send:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
}
|
||||||
|
case InternshipReportActions.Receive:
|
||||||
|
if (state.overwritten) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accepted: action.state === ApiSubmissionState.Accepted,
|
||||||
|
declined: action.state === ApiSubmissionState.Rejected,
|
||||||
|
sent: [
|
||||||
|
ApiSubmissionState.Accepted,
|
||||||
|
ApiSubmissionState.Rejected,
|
||||||
|
ApiSubmissionState.Submitted
|
||||||
|
].includes(action.state),
|
||||||
|
report: reportSerializationTransformer.transform(action.report),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default internshipReportReducer;
|
Loading…
Reference in New Issue
Block a user