From 4be1c4997583bdc8a02a6280e015d3f4c2a0060a Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 25 Jul 2020 16:18:47 +0200 Subject: [PATCH 01/11] Fix date formatting on language change --- package.json | 1 + src/app.tsx | 42 ++++++++++++++++++++++------------- src/i18n.ts | 18 +++++++++++---- src/pages/main.tsx | 6 ++--- src/state/actions/index.ts | 20 +++++++++++++++++ src/state/actions/settings.ts | 12 ++++++++++ src/state/reducer/index.ts | 4 ++++ src/state/reducer/settings.ts | 24 ++++++++++++++++++++ translations/en.yaml | 6 ++--- translations/pl.yaml | 6 ++--- yarn.lock | 5 +++++ 11 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 src/state/actions/settings.ts create mode 100644 src/state/reducer/settings.ts diff --git a/package.json b/package.json index f1d1f0f..93cb25f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-dev-utils": "^10.2.1", "react-dom": "^16.13.1", "react-i18next": "^11.7.0", + "react-moment": "^0.9.7", "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", "redux": "^4.0.5", diff --git a/src/app.tsx b/src/app.tsx index 0f42d36..3dc9cb6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,23 +1,27 @@ -import React, { Dispatch, HTMLProps, useEffect } from 'react'; +import React, { HTMLProps, useEffect } from 'react'; import { Link, Route, Switch } from "react-router-dom" -import moment from "moment"; import { route, routes } from "@/routing"; -import { useDispatch, useSelector } from "react-redux"; -import { AppState } from "@/state/reducer"; -import { StudentAction, StudentActions } from "@/state/actions/student"; +import { useSelector } from "react-redux"; +import { AppState, isReady } from "@/state/reducer"; +import { StudentActions } from "@/state/actions/student"; import { sampleStudent } from "@/provider/dummy/student"; import { Trans, useTranslation } from "react-i18next"; import { Student } from "@/data"; import '@/styles/overrides.scss' import '@/styles/header.scss' import classNames from "classnames"; -import { EditionAction, EditionActions } from "@/state/actions/edition"; +import { EditionActions } from "@/state/actions/edition"; import { sampleEdition } from "@/provider/dummy/edition"; import { Edition } from "@/data/edition"; +import { SettingActions } from "@/state/actions/settings"; +import { useDispatch } from "@/state/actions"; +import { getLocale, Locale } from "@/state/reducer/settings"; +import i18n from "@/i18n"; +import moment from "moment"; const UserMenu = (props: HTMLProps) => { const student = useSelector(state => state.student as Student); - const dispatch = useDispatch>(); + const dispatch = useDispatch(); const { t } = useTranslation(); const handleUserLogin = () => { @@ -47,17 +51,17 @@ const UserMenu = (props: HTMLProps) => { const LanguageSwitcher = ({ className, ...props }: HTMLProps) => { const { i18n } = useTranslation(); - const handleLanguageChange = (language: string) => () => { - i18n.changeLanguage(language); - document.documentElement.lang = language; - moment.locale(language) + const dispatch = useDispatch(); + + const handleLanguageChange = (language: Locale) => () => { + dispatch({ type: SettingActions.SetLocale, locale: language }) } const isActive = (language: string) => language.toLowerCase() === i18n.language.toLowerCase(); return
    { ['pl', 'en'].map(language =>
  • - { language } @@ -66,16 +70,24 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps) } function App() { - const dispatch = useDispatch>(); + const dispatch = useDispatch(); const edition = useSelector(state => state.edition); + const locale = useSelector(state => getLocale(state.settings)); + useEffect(() => { if (!edition) { dispatch({ type: EditionActions.Set, edition: sampleEdition }); } }) - const isReady = !!edition; + useEffect(() => { + i18n.changeLanguage(locale); + document.documentElement.lang = locale; + moment.locale(locale) + }) + + const ready = useSelector(isReady); return <>
    @@ -96,7 +108,7 @@ function App() {
    - { isReady && { routes.map(({ name, content, ...route }) => { content() }) } } + { ready && { routes.map(({ name, content, ...route }) => { content() }) } } ; } diff --git a/src/i18n.ts b/src/i18n.ts index 4f8c6a2..eb71721 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -2,10 +2,9 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector"; -import moment from "moment"; - import "moment/locale/pl" import "moment/locale/en-gb" +import moment, { isDuration, isMoment } from "moment"; const resources = { en: { @@ -21,9 +20,20 @@ i18n .use(initReactI18next) .init({ resources, - fallbackLng: "en", + fallbackLng: "pl", interpolation: { - escapeValue: false + escapeValue: false, + format: (value, format, lng) => { + if (isMoment(value)) { + return value.locale(lng || "pl").format(format || "DD MMM YYYY"); + } + + if (isDuration(value)) { + return value.locale(lng || "pl").humanize(); + } + + return value; + } } }) diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 0b773d9..c48e059 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -31,10 +31,10 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step { label } { until && - { t('until', { date: until.format("DD MMMM YYYY") }) } + { t('until', { date: until }) } { isLate && - { t('late', { by: moment.duration(now.diff(until)).humanize() }) } } - { !isLate && !completed && - { t('left', { left: left.humanize() }) } } + variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) } } + { !isLate && !completed && - { t('left', { left: left }) } } } diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index e69de29..9959cb5 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -0,0 +1,20 @@ +import { StudentAction, StudentActions } from "@/state/actions/student"; +import { EditionAction, EditionActions } from "@/state/actions/edition"; +import { SettingActions, SettingsAction } from "@/state/actions/settings"; +import { Dispatch } from "react"; + +import { useDispatch as useReduxDispatch } from "react-redux"; + +export * from "./base" +export * from "./edition" +export * from "./settings" +export * from "./student" + +export type Action = StudentAction | EditionAction | SettingsAction; + +export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions } +export type Actions = typeof Actions; + +export const useDispatch = () => useReduxDispatch>() + +export default Actions; diff --git a/src/state/actions/settings.ts b/src/state/actions/settings.ts new file mode 100644 index 0000000..d506565 --- /dev/null +++ b/src/state/actions/settings.ts @@ -0,0 +1,12 @@ +import { Action } from "@/state/actions/base"; +import { Locale } from "@/state/reducer/settings"; + +export enum SettingActions { + SetLocale = "SET_LOCALE", +} + +export interface SetLocaleAction extends Action { + locale: Locale +} + +export type SettingsAction = SetLocaleAction; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index c839818..5d22892 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -2,12 +2,16 @@ import { combineReducers } from "redux"; import studentReducer from "./student" import editionReducer from "@/state/reducer/edition"; +import settingsReducer from "@/state/reducer/settings"; const rootReducer = combineReducers({ student: studentReducer, edition: editionReducer, + settings: settingsReducer, }) export type AppState = ReturnType; export default rootReducer; + +export const isReady = (state: AppState) => !!state.edition; diff --git a/src/state/reducer/settings.ts b/src/state/reducer/settings.ts new file mode 100644 index 0000000..05757fa --- /dev/null +++ b/src/state/reducer/settings.ts @@ -0,0 +1,24 @@ +import { SettingActions, SettingsAction } from "@/state/actions/settings"; + +export type Locale = "pl" | "en" + +export type SettingsState = { + locale: Locale +} + +const defaultSettingsState: SettingsState = { + locale: "pl", +} + +const settingsReducer = (state: SettingsState = defaultSettingsState, action: SettingsAction): SettingsState => { + switch (action.type) { + case SettingActions.SetLocale: + return { ...state, locale: action.locale } + default: + return state; + } +} + +export default settingsReducer; + +export const getLocale = (state: SettingsState): Locale => state.locale; diff --git a/translations/en.yaml b/translations/en.yaml index ca1e2c6..6593964 100644 --- a/translations/en.yaml +++ b/translations/en.yaml @@ -3,9 +3,9 @@ login: login logout: logout logged-in-as: logged in as <1>{{ name }} -until: until {{ date }} -late: late by {{ by }} -left: '{{ left }} left' +until: until {{ date, DD MMMM YYYY }} +late: late by {{ by, humanize }} +left: '{{ left, humanize }} left' dropzone: "Drag and drop a file here or click to choose" diff --git a/translations/pl.yaml b/translations/pl.yaml index 8b9e395..3134031 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -3,9 +3,9 @@ login: zaloguj się logout: wyloguj się logged-in-as: zalogowany jako <1>{{ name }} -until: do {{ date }} -late: '{{ by }} spóźnienia' -left: jeszcze {{ left }} +until: do {{ date, DD MMMM YYYY }} +late: '{{ by, humanize }} spóźnienia' +left: jeszcze {{ left, humanize }} confirm: zatwierdź go-back: wstecz diff --git a/yarn.lock b/yarn.lock index bad0fdf..9b75961 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7408,6 +7408,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-moment@^0.9.7: + version "0.9.7" + resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-0.9.7.tgz#ca570466595b1aa4f7619e62da18b3bb2de8b6f3" + integrity sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g== + react-redux@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" -- 2.45.2 From b5717000c42146a6bc4d7793b3fd6230e0bdcb5c Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 28 Jul 2020 18:49:22 +0200 Subject: [PATCH 02/11] Add footer --- src/app.tsx | 15 ++++++++++++--- src/styles/footer.scss | 14 ++++++++++++++ src/styles/overrides.scss | 14 ++++++++++++++ translations/en.yaml | 2 ++ translations/pl.yaml | 2 ++ 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/styles/footer.scss diff --git a/src/app.tsx b/src/app.tsx index 3dc9cb6..cfdb347 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -9,6 +9,7 @@ import { Trans, useTranslation } from "react-i18next"; import { Student } from "@/data"; import '@/styles/overrides.scss' import '@/styles/header.scss' +import '@/styles/footer.scss' import classNames from "classnames"; import { EditionActions } from "@/state/actions/edition"; import { sampleEdition } from "@/provider/dummy/edition"; @@ -18,6 +19,7 @@ import { useDispatch } from "@/state/actions"; import { getLocale, Locale } from "@/state/reducer/settings"; import i18n from "@/i18n"; import moment from "moment"; +import { Container } from "@material-ui/core"; const UserMenu = (props: HTMLProps) => { const student = useSelector(state => state.student as Student); @@ -72,7 +74,7 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps) function App() { const dispatch = useDispatch(); const edition = useSelector(state => state.edition); - + const { t } = useTranslation(); const locale = useSelector(state => getLocale(state.settings)); useEffect(() => { @@ -85,7 +87,7 @@ function App() { i18n.changeLanguage(locale); document.documentElement.lang = locale; moment.locale(locale) - }) + }, [ locale ]) const ready = useSelector(isReady); @@ -108,7 +110,14 @@ function App() { - { ready && { routes.map(({ name, content, ...route }) => { content() }) } } +
    + { ready && { routes.map(({ name, content, ...route }) => { content() }) } } +
    +
    + +
    { t('copyright', { date: moment() }) }
    +
    +
    ; } diff --git a/src/styles/footer.scss b/src/styles/footer.scss new file mode 100644 index 0000000..943ddb0 --- /dev/null +++ b/src/styles/footer.scss @@ -0,0 +1,14 @@ +@import "variables"; + +.footer { + background: $main-dark; + margin-top: 3rem; + color: #e4f1fe; + padding: 1rem 0; + display: flex; + font-size: 0.8rem; +} + +.footer__copyright { + text-align: right; +} diff --git a/src/styles/overrides.scss b/src/styles/overrides.scss index 08cb80e..d250e0d 100644 --- a/src/styles/overrides.scss +++ b/src/styles/overrides.scss @@ -14,6 +14,7 @@ html, body { margin: 0; padding: 0; + min-height: 100%; font-family: "Roboto", "Helvetica", "Arial", sans-serif; } @@ -21,3 +22,16 @@ html, body { * { box-sizing: border-box; } + +#root { + display: flex; + flex-direction: column; +} + +#content { + flex: 1 1 auto; +} + +body, #root { + min-height: 100vh; +} diff --git a/translations/en.yaml b/translations/en.yaml index 6593964..902dbbd 100644 --- a/translations/en.yaml +++ b/translations/en.yaml @@ -1,4 +1,6 @@ --- +copyright: ETI © {{ date, YYYY }} + login: login logout: logout logged-in-as: logged in as <1>{{ name }} diff --git a/translations/pl.yaml b/translations/pl.yaml index 3134031..e2c6ab2 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -1,4 +1,6 @@ --- +copyright: Wydział ETI Politechniki Gdańskiej © {{ date, YYYY }} + login: zaloguj się logout: wyloguj się logged-in-as: zalogowany jako <1>{{ name }} -- 2.45.2 From a0b555d0e58429194306274b6e7197cce356a27d Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Thu, 30 Jul 2020 22:02:23 +0200 Subject: [PATCH 03/11] Add proposal reducer --- src/data/deanApproval.ts | 3 ++ src/provider/dummy/index.ts | 4 +++ src/state/actions/index.ts | 6 ++-- src/state/actions/proposal.ts | 37 ++++++++++++++++++++++++ src/state/reducer/index.ts | 4 ++- src/state/reducer/proposal.ts | 53 +++++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/data/deanApproval.ts create mode 100644 src/state/actions/proposal.ts create mode 100644 src/state/reducer/proposal.ts diff --git a/src/data/deanApproval.ts b/src/data/deanApproval.ts new file mode 100644 index 0000000..6231196 --- /dev/null +++ b/src/data/deanApproval.ts @@ -0,0 +1,3 @@ +export type DeanApproval = { + +} diff --git a/src/provider/dummy/index.ts b/src/provider/dummy/index.ts index 7466cc3..7e8f58d 100644 --- a/src/provider/dummy/index.ts +++ b/src/provider/dummy/index.ts @@ -1 +1,5 @@ export * from './company' +export * from './edition' +export * from './student' +export * from './internship' +export * from './helpers' diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 9959cb5..39de422 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -1,6 +1,7 @@ import { StudentAction, StudentActions } from "@/state/actions/student"; import { EditionAction, EditionActions } from "@/state/actions/edition"; import { SettingActions, SettingsAction } from "@/state/actions/settings"; +import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions/proposal"; import { Dispatch } from "react"; import { useDispatch as useReduxDispatch } from "react-redux"; @@ -9,10 +10,11 @@ export * from "./base" export * from "./edition" export * from "./settings" export * from "./student" +export * from "./proposal" -export type Action = StudentAction | EditionAction | SettingsAction; +export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction; -export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions } +export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions } export type Actions = typeof Actions; export const useDispatch = () => useReduxDispatch>() diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts new file mode 100644 index 0000000..a7876c4 --- /dev/null +++ b/src/state/actions/proposal.ts @@ -0,0 +1,37 @@ +import { Action } from "@/state/actions/base"; +import { Internship } from "@/data"; + +export enum InternshipProposalActions { + Send = "SEND_PROPOSAL", + Save = "SAVE_PROPOSAL", + Approve = "RECEIVE_PROPOSAL_APPROVE", + Decline = "RECEIVE_PROPOSAL_DECLINE", + Receive = "RECEIVE_PROPOSAL_STATE", +} + +export interface SendProposalAction extends Action { + internship: Internship; +} + +export interface ReceiveProposalApproveAction extends Action { + +} + +export interface ReceiveProposalDeclineAction extends Action { + +} + +export interface ReceiveProposalUpdateAction extends Action { + +} + +export interface SaveProposalAction extends Action { + internship: Internship; +} + +export type InternshipProposalAction + = SendProposalAction + | SaveProposalAction + | ReceiveProposalApproveAction + | ReceiveProposalDeclineAction + | ReceiveProposalUpdateAction; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 5d22892..3b9c780 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -1,13 +1,15 @@ import { combineReducers } from "redux"; -import studentReducer from "./student" +import studentReducer from "@/state/reducer/student" import editionReducer from "@/state/reducer/edition"; import settingsReducer from "@/state/reducer/settings"; +import internshipProposalReducer from "@/state/reducer/proposal"; const rootReducer = combineReducers({ student: studentReducer, edition: editionReducer, settings: settingsReducer, + proposal: internshipProposalReducer, }) export type AppState = ReturnType; diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts new file mode 100644 index 0000000..c779e70 --- /dev/null +++ b/src/state/reducer/proposal.ts @@ -0,0 +1,53 @@ +import { DeanApproval } from "@/data/deanApproval"; +import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions"; +import { Internship } from "@/data"; + +export type ProposalStatus = "draft" | "awaiting" | "accepted" | "declined"; + +export type InternshipProposalState = { + accepted: boolean; + sent: boolean; + declined: boolean; + requiredDeanApprovals: DeanApproval[]; + proposal: Internship | null; + comment: string | null; +} + +const defaultInternshipProposalState: InternshipProposalState = { + accepted: false, + declined: false, + proposal: null, + requiredDeanApprovals: [], + sent: false, + comment: null +} + +const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): ProposalStatus => { + 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 })); + } +} + +const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => { + switch (action.type) { + case InternshipProposalActions.Save: + case InternshipProposalActions.Send: + return { + ...state, + proposal: action.internship + } + default: + return state; + } +} + +export default internshipProposalReducer; -- 2.45.2 From 2f7bbb0caab6fa118f25ec5660d4d463e7b69b8a Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 2 Aug 2020 16:00:12 +0200 Subject: [PATCH 04/11] Save proposal form in state --- package.json | 1 + src/components/index.tsx | 2 + src/components/step.tsx | 46 +++++++++++ src/data/internship.ts | 5 +- src/forms/company.tsx | 39 ++++----- src/forms/helpers.ts | 2 +- src/forms/{Internship.tsx => internship.tsx} | 73 +++++++++-------- src/helpers.ts | 5 ++ src/hooks/index.ts | 1 + src/hooks/useProxyState.ts | 9 +++ src/pages/index.ts | 1 - src/pages/internship/proposal.tsx | 2 +- src/pages/main.tsx | 84 ++++++++++++-------- src/serialization/index.ts | 3 + src/serialization/internship.ts | 17 ++++ src/serialization/moment.ts | 7 ++ src/serialization/types.ts | 18 +++++ src/state/actions/proposal.ts | 2 +- src/state/reducer/proposal.ts | 38 ++++++++- src/state/store.ts | 4 +- translations/pl.yaml | 7 ++ webpack.config.js | 1 + yarn.lock | 5 ++ 23 files changed, 274 insertions(+), 98 deletions(-) create mode 100644 src/components/index.tsx create mode 100644 src/components/step.tsx rename src/forms/{Internship.tsx => internship.tsx} (71%) create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useProxyState.ts create mode 100644 src/serialization/index.ts create mode 100644 src/serialization/internship.ts create mode 100644 src/serialization/moment.ts create mode 100644 src/serialization/types.ts diff --git a/package.json b/package.json index 93cb25f..1379ef1 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "i18next": "^19.6.0", "i18next-browser-languagedetector": "^5.0.0", "material-ui-dropzone": "^3.3.0", + "mdi-material-ui": "^6.17.0", "moment": "^2.26.0", "node-sass": "^4.14.1", "optimize-css-assets-webpack-plugin": "5.0.3", diff --git a/src/components/index.tsx b/src/components/index.tsx new file mode 100644 index 0000000..9feeb59 --- /dev/null +++ b/src/components/index.tsx @@ -0,0 +1,2 @@ +export * from "./actions" +export * from "./step" diff --git a/src/components/step.tsx b/src/components/step.tsx new file mode 100644 index 0000000..0cf9cd6 --- /dev/null +++ b/src/components/step.tsx @@ -0,0 +1,46 @@ +import moment, { Moment } from "moment"; +import { Box, Step as StepperStep, StepContent, StepLabel, StepProps as StepperStepProps, Typography } from "@material-ui/core"; +import { useTranslation } from "react-i18next"; +import React, { ReactChild, useMemo } from "react"; + +type StepProps = StepperStepProps & { + until?: Moment; + completedOn?: Moment; + label: string; + state?: ReactChild | null; + + /** this roughly translates into completed */ + accepted?: boolean; + + /** this roughly translates into error */ + declined?: boolean; + sent?: boolean; +} + +const now = moment(); + +export const Step = ({ until, label, completedOn, children, accepted = false, declined = false, completed = false, state = null, ...props }: StepProps) => { + const { t } = useTranslation(); + + const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]); + const left = useMemo(() => moment.duration(now.diff(until)), [until]); + + return + + { label } + { until && + { state && <> + { state } + + } + + { 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/internship.ts b/src/data/internship.ts index 4961e27..bc82578 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -1,7 +1,7 @@ import { Moment } from "moment"; import { Identifiable } from "./common"; import { Student } from "@/data/student"; -import { Company } from "@/data/company"; +import { BranchOffice, Company } from "@/data/company"; export enum InternshipType { FreeInternship = "FreeInternship", @@ -61,8 +61,10 @@ export interface Internship extends Identifiable { endDate: Moment; isAccepted: boolean; lengthInWeeks: number; + hours: number; mentor: Mentor; company: Company; + office: BranchOffice; } export interface Mentor { @@ -71,3 +73,4 @@ export interface Mentor { email: string; phone: string | null; } + diff --git a/src/forms/company.tsx b/src/forms/company.tsx index c3fd10f..a728935 100644 --- a/src/forms/company.tsx +++ b/src/forms/company.tsx @@ -1,19 +1,19 @@ -import React, { HTMLProps, useEffect, useMemo, useState } from "react"; -import { BranchOffice, Company, Course, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data"; +import React, { HTMLProps, useMemo } from "react"; +import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data"; import { sampleCompanies } from "@/provider/dummy"; -import { Alert, Autocomplete } from "@material-ui/lab"; -import { Button, Grid, TextField, Typography } from "@material-ui/core"; +import { Autocomplete } from "@material-ui/lab"; +import { Grid, TextField, Typography } from "@material-ui/core"; import { BoundProperty, formFieldProps } from "./helpers"; -import { InternshipFormSectionProps } from "@/forms/Internship"; -import { sampleCourse } from "@/provider/dummy/student"; +import { InternshipFormSectionProps } from "@/forms/internship"; import { emptyMentor } from "@/provider/dummy/internship"; +import { useProxyState } from "@/hooks"; export type CompanyFormProps = {} & InternshipFormSectionProps; export type BranchOfficeProps = { - company: Company, - disabled?: boolean -} + disabled?: boolean; + offices?: BranchOffice[]; +} & BoundProperty export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps) => (
    @@ -29,11 +29,8 @@ export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTML
    ) -export const BranchForm: React.FC = ({ company, disabled = false }) => { - const [office, setOffice] = useState(emptyBranchOffice) - +export const BranchForm: React.FC = ({ value: office, onChange: setOffice, offices = [], disabled = false }) => { const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]); - const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address })) const handleCityChange = (event: any, value: BranchOffice | string | null) => { @@ -63,13 +60,11 @@ export const BranchForm: React.FC = ({ company, disabled = fa }) } - useEffect(() => void (office.id && setOffice(emptyBranchOffice)), [company]) - return (
    - typeof office == "string" ? office : office.address.city } renderOption={ office => } @@ -122,14 +117,12 @@ export const MentorForm = ({ mentor, onMentorChange }: BoundProperty = ({ internship, onChange }) => { - const [company, setCompany] = useState(emptyCompany); - const [mentor, setMentor] = useState(emptyMentor); + const [company, setCompany] = useProxyState(internship.company || emptyCompany, company => onChange({ ...internship, company })); + const [mentor, setMentor] = useProxyState(internship.mentor || emptyMentor, mentor => onChange({ ...internship, mentor })); + const [office, setOffice] = useProxyState(internship.office || emptyBranchOffice, office => onChange({ ...internship, office })); const canEdit = useMemo(() => !company.id, [company.id]); - useEffect(() => onChange({ ...internship, mentor }), [ mentor ]); - useEffect(() => onChange({ ...internship, company }), [ company ]); - const fieldProps = formFieldProps(company, setCompany) const handleCompanyChange = (event: any, value: Company | string | null) => { @@ -153,7 +146,7 @@ export const CompanyForm: React.FunctionComponent = ({ interns getOptionLabel={ option => option.name } renderOption={ company => } renderInput={ props => } - onChange={ handleCompanyChange } + onChange={ handleCompanyChange } value={ company } freeSolo /> @@ -167,7 +160,7 @@ export const CompanyForm: React.FunctionComponent = ({ interns Zakładowy opiekun praktyki Oddział - + ) } diff --git a/src/forms/helpers.ts b/src/forms/helpers.ts index 96fdbde..5eabad7 100644 --- a/src/forms/helpers.ts +++ b/src/forms/helpers.ts @@ -15,7 +15,7 @@ export function formFieldProps(subject: T, update: (value: T) => void, option return

    ( field: P, extractor: (...args: TArgs) => T[P] = ((event: DOMEvent) => event.target.value as unknown as T[P]) as any - ) => ({ + ): any => ({ [property]: subject[field], [event]: (...args: TArgs) => update({ ...subject, diff --git a/src/forms/Internship.tsx b/src/forms/internship.tsx similarity index 71% rename from src/forms/Internship.tsx rename to src/forms/internship.tsx index 967b1b6..abd58f4 100644 --- a/src/forms/Internship.tsx +++ b/src/forms/internship.tsx @@ -1,18 +1,5 @@ -import React, { HTMLProps, useMemo, useState } from "react"; -import { - FormControl, - Grid, - Input, - InputLabel, - Typography, - FormHelperText, - TextField, - FormGroup, - FormControlLabel, - Checkbox, - FormLabel, - Button -} from "@material-ui/core"; +import React, { HTMLProps, useEffect, useMemo, useState } from "react"; +import { Button, FormControl, FormHelperText, Grid, Input, InputLabel, TextField, Typography } from "@material-ui/core"; import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; import { CompanyForm } from "@/forms/company"; import { StudentForm } from "@/forms/student"; @@ -24,6 +11,14 @@ import { computeWorkingHours } from "@/utils/date"; import { Autocomplete } from "@material-ui/lab"; import { formFieldProps } from "@/forms/helpers"; import { emptyInternship } from "@/provider/dummy/internship"; +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 { route } from "@/routing"; +import { useProxyState } from "@/hooks"; +import { getInternshipProposal } from "@/state/reducer/proposal"; export type InternshipFormProps = {} @@ -62,25 +57,26 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr { internship.type === InternshipType.Other && } - - - Realizowane punkty programu praktyk (minimum 3) - { course.possibleProgramEntries.map(entry => { - return ( - } - /> - ) - }) } - - + {/**/} + {/* */} + {/* Realizowane punkty programu praktyk (minimum 3)*/} + {/* { course.possibleProgramEntries.map(entry => {*/} + {/* return (*/} + {/* }*/} + {/* />*/} + {/* )*/} + {/* }) }*/} + {/* */} + {/**/} ) } -const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => { - const [startDate, setStartDate] = useState(internship.startDate); - const [endDate, setEndDate] = useState(internship.endDate); +const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionProps) => { + const [startDate, setStartDate] = useProxyState(internship.startDate, value => onChange({ ...internship, startDate: value })); + const [endDate, setEndDate] = useProxyState(internship.endDate, value => onChange({ ...internship, endDate: value })); + const [overrideHours, setHoursOverride] = useState(null) const [workingHours, setWorkingHours] = useState(40) @@ -89,6 +85,8 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => { const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]); const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [ hours ]); + useEffect(() => onChange({ ...internship, hours }), [hours]) + return ( @@ -140,7 +138,18 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => { } export const InternshipForm: React.FunctionComponent = props => { - const [internship, setInternship] = useState>({ ...emptyInternship, intern: sampleStudent }) + const initialInternshipState = useSelector>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, intern: sampleStudent }); + + const [internship, setInternship] = useState>(initialInternshipState) + const { t } = useTranslation(); + + const dispatch = useDispatch(); + const history = useHistory(); + + const handleSubmit = () => { + dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship }); + history.push(route("home")) + } return (

    @@ -152,7 +161,7 @@ export const InternshipForm: React.FunctionComponent = prop Miejsce odbywania praktyki - +
    ) } diff --git a/src/helpers.ts b/src/helpers.ts index e128e6c..b24217f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,10 @@ export type Nullable = { [P in keyof T]: T[P] | null } +export type Partial = { [K in keyof T]?: T[K] } +export type Dictionary = { [key: string]: T }; + +export type Index = string | symbol | number; + export interface DOMEvent extends Event { target: TTarget; } diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..283d0e1 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useProxyState" diff --git a/src/hooks/useProxyState.ts b/src/hooks/useProxyState.ts new file mode 100644 index 0000000..3dd51fa --- /dev/null +++ b/src/hooks/useProxyState.ts @@ -0,0 +1,9 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; + +export function useProxyState(initial: T, setter: (value: T) => void): [T, Dispatch>] { + const [value, proxy] = useState(initial); + + useEffect(() => setter(value), [ value ]); + + return [value, proxy]; +} diff --git a/src/pages/index.ts b/src/pages/index.ts index 538a1bd..c5f71a3 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,4 +1,3 @@ export * from "./internship/proposal"; export * from "./errors/not-found" export * from "./main" -export { Actions } from "@/components/actions"; diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index 00e233c..a9ef954 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -2,7 +2,7 @@ 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 { InternshipForm } from "@/forms/Internship"; +import { InternshipForm } from "@/forms/internship"; import React from "react"; export const InternshipProposalPage = () => { diff --git a/src/pages/main.tsx b/src/pages/main.tsx index c48e059..46a1eaa 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,45 +1,69 @@ import React, { useMemo } from "react"; import { Page } from "@/pages/base"; -import { Box, Button, Container, Step as StepperStep, StepContent, StepLabel, Stepper, StepProps as StepperStepProps, Typography } from "@material-ui/core"; +import { Button, Container, Stepper, StepProps, Theme, Typography } from "@material-ui/core"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; -import moment, { Moment } from "moment"; 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 } from "@/components/actions"; +import { Actions, Step } from "@/components"; +import { getInternshipProposalStatus, InternshipProposalState, InternshipProposalStatus } from "@/state/reducer/proposal"; +import { createStyles, makeStyles } from "@material-ui/core/styles"; -type StepProps = StepperStepProps & { - until?: Moment; - completedOn?: Moment; - label: string; + +const getColorByStatus = (status: InternshipProposalStatus, 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 now = moment(); +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 => getInternshipProposalStatus(state.proposal)) + const classes = useStatusStyles({ status }); -const Step = ({ until, label, completedOn, children, completed, ...props }: StepProps) => { const { t } = useTranslation(); - const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]); - const left = useMemo(() => moment.duration(now.diff(until)), [until]); + return { t(`proposal.status.${status}`) }; +} - return - - { label } - { until && - - { t('until', { date: until }) } - { isLate && - { t('late', { by: moment.duration(now.diff(until)) }) } } - { !isLate && !completed && - { t('left', { left: left }) } } - - } - - { children && { children } } - +const ProposalStep = (props: StepProps) => { + const { t } = useTranslation(); + + const { sent } = useSelector(state => state.proposal); + const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + + return }> +

    { t('steps.internship-proposal.info') }

    + + +
    ; } export const MainPage = () => { @@ -67,13 +91,7 @@ export const MainPage = () => { } - -

    { t('steps.internship-proposal.info') }

    - - -
    +

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

    @@ -81,7 +99,7 @@ export const MainPage = () => { - diff --git a/src/serialization/index.ts b/src/serialization/index.ts new file mode 100644 index 0000000..e6efcb2 --- /dev/null +++ b/src/serialization/index.ts @@ -0,0 +1,3 @@ +export * from "./internship" +export * from "./moment" +export * from "./types" diff --git a/src/serialization/internship.ts b/src/serialization/internship.ts new file mode 100644 index 0000000..be27ddf --- /dev/null +++ b/src/serialization/internship.ts @@ -0,0 +1,17 @@ +import { Internship, InternshipType } from "@/data"; +import { Serializable, SerializationTransformer } from "@/serialization/types"; +import { momentSerializationTransformer } from "@/serialization/moment"; + +export const internshipSerializationTransformer: SerializationTransformer = { + transform: (internship: Internship): Serializable => ({ + ...internship, + startDate: momentSerializationTransformer.transform(internship.startDate), + endDate: momentSerializationTransformer.transform(internship.endDate), + }), + reverseTransform: (serialized: Serializable): Internship => ({ + ...serialized, + startDate: momentSerializationTransformer.reverseTransform(serialized.startDate), + endDate: momentSerializationTransformer.reverseTransform(serialized.endDate), + type: serialized.type as InternshipType, + }), +} diff --git a/src/serialization/moment.ts b/src/serialization/moment.ts new file mode 100644 index 0000000..19acae1 --- /dev/null +++ b/src/serialization/moment.ts @@ -0,0 +1,7 @@ +import { SerializationTransformer } from "@/serialization/types"; +import moment, { Moment } from "moment"; + +export const momentSerializationTransformer: SerializationTransformer = { + transform: (subject: Moment) => subject.toISOString(), + reverseTransform: (subject: string) => moment(subject), +} diff --git a/src/serialization/types.ts b/src/serialization/types.ts new file mode 100644 index 0000000..94e443d --- /dev/null +++ b/src/serialization/types.ts @@ -0,0 +1,18 @@ +import { Moment } from "moment"; + +type Simplify = string | + T extends string ? string : + T extends number ? number : + T extends boolean ? boolean : + T extends Moment ? string : + T extends Array ? Array> : + T extends (infer K)[] ? Simplify[] : + T extends Object ? Serializable : any; + +export type Serializable = { [K in keyof T]: Simplify } +export type Transformer = { + transform(subject: TFrom): TResult; + reverseTransform(subject: TResult): TFrom; +} + +export type SerializationTransformer> = Transformer diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index a7876c4..8189ec3 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -18,7 +18,7 @@ export interface ReceiveProposalApproveAction extends Action { - + comment: string; } export interface ReceiveProposalUpdateAction extends Action { diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts index c779e70..1ccb187 100644 --- a/src/state/reducer/proposal.ts +++ b/src/state/reducer/proposal.ts @@ -1,28 +1,33 @@ 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"; -export type ProposalStatus = "draft" | "awaiting" | "accepted" | "declined"; +export type InternshipProposalStatus = "draft" | "awaiting" | "accepted" | "declined"; export type InternshipProposalState = { accepted: boolean; sent: boolean; + sentOn: string | null; declined: boolean; requiredDeanApprovals: DeanApproval[]; - proposal: Internship | null; + proposal: Serializable | null; comment: string | null; } const defaultInternshipProposalState: InternshipProposalState = { accepted: false, declined: false, + sentOn: null, proposal: null, requiredDeanApprovals: [], sent: false, comment: null } -const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): ProposalStatus => { +export const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): InternshipProposalStatus => { switch (true) { case !sent: return "draft"; @@ -37,13 +42,38 @@ const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipPro } } +export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null => + proposal && internshipSerializationTransformer.reverseTransform(proposal); + const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => { switch (action.type) { + case InternshipProposalActions.Approve: + return { + ...state, + accepted: true, + declined: false, + 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: action.internship + proposal: internshipSerializationTransformer.transform(action.internship), + sent: true, + sentOn: momentSerializationTransformer.transform(moment()), + accepted: false, + declined: false, } default: return state; diff --git a/src/state/store.ts b/src/state/store.ts index f35ec6d..d3f3095 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -16,6 +16,8 @@ const store = createStore( devToolsEnhancer({}) ); -export const persistor = persistStore(store) +export const persistor = persistStore(store); + +(window as any)._store = store; export default store; diff --git a/translations/pl.yaml b/translations/pl.yaml index e2c6ab2..7fd75de 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -32,6 +32,13 @@ student: email: adres e-mail albumNumber: numer albumu +proposal: + status: + awaiting: "wysłano, oczekuje na weryfikacje" + accepted: "zaakceptowano" + declined: "do poprawy" + draft: "wersja robocza" + steps: personal-data: header: "Uzupełnienie informacji" diff --git a/webpack.config.js b/webpack.config.js index 6aeedda..dae0296 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -53,6 +53,7 @@ const config = { host: 'system-praktyk-front.localhost', disableHostCheck: true, historyApiFallback: true, + overlay: true, }, optimization: { usedExports: true diff --git a/yarn.lock b/yarn.lock index 9b75961..697aea8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5521,6 +5521,11 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +mdi-material-ui@^6.17.0: + version "6.17.0" + resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.17.0.tgz#da69f0b7d7c6fc2255e6007ed8b8ca858c1aede7" + integrity sha512-eOprRu31lklPIS1WGe3cM0G/8glKl1WKRvewxjDrgXH2Ryxxg7uQ+uwDUwUEONtLku0p2ZOLzgXUIy2uRy5rLg== + mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" -- 2.45.2 From 4aef2fd43573f327a843d9c9699dc949c37ea221 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 2 Aug 2020 23:00:18 +0200 Subject: [PATCH 05/11] Add support for different states of proposal --- .babelrc.js | 9 ++++ src/components/step.tsx | 14 +++--- src/components/stepIcon.tsx | 22 ++++++++++ src/pages/internship/proposal.tsx | 2 + src/pages/main.tsx | 72 +++++++++++++++++++++++++++---- src/provider/dummy/internship.ts | 4 +- src/state/actions/proposal.ts | 2 +- src/state/reducer/proposal.ts | 3 +- translations/pl.yaml | 20 ++++++++- 9 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 src/components/stepIcon.tsx diff --git a/.babelrc.js b/.babelrc.js index f605d9b..6e74520 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -17,6 +17,15 @@ const plugins = [ }, 'core' ], + [ + 'babel-plugin-import', + { + 'libraryName': 'mdi-material-ui', + 'libraryDirectory': '.', + 'camel2DashComponentName': false + }, + 'mdi-material-ui' + ], [ 'babel-plugin-import', { diff --git a/src/components/step.tsx b/src/components/step.tsx index 0cf9cd6..fa08903 100644 --- a/src/components/step.tsx +++ b/src/components/step.tsx @@ -2,6 +2,7 @@ import moment, { Moment } from "moment"; import { Box, Step as StepperStep, StepContent, StepLabel, StepProps as StepperStepProps, Typography } from "@material-ui/core"; import { useTranslation } from "react-i18next"; import React, { ReactChild, useMemo } from "react"; +import { StepIcon } from "@/components/stepIcon"; type StepProps = StepperStepProps & { until?: Moment; @@ -9,24 +10,21 @@ type StepProps = StepperStepProps & { label: string; state?: ReactChild | null; - /** this roughly translates into completed */ - accepted?: boolean; - - /** this roughly translates into error */ + waiting?: boolean; declined?: boolean; - sent?: boolean; } const now = moment(); -export const Step = ({ until, label, completedOn, children, accepted = false, declined = false, completed = false, state = null, ...props }: StepProps) => { +export const Step = (props: StepProps) => { + const { until, label, completedOn, children, completed = false, declined = false, waiting = false, state = null, ...rest } = props; const { t } = useTranslation(); const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]); const left = useMemo(() => moment.duration(now.diff(until)), [until]); - return - + return + { label } { until && { state && <> diff --git a/src/components/stepIcon.tsx b/src/components/stepIcon.tsx new file mode 100644 index 0000000..c8b9f8b --- /dev/null +++ b/src/components/stepIcon.tsx @@ -0,0 +1,22 @@ +import { StepIcon as MuiStepIcon, StepIconProps as MuiStepIconProps, Theme } from "@material-ui/core"; +import React from "react"; +import { TimerSand } from "mdi-material-ui"; +import { createStyles, makeStyles } from "@material-ui/core/styles"; + +type StepIconProps = MuiStepIconProps & { + waiting: boolean +} + +const useStyles = makeStyles((theme: Theme) => createStyles({ + root: { + color: theme.palette.primary.main + } +})) + +export const StepIcon = ({ waiting, ...props }: StepIconProps) => { + const classes = useStyles(); + + return waiting + ? + : ; +} diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index a9ef954..e45b4cb 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -4,6 +4,7 @@ import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { InternshipForm } from "@/forms/internship"; import React from "react"; +import { ProposalComment } from "@/pages"; export const InternshipProposalPage = () => { return @@ -15,6 +16,7 @@ export const InternshipProposalPage = () => { Zgłoszenie praktyki + diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 46a1eaa..97f1016 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from "react"; +import React, { HTMLProps, useMemo } from "react"; import { Page } from "@/pages/base"; -import { Button, Container, Stepper, StepProps, Theme, Typography } from "@material-ui/core"; +import { Box, Button, ButtonProps, Container, Stepper, StepProps, Theme, Typography } from "@material-ui/core"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; @@ -12,6 +12,8 @@ import { Description as DescriptionIcon } from "@material-ui/icons" import { Actions, Step } from "@/components"; import { getInternshipProposalStatus, InternshipProposalState, InternshipProposalStatus } 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"; const getColorByStatus = (status: InternshipProposalStatus, theme: Theme) => { @@ -51,18 +53,70 @@ const ProposalStatus = () => { return { t(`proposal.status.${status}`) }; } +const ProposalActions = () => { + const status = useSelector(state => getInternshipProposalStatus(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 + { t('comments') } + { comment } + +} + const ProposalStep = (props: StepProps) => { const { t } = useTranslation(); - const { sent } = useSelector(state => state.proposal); + const { sent, comment, declined } = useSelector(state => state.proposal); + const status = useSelector(state => getInternshipProposalStatus(state.proposal)); const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point - return }> -

    { t('steps.internship-proposal.info') }

    - - + return }> +

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

    + { comment && } +
    ; } diff --git a/src/provider/dummy/internship.ts b/src/provider/dummy/internship.ts index 9d86cc1..9f10d5b 100644 --- a/src/provider/dummy/internship.ts +++ b/src/provider/dummy/internship.ts @@ -1,5 +1,5 @@ import { Nullable } from "@/helpers"; -import { emptyCompany, Internship, Mentor } from "@/data"; +import { emptyBranchOffice, emptyCompany, Internship, Mentor } from "@/data"; export const emptyMentor: Mentor = { phone: null, @@ -18,4 +18,6 @@ export const emptyInternship: Nullable = { lengthInWeeks: 0, mentor: emptyMentor, company: emptyCompany, + hours: 0, + office: emptyBranchOffice, } diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index 8189ec3..9b13c85 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -14,7 +14,7 @@ export interface SendProposalAction extends Action { - + comment: string | null; } export interface ReceiveProposalDeclineAction extends Action { diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts index 1ccb187..890f543 100644 --- a/src/state/reducer/proposal.ts +++ b/src/state/reducer/proposal.ts @@ -52,7 +52,7 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter ...state, accepted: true, declined: false, - comment: "" + comment: action.comment, } case InternshipProposalActions.Decline: return { @@ -74,6 +74,7 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter sentOn: momentSerializationTransformer.transform(moment()), accepted: false, declined: false, + comment: null, } default: return state; diff --git a/translations/pl.yaml b/translations/pl.yaml index 7fd75de..e1564df 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -12,6 +12,12 @@ left: jeszcze {{ left, humanize }} confirm: zatwierdź go-back: wstecz +make-changes: wprowadź zmiany +review: podgląd +fix-errors: popraw uwagi +contact: skontaktuj się z pełnomocnikiem +comments: Zgłoszone uwagi + dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać" sections: @@ -48,9 +54,19 @@ steps: form: "Uzupełnij dane" internship-proposal: header: "Zgłoszenie praktyki" - info: > - Przed podjęciem praktyki należy ją zgłosić. + info: + draft: > + Przed podjęciem praktyki należy ją zgłosić. + awaiting: > + Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o + akceptacji bądź konieczności wprowadzenia zmian. + accepted: > + Twoje zgłoszenie zostało zweryfikowane i zaakceptowane. + declined: > + Twoje zgłoszenie zostało zweryfikowane i odrzucone. Popraw zgłoszone uwagi i wyślij zgłoszenie ponownie. W razie + pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku. form: "Formularz zgłaszania praktyki" + action: "zgłoś praktykę" plan: header: "Indywidualny Program Praktyki" info: "" -- 2.45.2 From 9b64926675ce1fa08a0a0979b20359fc69f63861 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 3 Aug 2020 20:05:51 +0200 Subject: [PATCH 06/11] Refactor submission state management so it is reusable --- src/data/internship.ts | 4 ++ src/forms/internship.tsx | 11 ++++- src/pages/main.tsx | 15 ++++--- src/serialization/moment.ts | 6 +-- src/state/actions/proposal.ts | 18 +++++--- src/state/actions/submission.ts | 26 +++++++++++ src/state/reducer/proposal.ts | 79 ++++++++++---------------------- src/state/reducer/submission.ts | 80 +++++++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 74 deletions(-) create mode 100644 src/state/actions/submission.ts create mode 100644 src/state/reducer/submission.ts diff --git a/src/data/internship.ts b/src/data/internship.ts index bc82578..ace8c57 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -67,6 +67,10 @@ export interface Internship extends Identifiable { office: BranchOffice; } +export interface Plan extends Identifiable { + +} + export interface Mentor { name: string; surname: string; diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index abd58f4..01b8689 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -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 = prop Miejsce odbywania praktyki - + + + + +
    ) } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 97f1016..21df4f3 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -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(state => getInternshipProposalStatus(state.proposal)) + const status = useSelector(state => getSubmissionStatus(state.proposal)) const classes = useStatusStyles({ status }); const { t } = useTranslation(); @@ -54,7 +55,7 @@ const ProposalStatus = () => { } const ProposalActions = () => { - const status = useSelector(state => getInternshipProposalStatus(state.proposal)); + const status = useSelector(state => getSubmissionStatus(state.proposal)); const { t } = useTranslation(); const ReviewAction = (props: ButtonProps) => @@ -96,17 +97,17 @@ export const ProposalComment = (props: HTMLProps) => { const { comment, declined } = useSelector(state => state.proposal); const { t } = useTranslation(); - return + 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 => getInternshipProposalStatus(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 = { - transform: (subject: Moment) => subject.toISOString(), - reverseTransform: (subject: string) => moment(subject), +export const momentSerializationTransformer: SerializationTransformer = { + transform: (subject: Moment) => subject && subject.toISOString(), + reverseTransform: (subject: string) => subject ? moment(subject) : null, } diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index 9b13c85..20f4af6 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -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 { +export interface SendProposalAction extends SendSubmissionAction { internship: Internship; } -export interface ReceiveProposalApproveAction extends Action { +export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction { comment: string | null; } -export interface ReceiveProposalDeclineAction extends Action { +export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction { comment: string; } -export interface ReceiveProposalUpdateAction extends Action { +export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction { } -export interface SaveProposalAction extends Action { +export interface SaveProposalAction extends SaveSubmissionAction { internship: Internship; } diff --git a/src/state/actions/submission.ts b/src/state/actions/submission.ts new file mode 100644 index 0000000..e8338e4 --- /dev/null +++ b/src/state/actions/submission.ts @@ -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 extends Action { +} + +export interface ReceiveSubmissionApproveAction extends Action { + comment: string | null; +} + +export interface ReceiveSubmissionDeclineAction extends Action { + comment: string; +} + +export interface ReceiveSubmissionUpdateAction extends Action { +} + +export interface SaveSubmissionAction extends Action { +} diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts index 890f543..783983c 100644 --- a/src/state/reducer/proposal.ts +++ b/src/state/reducer/proposal.ts @@ -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 | 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 = 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; diff --git a/src/state/reducer/submission.ts b/src/state/reducer/submission.ts new file mode 100644 index 0000000..864cf2f --- /dev/null +++ b/src/state/reducer/submission.ts @@ -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(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).comment, + } + case SubmissionAction.Decline: + return { + ...state, + accepted: false, + declined: true, + comment: (action as ReceiveSubmissionDeclineAction).comment, + } + case SubmissionAction.Send: + return { + ...state, + sent: true, + sentOn: momentSerializationTransformer.transform(moment()), + accepted: false, + declined: false, + comment: null, + } + default: + return state; + } + } +} -- 2.45.2 From 4285fb60cb3d6cd127cdde52de5a1162767cd49f Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 4 Aug 2020 20:20:58 +0200 Subject: [PATCH 07/11] 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: -- 2.45.2 From 0e1ef4411a2c159a950257d4682611b34198ed00 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Wed, 5 Aug 2020 22:17:22 +0200 Subject: [PATCH 08/11] Add IPP descriptions and confirms --- .build/deploy.sh | 1 + src/forms/internship.tsx | 78 +++++++++++++++++++++++++++++----------- src/pages/steps/plan.tsx | 25 +++++++------ translations/pl.yaml | 24 ++++++++----- 4 files changed, 89 insertions(+), 39 deletions(-) diff --git a/.build/deploy.sh b/.build/deploy.sh index 36f34bb..89c7bba 100755 --- a/.build/deploy.sh +++ b/.build/deploy.sh @@ -2,5 +2,6 @@ BUILD_PATH=$1 DEPLOY_PATH=$2 # copy all dist files to deploy path +rsync -avz $BUILD_PATH/public/* $DEPLOY_PATH rsync -avz $BUILD_PATH/build/* $DEPLOY_PATH diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 01b8689..752bf28 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -1,5 +1,18 @@ import React, { HTMLProps, useEffect, useMemo, useState } from "react"; -import { Button, FormControl, FormHelperText, Grid, Input, InputLabel, TextField, Typography } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + FormControl, + FormHelperText, + Grid, + Input, + InputLabel, + TextField, + Typography +} from "@material-ui/core"; import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; import { CompanyForm } from "@/forms/company"; import { StudentForm } from "@/forms/student"; @@ -46,30 +59,30 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr return ( - + } getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label } - renderOption={ (option: InternshipType) => } + renderOption={ (option: InternshipType) => } options={ Object.values(InternshipType) as InternshipType[] } disableClearable { ...fieldProps("type", (event, value) => value) as any } /> - - { internship.type === InternshipType.Other && } + + { internship.type === InternshipType.Other && } - {/**/} - {/* */} - {/* Realizowane punkty programu praktyk (minimum 3)*/} - {/* { course.possibleProgramEntries.map(entry => {*/} - {/* return (*/} - {/* }*/} - {/* />*/} - {/* )*/} - {/* }) }*/} - {/* */} - {/**/} + {/**/ } + {/* */ } + {/* Realizowane punkty programu praktyk (minimum 3)*/ } + {/* { course.possibleProgramEntries.map(entry => {*/ } + {/* return (*/ } + {/* }*/ } + {/* />*/ } + {/* )*/ } + {/* }) }*/ } + {/* */ } + {/**/ } ) } @@ -84,7 +97,7 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP const computedHours = useMemo(() => startDate && endDate && computeWorkingHours(startDate, endDate, workingHours / 5), [startDate, endDate, workingHours]); const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]); - const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [ hours ]); + const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [hours]); useEffect(() => onChange({ ...internship, hours }), [hours]) @@ -139,7 +152,10 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP } export const InternshipForm: React.FunctionComponent = props => { - const initialInternshipState = useSelector>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, intern: sampleStudent }); + const initialInternshipState = useSelector>(state => getInternshipProposal(state.proposal) || { + ...emptyInternship, + intern: sampleStudent + }); const [internship, setInternship] = useState>(initialInternshipState) const { t } = useTranslation(); @@ -147,11 +163,23 @@ export const InternshipForm: React.FunctionComponent = prop const dispatch = useDispatch(); const history = useHistory(); + const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); + const handleSubmit = () => { + setConfirmDialogOpen(false); + dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship }); history.push(route("home")) } + const handleSubmitConfirmation = () => { + setConfirmDialogOpen(true); + } + + const handleCancel = () => { + setConfirmDialogOpen(false); + } + return (
    Dane osoby odbywającej praktykę @@ -163,12 +191,22 @@ export const InternshipForm: React.FunctionComponent = prop Miejsce odbywania praktyki - + + + + + { t('forms.internship.send-confirmation') } + + + + + +
    ) } diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index 197c0cb..e92169d 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -3,7 +3,7 @@ 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 { CommentQuestion, FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index"; import { route } from "@/routing"; import { Link as RouterLink } from "react-router-dom"; import { Actions, Step } from "@/components"; @@ -18,11 +18,16 @@ const PlanActions = () => { const { t } = useTranslation(); const ReviewAction = (props: ButtonProps) => - + const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) => - + + const TemplateAction = (props: ButtonProps) => + const ContactAction = (props: ButtonProps) => @@ -31,7 +36,7 @@ const PlanActions = () => { switch (status) { case "awaiting": return - + case "accepted": return @@ -41,16 +46,14 @@ const PlanActions = () => { case "declined": return { t('fix-errors') } + + case "draft": return - - + + default: diff --git a/translations/pl.yaml b/translations/pl.yaml index 6c9bfc3..07be4d1 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -18,6 +18,7 @@ fix-errors: popraw uwagi contact: skontaktuj się z pełnomocnikiem comments: Zgłoszone uwagi send-again: wyślij ponownie +cancel: anuluj dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać" @@ -26,9 +27,13 @@ sections: header: "Moja praktyka" forms: - plan: + internship: + send-confirmation: > + Po wysłaniu zgłoszenia nie będzie możliwości jego zmiany do czasu zweryfikowania go przez pełnomocnika ds. Twojego + kierunku. Czy na pewno chcesz wysłać zgłoszenie praktyki w tej formie? + program: instructions: > - Wypełnij i zeskanuj Indywidualny Plan Praktyk a następnie wyślij go z pomocą tego formularza. + Wypełnij i zeskanuj Indywidualny program Praktyk a następnie wyślij go z pomocą tego formularza. dropzone-help: Skan dokumentu w formacie PDF student: @@ -51,13 +56,13 @@ steps: header: "Uzupełnienie informacji" info: > Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz - problem z uzupełnieniem tych informacji - skontaktuj się z koordynatorem praktyk dla Twojego kierunku. + problem z uzupełnieniem tych informacji - skontaktuj się z pełnomocnikiem ds. praktyk dla Twojego kierunku. form: "Uzupełnij dane" internship-proposal: header: "Zgłoszenie praktyki" info: draft: > - Przed podjęciem praktyki należy ją zgłosić. + Przed podjęciem praktyki należy ją zgłosić. (TODO) awaiting: > Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian. @@ -74,13 +79,16 @@ steps: draft: > TODO awaiting: > - TODO + Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i + zatwierdzony. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian. accepted: > - TODO + Twój indywidualny program praktyki został zweryfikowany i zaakceptowany. declined: > - TODO + Twój indywidualny program praktyki został zweryfikowany i odrzucony. Popraw zgłoszone uwagi i wyślij nowy program. W + razie pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku. template: "Pobierz szablon" - submit: "Wyślij Indywidualny Plan Praktyki" + submit: "Wyślij Indywidualny Program Praktyki" + download: Twój indywidualny program praktyki report: header: "Raport z praktyki" grade: -- 2.45.2 From e7831cf1e5ea764eddf2b1294df46c830b42fbf9 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Wed, 5 Aug 2020 22:50:57 +0200 Subject: [PATCH 09/11] Add insurance step --- src/data/edition.ts | 1 + src/pages/main.tsx | 5 ++++- src/pages/steps/common.tsx | 9 ++++++++- src/pages/steps/insurance.tsx | 27 +++++++++++++++++++++++++++ src/pages/steps/plan.tsx | 7 ++----- src/pages/steps/proposal.tsx | 9 +++------ src/state/actions/index.ts | 5 +++-- src/state/actions/insurance.ts | 12 ++++++++++++ src/state/reducer/index.ts | 2 ++ src/state/reducer/insurance.ts | 26 ++++++++++++++++++++++++++ translations/pl.yaml | 2 ++ 11 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 src/pages/steps/insurance.tsx create mode 100644 src/state/actions/insurance.ts create mode 100644 src/state/reducer/insurance.ts diff --git a/src/data/edition.ts b/src/data/edition.ts index 8a29c94..7fc5438 100644 --- a/src/data/edition.ts +++ b/src/data/edition.ts @@ -11,6 +11,7 @@ export type Deadlines = { proposal?: Moment; personalPlan?: Moment; report?: Moment; + insurance?: Moment; } export function getEditionDeadlines(edition: Edition): Deadlines { diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 3ddcfef..5d2da88 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -11,12 +11,15 @@ import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; import { Step } from "@/components"; import { ProposalStep } from "@/pages/steps/proposal"; import { PlanStep } from "@/pages/steps/plan"; +import { InsuranceStep } from "@/pages/steps/insurance"; +import { InsuranceState } from "@/state/reducer/insurance"; export const MainPage = () => { const { t } = useTranslation(); const student = useSelector(state => state.student); const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + const insurance = useSelector(root => root.insurance); const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); @@ -39,7 +42,7 @@ export const MainPage = () => {
    - + { insurance.required && } diff --git a/src/pages/steps/common.tsx b/src/pages/steps/common.tsx index 261ba52..58fdb8e 100644 --- a/src/pages/steps/common.tsx +++ b/src/pages/steps/common.tsx @@ -1,8 +1,9 @@ import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission"; -import { Theme } from "@material-ui/core"; +import { Button, ButtonProps, Theme } from "@material-ui/core"; import { createStyles, makeStyles } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import React from "react"; +import { CommentQuestion } from "mdi-material-ui/index"; export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => { switch (status) { @@ -44,3 +45,9 @@ export const Status = ({ submission } : SubmissionStatusProps) => { return { t(`submission.status.${ status }`) }; } + +export const ContactAction = (props: ButtonProps) => { + const { t } = useTranslation(); + + return +} diff --git a/src/pages/steps/insurance.tsx b/src/pages/steps/insurance.tsx new file mode 100644 index 0000000..2245c53 --- /dev/null +++ b/src/pages/steps/insurance.tsx @@ -0,0 +1,27 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { InsuranceState } from "@/state/reducer/insurance"; +import { Actions, Step } from "@/components"; +import { useTranslation } from "react-i18next"; +import React from "react"; +import { Edition, getEditionDeadlines } from "@/data/edition"; +import { Moment } from "moment"; +import { ContactAction } from "@/pages/steps/common"; + +export const InsuranceStep = () => { + const insurance = useSelector(root => root.insurance); + const deadline = useSelector(state => getEditionDeadlines(state.edition as Edition).insurance); // edition cannot be null at this point + const { t } = useTranslation(); + + // we don't want to show this step unless it's required + if (!insurance.required) { + return null; + } + + return +

    { t(`steps.insurance.instructions`) }

    + + + +
    +} diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index e92169d..7a94c7f 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -3,14 +3,14 @@ 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, FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index"; +import { FileDownloadOutline, 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 { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; -import { Status } from "@/pages/steps/common"; +import { ContactAction, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; const PlanActions = () => { @@ -30,9 +30,6 @@ const PlanActions = () => { { t('steps.plan.template') } - const ContactAction = (props: ButtonProps) => - - switch (status) { case "awaiting": return diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx index 5565cdc..b6f6cd1 100644 --- a/src/pages/steps/proposal.tsx +++ b/src/pages/steps/proposal.tsx @@ -10,8 +10,8 @@ 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"; +import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index"; +import { ContactAction, Status } from "@/pages/steps/common"; const ProposalActions = () => { const status = useSelector(state => getSubmissionStatus(state.proposal)); @@ -26,9 +26,6 @@ const ProposalActions = () => { { children } - const ContactAction = (props: ButtonProps) => - - switch (status) { case "awaiting": return @@ -42,7 +39,7 @@ const ProposalActions = () => { case "declined": return { t('fix-errors') } - + case "draft": return diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index f5bcdfb..54ac996 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -6,6 +6,7 @@ import { Dispatch } from "react"; import { useDispatch as useReduxDispatch } from "react-redux"; import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions/plan"; +import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance"; export * from "./base" export * from "./edition" @@ -14,9 +15,9 @@ export * from "./student" export * from "./proposal" export * from "./plan" -export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction; +export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction | InsuranceAction; -export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions } +export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions, ...InsuranceActions } export type Actions = typeof Actions; export const useDispatch = () => useReduxDispatch>() diff --git a/src/state/actions/insurance.ts b/src/state/actions/insurance.ts new file mode 100644 index 0000000..721d7d6 --- /dev/null +++ b/src/state/actions/insurance.ts @@ -0,0 +1,12 @@ +import { Action } from "@/state/actions/base"; +import { InsuranceState } from "@/state/reducer/insurance"; + +export enum InsuranceActions { + Signed = "RECEIVE_INSURANCE_SIGN", + Update = "RECEIVE_INSURANCE_UPDATE", +} + +export type InsuranceSigned = Action; +export type InsuranceUpdate = Action & Partial; + +export type InsuranceAction = InsuranceSigned | InsuranceUpdate; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 3d139e2..ca5d238 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -5,6 +5,7 @@ import editionReducer from "@/state/reducer/edition"; import settingsReducer from "@/state/reducer/settings"; import internshipProposalReducer from "@/state/reducer/proposal"; import internshipPlanReducer from "@/state/reducer/plan"; +import { insuranceReducer } from "@/state/reducer/insurance"; const rootReducer = combineReducers({ student: studentReducer, @@ -12,6 +13,7 @@ const rootReducer = combineReducers({ settings: settingsReducer, proposal: internshipProposalReducer, plan: internshipPlanReducer, + insurance: insuranceReducer, }) export type AppState = ReturnType; diff --git a/src/state/reducer/insurance.ts b/src/state/reducer/insurance.ts new file mode 100644 index 0000000..d5aaa4f --- /dev/null +++ b/src/state/reducer/insurance.ts @@ -0,0 +1,26 @@ +import { Reducer } from "react"; +import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance"; + +export type InsuranceState = { + required: boolean; + signed: boolean; + /// other data? +} + +const initialInsuranceState: InsuranceState = { + required: false, + signed: false, +} + +export const insuranceReducer: Reducer = (state = initialInsuranceState, action) => { + const { type, ...payload } = action; + + switch (action.type) { + case InsuranceActions.Signed: + return { ...state, signed: true } + case InsuranceActions.Update: + return { ...state, ...payload } + default: + return state; + } +} diff --git a/translations/pl.yaml b/translations/pl.yaml index 07be4d1..2cfd0fa 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -95,5 +95,7 @@ steps: header: "Ocena z praktyki" insurance: header: "Ubezpieczenie NNW" + instructions: > + papierki do podpisania... contact-coordinator: "Skontaktuj się z koordynatorem" -- 2.45.2 From 5316630a29eb0d5bd5cc7da2db3649093bb1b091 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 9 Aug 2020 19:21:47 +0200 Subject: [PATCH 10/11] Add simple proposal previwe --- src/components/proposalPreview.tsx | 75 ++++++++++++++++++++++++++++++ src/forms/internship.tsx | 8 ++-- src/forms/plan.tsx | 2 +- src/i18n.ts | 5 ++ src/pages/index.ts | 4 -- src/pages/internship/proposal.tsx | 31 ++++++++++-- src/pages/steps/proposal.tsx | 6 ++- src/routing.tsx | 5 +- src/styles/page.scss | 4 ++ src/utils/numbers.ts | 32 +++++++++++++ translations/pl.yaml | 21 ++++++++- 11 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 src/components/proposalPreview.tsx create mode 100644 src/utils/numbers.ts diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx new file mode 100644 index 0000000..96a5d9b --- /dev/null +++ b/src/components/proposalPreview.tsx @@ -0,0 +1,75 @@ +import { Internship } from "@/data"; +import React from "react"; +import { Paper, PaperProps, Typography, TypographyProps } from "@material-ui/core"; +import { useTranslation } from "react-i18next"; +import { createStyles, makeStyles } from "@material-ui/core/styles"; +import classNames from "classnames"; + +export type ProposalPreviewProps = { + proposal: Internship; +} + +const Label = ({ children }: TypographyProps) => { + return { children } +} + +const useSectionStyles = makeStyles(theme => createStyles({ + root: { + padding: theme.spacing(2), + marginTop: theme.spacing(3) + } +})) + +const Section = ({ children, ...props }: PaperProps) => { + const classes = useSectionStyles(); + + return + { children } + +} + +export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => { + const { t } = useTranslation(); + + return
    + { proposal.intern.name } { proposal.intern.surname } + + { t('internship.intern.semester', { semester: proposal.intern.semester }) } + { ", " } + { t('internship.intern.album', { album: proposal.intern.albumNumber }) } + + +
    + + { proposal.type } +
    + +
    + + + { t('internship.date-range', { start: proposal.startDate, end: proposal.endDate }) } + + + { t('internship.duration', { duration: 0 })} + { ", " } + { t('internship.hours', { hours: proposal.hours })} + +
    + +
    + + + { proposal.company.name } + + + NIP: { proposal.company.nip } + +
    + +
    + + { t('internship.address.city', proposal.office.address) } + { t('internship.address.street', proposal.office.address) } +
    +
    +} diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 752bf28..fb5b9ca 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -182,13 +182,13 @@ export const InternshipForm: React.FunctionComponent = prop return (
    - Dane osoby odbywającej praktykę + { t('internship.intern-info') } - Rodzaj i program praktyki + { t('internship.kind' )} - Czas trwania praktyki + { t('internship.duration') } - Miejsce odbywania praktyki + { t('internship.place') } diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 24a8fa7..0c96203 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -32,7 +32,7 @@ export const PlanForm = () => { - + { t('forms.plan.dropzone-help') } diff --git a/src/i18n.ts b/src/i18n.ts index eb71721..c01bc57 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -5,6 +5,7 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector"; import "moment/locale/pl" import "moment/locale/en-gb" import moment, { isDuration, isMoment } from "moment"; +import { convertToRoman } from "@/utils/numbers"; const resources = { en: { @@ -24,6 +25,10 @@ i18n interpolation: { escapeValue: false, format: (value, format, lng) => { + if (typeof value === "number" && format == "roman") { + return convertToRoman(value); + } + if (isMoment(value)) { return value.locale(lng || "pl").format(format || "DD MMM YYYY"); } diff --git a/src/pages/index.ts b/src/pages/index.ts index bb1a4b5..c5f71a3 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,7 +1,3 @@ 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/proposal.tsx b/src/pages/internship/proposal.tsx index e45b4cb..09b8ca8 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -4,9 +4,15 @@ import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { InternshipForm } from "@/forms/internship"; import React from "react"; -import { ProposalComment } from "@/pages"; +import { ProposalComment } from "@/pages/steps/proposal"; +import { useTranslation } from "react-i18next"; +import { ProposalPreview } from "@/components/proposalPreview"; +import { useSelector } from "react-redux"; +import { Internship } from "@/data"; +import { AppState } from "@/state/reducer"; +import { internshipSerializationTransformer } from "@/serialization"; -export const InternshipProposalPage = () => { +export const InternshipProposalFormPage = () => { return @@ -22,4 +28,23 @@ export const InternshipProposalPage = () => { } -export default InternshipProposalPage; +export const InternshipProposalPreviewPage = () => { + const { t } = useTranslation(); + const proposal = useSelector(state => state.proposal.proposal && internshipSerializationTransformer.reverseTransform(state.proposal.proposal)); + + return + + + Moja praktyka + Podgląd zgłoszenia + + Moje zgłoszenie + + + + { proposal && } + + +} + +export default InternshipProposalFormPage; diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx index b6f6cd1..f7646d9 100644 --- a/src/pages/steps/proposal.tsx +++ b/src/pages/steps/proposal.tsx @@ -18,7 +18,11 @@ const ProposalActions = () => { const { t } = useTranslation(); const ReviewAction = (props: ButtonProps) => - + const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) => +
    } diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index fb5b9ca..f4d2324 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -182,13 +182,13 @@ export const InternshipForm: React.FunctionComponent = prop return (
    - { t('internship.intern-info') } + { t('internship.sections.intern-info') } - { t('internship.kind' )} + { t('internship.sections.kind' )} - { t('internship.duration') } + { t('internship.sections.duration') } - { t('internship.place') } + { t('internship.sections.place') } diff --git a/src/hooks/useProxyState.ts b/src/hooks/useProxyState.ts index 3dd51fa..87b7945 100644 --- a/src/hooks/useProxyState.ts +++ b/src/hooks/useProxyState.ts @@ -1,9 +1,10 @@ -import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; export function useProxyState(initial: T, setter: (value: T) => void): [T, Dispatch>] { const [value, proxy] = useState(initial); - useEffect(() => setter(value), [ value ]); - - return [value, proxy]; + return [value, (newValue: SetStateAction) => { + proxy(newValue); + setter(typeof newValue === "function" ? (newValue as any)(value) : newValue); + }]; } diff --git a/src/serialization/internship.ts b/src/serialization/internship.ts index be27ddf..9a9f628 100644 --- a/src/serialization/internship.ts +++ b/src/serialization/internship.ts @@ -1,6 +1,7 @@ import { Internship, InternshipType } from "@/data"; import { Serializable, SerializationTransformer } from "@/serialization/types"; import { momentSerializationTransformer } from "@/serialization/moment"; +import { Moment } from "moment"; export const internshipSerializationTransformer: SerializationTransformer = { transform: (internship: Internship): Serializable => ({ @@ -10,8 +11,8 @@ export const internshipSerializationTransformer: SerializationTransformer): Internship => ({ ...serialized, - startDate: momentSerializationTransformer.reverseTransform(serialized.startDate), - endDate: momentSerializationTransformer.reverseTransform(serialized.endDate), + startDate: momentSerializationTransformer.reverseTransform(serialized.startDate) as Moment, + endDate: momentSerializationTransformer.reverseTransform(serialized.endDate) as Moment, type: serialized.type as InternshipType, }), } diff --git a/src/styles/index.ts b/src/styles/index.ts new file mode 100644 index 0000000..c7b6bdf --- /dev/null +++ b/src/styles/index.ts @@ -0,0 +1 @@ +export * from "./spacing" diff --git a/src/styles/page.scss b/src/styles/page.scss index f81763a..f6a1b68 100644 --- a/src/styles/page.scss +++ b/src/styles/page.scss @@ -16,3 +16,7 @@ .proposal__primary { font-size: 1.675rem; } + +.proposal__header:not(:first-child) { + margin-top: 1rem; +} diff --git a/src/styles/spacing.ts b/src/styles/spacing.ts new file mode 100644 index 0000000..dc8863d --- /dev/null +++ b/src/styles/spacing.ts @@ -0,0 +1,19 @@ +import { createStyles, makeStyles } from "@material-ui/core/styles"; + +const defaultSpacing: number = 3; + +export const useVerticalSpacing = makeStyles(theme => createStyles({ + root: { + "& > *:not(:last-child)": { + marginBottom: (spacing: number = defaultSpacing) => theme.spacing(spacing) + } + } +})) + +export const useHorizontalSpacing = makeStyles(theme => createStyles({ + root: { + "& > *:not(:last-child)": { + marginRight: (spacing: number = defaultSpacing) => theme.spacing(spacing) + } + } +})) diff --git a/translations/pl.yaml b/translations/pl.yaml index ac7d8dd..c52c43d 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -57,7 +57,7 @@ internship: semester: semestr {{ semester, roman }} album: "numer albumu {{ album }}" date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}" - duration: "{{ duration, humanize }} tygodni" + duration: "{{ duration, humanize }}" hours: "{{ hours }} godzin" office: "Oddział / adres" address: @@ -68,6 +68,7 @@ internship: duration: "Czas trwania praktyki" place: "Miejsce odbywania praktyki" kind: "Rodzaj i program praktyki" + mentor: "Zakładowy opiekun praktyki" steps: -- 2.45.2