diff --git a/src/components/step.tsx b/src/components/step.tsx index 3f322a7..d2b416e 100644 --- a/src/components/step.tsx +++ b/src/components/step.tsx @@ -29,19 +29,18 @@ export const Step = (props: StepProps) => { { label } { state && { state } } + { (notBefore || until) && } { notBefore && { t('not-before', { date: notBefore }) } } - { until && <> - + { until && { t('until', { date: until }) } { isLate && - { t('late', { by: moment.duration(now.diff(until)) }) } } { !isLate && !completed && - { t('left', { left: left }) } } - - } + } { children && { children } } diff --git a/src/data/report.ts b/src/data/report.ts new file mode 100644 index 0000000..d399ce8 --- /dev/null +++ b/src/data/report.ts @@ -0,0 +1,37 @@ +import { Multilingual } from "@/data/common"; + +interface PredefinedChoices { + choices: Multilingual[]; +} + +export interface BaseField { + name: string; + description: Multilingual; + label: Multilingual; +} + +export interface TextField extends BaseField { + type: "text"; + value: string | null; +} + +export interface MultiChoiceField extends BaseField, PredefinedChoices { + type: "multi-choice"; + value: Multilingual[] | null; +} + +export interface SingleChoiceField extends BaseField, PredefinedChoices { + type: "single-choice"; + value: Multilingual | null +} + +export interface SelectField extends BaseField, PredefinedChoices { + type: "select"; + value: Multilingual | null +} + +export type ReportField = TextField | MultiChoiceField | SingleChoiceField | SelectField; + +export interface Report { + fields: ReportField[]; +} diff --git a/src/forms/report.tsx b/src/forms/report.tsx new file mode 100644 index 0000000..a4936bf --- /dev/null +++ b/src/forms/report.tsx @@ -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 = { + field: TField; +} + +const CustomField = ({ field, ...props }: ReportFieldProps) => { + switch (field.type) { + case "text": + return + case "single-choice": + case "multi-choice": + return + case "select": + return + } +} + +CustomField.Text = ({ field }: ReportFieldProps) => { + return <> + + + +} + +CustomField.Select = ({ field }: ReportFieldProps) => { + const { t } = useTranslation(); + const id = `custom-field-${field.name}`; + const { values, setFieldValue } = useFormikContext(); + + const value = values[field.name]; + + return + { field.label.pl } + + + +} + +CustomField.Choice = ({ field }: ReportFieldProps) => { + const { t } = useTranslation(); + const { values, setFieldValue } = useFormikContext(); + + const value = values[field.name]; + + const isSelected = field.type == 'single-choice' + ? (checked: Multilingual) => value == checked + : (checked: Multilingual) => (value || []).includes(checked) + + const handleChange = field.type == 'single-choice' + ? (choice: Multilingual) => () => setFieldValue(field.name, choice, false) + : (choice: Multilingual) => () => { + const current = value || []; + setFieldValue(field.name, !current.includes(choice) ? [ ...current, choice ] : current.filter((c: Multilingual) => c != choice), false); + } + + const Component = field.type == 'single-choice' ? Radio : Checkbox; + + return + { field.label.pl } + + { field.choices.map(choice => } + label={ choice.pl } + />) } + + + +} + +export type ReportFormValues = { [field: string]: any }; + +const reportToFormValuesTransformer: Transformer = { + 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 + { ({ submitForm }) =>
+ + + { t('forms.report.instructions') } + + { report.fields.map(field => ) } + + + + + + + + +
} +
+} + diff --git a/src/pages/internship/report.tsx b/src/pages/internship/report.tsx new file mode 100644 index 0000000..40af7c9 --- /dev/null +++ b/src/pages/internship/report.tsx @@ -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 + + + { t('pages.my-internship.header') } + { t("steps.report.submit") } + + { t("steps.report.submit") } + + + + + +} + +export default SubmitReportPage; diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 761a47c..fee4de1 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -18,6 +18,7 @@ import api from "@/api"; import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions"; import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration"; import { UploadType } from "@/api/upload"; +import { ReportStep } from "@/pages/steps/report"; export const updateInternshipInfo = async (dispatch: AppDispatch) => { const internship = await api.internship.get(); @@ -48,10 +49,8 @@ export const MainPage = () => { const student = useSelector(state => state.student); - const deadlines = useDeadlines(); const insurance = useSelector(root => root.insurance); const dispatch = useDispatch(); - const edition = useCurrentEdition(); useEffect(() => { dispatch(updateInternshipInfo); @@ -69,7 +68,7 @@ export const MainPage = () => { if (insurance.required) yield ; - yield + yield ; yield } diff --git a/src/pages/steps/report.tsx b/src/pages/steps/report.tsx new file mode 100644 index 0000000..083021f --- /dev/null +++ b/src/pages/steps/report.tsx @@ -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(state => getSubmissionStatus(state.report)); + + const { t } = useTranslation(); + + const FormAction = ({ children = t('steps.report.submit'), ...props }: ButtonProps) => + + + switch (status) { + case "awaiting": + return + + case "accepted": + return + { t('send-again') } + + case "declined": + return + { t('send-again') } + + + 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 ReportStep = (props: StepProps) => { + const { t } = useTranslation(); + + const submission = useSelector(state => state.report); + const spacing = useSpacing(2); + const edition = useCurrentEdition(); + + const status = getSubmissionStatus(submission); + const deadlines = useDeadlines(); + + const { sent, declined, comment } = submission; + + return }> +
+

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

+ + { comment && } + + +
+
; +} diff --git a/src/provider/dummy/report.ts b/src/provider/dummy/report.ts new file mode 100644 index 0000000..ad77aad --- /dev/null +++ b/src/provider/dummy/report.ts @@ -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 HTML description", + pl: "Pole tekstowe, z opisem w formacie HTML" + }, + value: null, + label: { + en: "Text Field", + pl: "Pole tekstowe", + }, + }, + { + type: "single-choice", + name: "single", + description: { + en: "single choice field, with HTML description", + pl: "Pole jednokrotnego wyboru, z opisem w formacie HTML" + }, + value: null, + choices, + label: { + en: "Single choice field", + pl: "Pole jednokrotnego wyboru", + }, + }, + { + type: "select", + name: "select", + description: { + en: "select field, with HTML description", + pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie HTML" + }, + value: choices[2], + choices, + label: { + en: "Select field", + pl: "Pole jednokrotnego wyboru (selectbox)", + }, + }, + { + name: "multi", + type: "multi-choice", + description: { + en: "Multiple choice field, with HTML description", + pl: "Pole wielokrotnego wyboru, z opisem w formacie HTML" + }, + value: [ choices[0], choices[3] ], + choices, + label: { + en: "Multi choice field", + pl: "Pole wielokrotnego wyboru", + }, + }, + ] +} diff --git a/src/routing.tsx b/src/routing.tsx index a88566b..9fcdb47 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -11,6 +11,7 @@ import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware"; import UserFillPage from "@/pages/user/fill"; import UserProfilePage from "@/pages/user/profile"; import { managementRoutes } from "@/management/routing"; +import SubmitReportPage from "@/pages/internship/report"; export type Route = { name?: string; @@ -44,6 +45,7 @@ export const routes: Route[] = [ { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, { name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, { name: "internship_plan", path: "/internship/plan", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, + { name: "internship_report", path: "/internship/report", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, // user { name: "user_login", path: "/user/login", content: () => }, diff --git a/src/serialization/report.ts b/src/serialization/report.ts new file mode 100644 index 0000000..32f8238 --- /dev/null +++ b/src/serialization/report.ts @@ -0,0 +1,4 @@ +import { identityTransformer, Serializable, Transformer } from "@/serialization/types"; +import { Report } from "@/data/report"; + +export const reportSerializationTransformer: Transformer> = identityTransformer; diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 0fc0534..626d47c 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -9,6 +9,7 @@ import { UserAction, UserActions } from "@/state/actions/user"; import { ThunkDispatch } from "redux-thunk"; import { AppState } from "@/state/reducer"; import { StudentAction, StudentActions } from "@/state/actions/student"; +import { InternshipReportAction, InternshipReportActions } from "@/state/actions/report"; export * from "./base" export * from "./edition" @@ -17,6 +18,7 @@ export * from "./proposal" export * from "./plan" export * from "./user" export * from "./student" +export * from "./report" export type Action = UserAction @@ -25,6 +27,7 @@ export type Action | InternshipProposalAction | StudentAction | InternshipPlanAction + | InternshipReportAction | InsuranceAction; export const Actions = { @@ -35,6 +38,7 @@ export const Actions = { ...InternshipPlanActions, ...InsuranceActions, ...StudentActions, + ...InternshipReportActions, } export type Actions = typeof Actions; export type AppDispatch = ThunkDispatch; diff --git a/src/state/actions/report.ts b/src/state/actions/report.ts new file mode 100644 index 0000000..c98aa7b --- /dev/null +++ b/src/state/actions/report.ts @@ -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 { +} + +export interface ReceiveReportApproveAction extends ReceiveSubmissionApproveAction { +} + +export interface ReceiveReportDeclineAction extends ReceiveSubmissionDeclineAction { +} + +export interface ReceiveReportUpdateAction extends ReceiveSubmissionUpdateAction { + report: Report; + state: SubmissionState, +} + +export interface SaveReportAction extends SaveSubmissionAction { + report: Report; +} + +export type InternshipReportAction + = SendReportAction + | SaveReportAction + | ReceiveReportApproveAction + | ReceiveReportDeclineAction + | ReceiveReportUpdateAction; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index d57303b..91d4521 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -7,6 +7,7 @@ import internshipProposalReducer from "@/state/reducer/proposal"; import internshipPlanReducer from "@/state/reducer/plan"; import insuranceReducer from "@/state/reducer/insurance"; import userReducer from "@/state/reducer/user"; +import internshipReportReducer from "@/state/reducer/report"; const rootReducer = combineReducers({ student: studentReducer, @@ -16,6 +17,7 @@ const rootReducer = combineReducers({ plan: internshipPlanReducer, insurance: insuranceReducer, user: userReducer, + report: internshipReportReducer, }) export type AppState = ReturnType; diff --git a/src/state/reducer/report.ts b/src/state/reducer/report.ts new file mode 100644 index 0000000..81fb5ac --- /dev/null +++ b/src/state/reducer/report.ts @@ -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 | null; +} + +const defaultInternshipReportState: InternshipReportState = { + ...defaultDeanApprovalsState, + ...defaultSubmissionState, + report: null, +} + +export const getInternshipReport = ({ report }: InternshipReportState): Report | null => + report && reportSerializationTransformer.reverseTransform(report); + +const internshipReportSubmissionReducer: Reducer = 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;