From ff2e9c8b82aac24543d017e5006c1de0614b4eba Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 3 Oct 2020 13:47:58 +0200 Subject: [PATCH 1/5] Add isLoggedIn middleware --- src/middleware.tsx | 15 +++++++++++++-- src/routing.tsx | 10 +++++----- src/state/reducer/user.ts | 3 +-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/middleware.tsx b/src/middleware.tsx index 7d16ca3..1bb00b0 100644 --- a/src/middleware.tsx +++ b/src/middleware.tsx @@ -1,10 +1,11 @@ import { Middleware, route } from "@/routing"; import { useSelector } from "react-redux"; -import { isReady } from "@/state/reducer"; +import { AppState, isReady } from "@/state/reducer"; import { Redirect } from "react-router-dom"; import React from "react"; +import { UserState } from "@/state/reducer/user"; -export const isReadyMiddleware: Middleware = next => { +export const isReadyMiddleware: Middleware = next => isLoggedInMiddleware(() => { const ready = useSelector(isReady); if (ready) { @@ -12,4 +13,14 @@ export const isReadyMiddleware: Middleware = next => { } return ; +}) + +export const isLoggedInMiddleware: Middleware = next => { + const user = useSelector(state => state.user) as UserState; + + if (user.loggedIn) { + return next(); + } + + return ; } diff --git a/src/routing.tsx b/src/routing.tsx index f9ef81f..2a5eddd 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -7,7 +7,7 @@ import SubmitPlanPage from "@/pages/internship/plan"; import { UserLoginPage } from "@/pages/user/login"; import { RegisterEditionPage } from "@/pages/edition/register"; import PickEditionPage from "@/pages/edition/pick"; -import { isReadyMiddleware } from "@/middleware"; +import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware"; type Route = { name?: string; @@ -30,16 +30,16 @@ export function processMiddlewares(middleware: Middleware , middlewares: [ isReadyMiddleware ] }, + { name: "home", path: "/", exact: true, content: () => , middlewares: [ isLoggedInMiddleware ] }, // edition - { name: "edition_register", path: "/edition/register", exact: true, content: () => }, - { name: "edition_pick", path: "/edition/pick", exact: true, content: () => }, + { name: "edition_register", path: "/edition/register", exact: true, content: () => , middlewares: [ isLoggedInMiddleware ] }, + { name: "edition_pick", path: "/edition/pick", exact: true, content: () => , middlewares: [ isLoggedInMiddleware ] }, // internship { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, { name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, - { name: "internship_plan", path: "/internship/plan", exact: true, content: () => }, + { name: "internship_plan", path: "/internship/plan", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, // user { name: "user_login", path: "/user/login", content: () => }, diff --git a/src/state/reducer/user.ts b/src/state/reducer/user.ts index ae868a6..1757986 100644 --- a/src/state/reducer/user.ts +++ b/src/state/reducer/user.ts @@ -1,4 +1,3 @@ -import { Reducer } from "react"; import { UserAction, UserActions } from "@/state/actions/user"; export type UserState = { @@ -10,7 +9,7 @@ const initialUserState: UserState = { loggedIn: false, } -const userReducer: Reducer = (state = initialUserState, action) => { +const userReducer = (state: UserState = initialUserState, action: UserAction): UserState => { switch (action.type) { case UserActions.Login: return { From 411603e3a1859a5219f7c279e0c9bdc3aee9964e Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 3 Oct 2020 19:50:12 +0200 Subject: [PATCH 2/5] Add ability to fill student data --- src/api/{page.tsx => page.ts} | 0 src/api/student.ts | 6 ++ src/api/user.ts | 9 +- src/components/proposalPreview.tsx | 8 +- src/data/student.ts | 2 +- src/forms/user.tsx | 133 +++++++++++++++++++++++++++++ src/hooks/index.ts | 1 + src/hooks/state.ts | 18 ++++ src/middleware.tsx | 6 +- src/pages/base.tsx | 2 +- src/pages/main.tsx | 33 ++----- src/pages/steps/insurance.tsx | 11 +-- src/pages/steps/plan.tsx | 4 +- src/pages/steps/proposal.tsx | 4 +- src/pages/steps/student.tsx | 39 +++++++++ src/pages/user/fill.tsx | 31 +++++++ src/pages/user/login.tsx | 4 +- src/pages/user/profile.tsx | 58 +++++++++++++ src/routing.tsx | 6 +- src/serialization/edition.ts | 31 +++++++ src/serialization/index.ts | 1 + src/state/reducer/edition.ts | 5 +- src/state/store.ts | 2 +- translations/pl.yaml | 24 +++++- webpack.config.js | 2 +- 25 files changed, 382 insertions(+), 58 deletions(-) rename src/api/{page.tsx => page.ts} (100%) create mode 100644 src/forms/user.tsx create mode 100644 src/hooks/state.ts create mode 100644 src/pages/steps/student.tsx create mode 100644 src/pages/user/fill.tsx create mode 100644 src/pages/user/profile.tsx create mode 100644 src/serialization/edition.ts diff --git a/src/api/page.tsx b/src/api/page.ts similarity index 100% rename from src/api/page.tsx rename to src/api/page.ts diff --git a/src/api/student.ts b/src/api/student.ts index e93a861..49ad468 100644 --- a/src/api/student.ts +++ b/src/api/student.ts @@ -11,3 +11,9 @@ export async function current(): Promise { return studentDtoTransfer.transform(dto); } +export async function update(student: Student): Promise { + const dto = studentDtoTransfer.reverseTransform(student); + const response = await axios.put(CURRENT_STUDENT_ENDPOINT, dto); + + return student; +} diff --git a/src/api/user.ts b/src/api/user.ts index e930190..831c87b 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,13 +1,16 @@ import { axios } from "@/api/index"; import { query, route } from "@/routing"; -const LOGIN_ENDPOINT = "/access/login" +const LOGIN_ENDPOINT = "/access/login"; +const DEV_LOGIN_ENDPOINT = "/dev/login"; const CLIENT_ID = process.env.LOGIN_CLIENT_ID || "PraktykiClientId"; const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oauth2.0/authorize"; -export async function login(code: string): Promise { - const response = await axios.get(LOGIN_ENDPOINT, { params: { code }}); +export async function login(code?: string): Promise { + const response = code + ? await axios.get(LOGIN_ENDPOINT, { params: { code }}) + : await axios.get(DEV_LOGIN_ENDPOINT); return response.data; } diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx index 3babfc7..8a17cd0 100644 --- a/src/components/proposalPreview.tsx +++ b/src/components/proposalPreview.tsx @@ -6,6 +6,7 @@ import classNames from "classnames"; import { useVerticalSpacing } from "@/styles"; import moment from "moment"; import { Label, Section } from "@/components/section"; +import { StudentPreview } from "@/pages/user/profile"; export type ProposalPreviewProps = { proposal: Internship; @@ -19,12 +20,7 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => { return
- { proposal.intern.name } { proposal.intern.surname } - - { t('internship.intern.semester', { semester: proposal.intern.semester }) } - { ", " } - { t('internship.intern.album', { album: proposal.intern.albumNumber }) } - +
diff --git a/src/data/student.ts b/src/data/student.ts index d649fbd..fe84901 100644 --- a/src/data/student.ts +++ b/src/data/student.ts @@ -23,6 +23,6 @@ export function getMissingStudentData(student: Student): (keyof Student)[] { !!student.email || "email", !!student.albumNumber || "albumNumber", !!student.semester || "semester", - !!student.course || "course", + // !!student.course || "course", ].filter(x => x !== true) as (keyof Student)[]; } diff --git a/src/forms/user.tsx b/src/forms/user.tsx new file mode 100644 index 0000000..9b450bc --- /dev/null +++ b/src/forms/user.tsx @@ -0,0 +1,133 @@ +import { Student } from "@/data"; +import { Transformer } from "@/serialization"; +import React, { useMemo } from "react"; +import { Field, Formik, useFormikContext } from "formik"; +import api from "@/api"; +import { Button, Grid, Typography } from "@material-ui/core"; +import { TextField as TextFieldFormik } from "formik-material-ui"; +import { useTranslation } from "react-i18next"; +import { Actions } from "@/components"; +import { Nullable } from "@/helpers"; +import * as Yup from "yup"; +import { StudentActions, useDispatch } from "@/state/actions"; + +interface StudentFormValues { + firstName: string; + lastName: string; + email: string; + albumNumber: number | ""; + semester: number | ""; +} + +type StudentFormProps = { + student: Student; +} + +const studentToFormValuesTransformer: Transformer, StudentFormValues, { current: Student }> = { + transform(subject: Nullable, context: { current: Student }): StudentFormValues { + return { + firstName: subject.name || "", + lastName: subject.surname || "", + albumNumber: subject.albumNumber || "", + semester: subject.semester || "", + email: subject.email || "", + }; + }, + reverseTransform(subject: StudentFormValues, { current }: { current: Student }): Nullable { + return { + ...current, + name: subject.firstName, + surname: subject.lastName, + albumNumber: subject.albumNumber ? subject.albumNumber : null, + semester: subject.semester ? subject.semester : null, + email: subject.email, + }; + }, +} + +export const StudentForm = ({ student }: StudentFormProps) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const validationSchema = useMemo(() => Yup.object({ + semester: Yup.number().required().min(1).max(10), + albumNumber: Yup.number().required(), + email: Yup.string().required(), + firstName: Yup.string().required(), + lastName: Yup.string().required(), + }), []); + + const initialValues: StudentFormValues = useMemo( + () => studentToFormValuesTransformer.transform(student, { current: student }), + [ student ] + ) + + + const handleFormSubmit = async (values: StudentFormValues) => { + const update = studentToFormValuesTransformer.reverseTransform(values, { current: student }) as Student; + const updated = await api.student.update(update); + + dispatch({ + type: StudentActions.Set, + student: updated, + }) + } + + + const InnerForm = () => { + const { handleSubmit } = useFormikContext(); + + return
+ { t("forms.student.sections.personal") } + + + + + + + + + + + + { t("forms.student.sections.studies")} + + + + + + + + + + + +
+ } + + return + + +} + +export default StudentForm; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 848cfd7..544e1f0 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from "./useProxyState" export * from "./useUpdateEffect" export * from "./useAsync" +export * from "./state" diff --git a/src/hooks/state.ts b/src/hooks/state.ts new file mode 100644 index 0000000..d024dcd --- /dev/null +++ b/src/hooks/state.ts @@ -0,0 +1,18 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { Edition, getEditionDeadlines } from "@/data/edition"; +import { editionSerializationTransformer } from "@/serialization"; +import { Student } from "@/data"; + +export const useCurrentStudent = () => useSelector( + state => state.student +) + +export const useCurrentEdition = () => useSelector( + state => state.edition && editionSerializationTransformer.reverseTransform(state.edition) +) + +export const useDeadlines = () => { + const edition = useCurrentEdition() as Edition; + return getEditionDeadlines(edition); +} diff --git a/src/middleware.tsx b/src/middleware.tsx index 1bb00b0..166de95 100644 --- a/src/middleware.tsx +++ b/src/middleware.tsx @@ -9,7 +9,7 @@ export const isReadyMiddleware: Middleware = next => isLoggedInMiddlew const ready = useSelector(isReady); if (ready) { - return next(); + return <>{ next() }; } return ; @@ -19,8 +19,8 @@ export const isLoggedInMiddleware: Middleware = next => { const user = useSelector(state => state.user) as UserState; if (user.loggedIn) { - return next(); + return <>{ next() }; } - return ; + return ; } diff --git a/src/pages/base.tsx b/src/pages/base.tsx index be27987..bc6e5cc 100644 --- a/src/pages/base.tsx +++ b/src/pages/base.tsx @@ -21,7 +21,7 @@ export const Page = ({ title, children, ...props }: PageProps) => { } -Page.Header = ({ children, maxWidth = false, ...props }: PageHeaderProps) => +Page.Header = ({ children, maxWidth = undefined, ...props }: PageHeaderProps) =>
{ children } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 28215eb..e6d9cf2 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,51 +1,34 @@ -import React, { useEffect, useMemo } from "react"; +import React from "react"; import { Page } from "@/pages/base"; -import { Button, Container, Stepper, Typography } from "@material-ui/core"; -import { Link as RouterLink, Redirect } from "react-router-dom"; +import { Container, Stepper, Typography } from "@material-ui/core"; +import { Redirect } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { AppState } from "@/state/reducer"; -import { getMissingStudentData, Student } from "@/data"; -import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; +import { Student } from "@/data"; import { Step } from "@/components"; import { ProposalStep } from "@/pages/steps/proposal"; import { PlanStep } from "@/pages/steps/plan"; import { InsuranceState } from "@/state/reducer/insurance"; import { InsuranceStep } from "@/pages/steps/insurance"; -import api from "@/api"; +import { StudentStep } from "@/pages/steps/student"; +import { useDeadlines } from "@/hooks"; 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 deadlines = useDeadlines(); const insurance = useSelector(root => root.insurance); - const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); - - useEffect(() => void api.edition.available()) - if (!student) { return ; } function *getSteps() { - yield - { missingStudentData.length > 0 && <> -

{ t('steps.personal-data.info') }

- -
    - { missingStudentData.map(field =>
  • { t(`student.${ field }`) }
  • ) } -
- - - } -
; - + yield ; yield ; yield ; diff --git a/src/pages/steps/insurance.tsx b/src/pages/steps/insurance.tsx index 56e1a41..b005389 100644 --- a/src/pages/steps/insurance.tsx +++ b/src/pages/steps/insurance.tsx @@ -4,16 +4,17 @@ 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"; +import { useDeadlines } from "@/hooks"; +import { StepProps } from "@material-ui/core"; -export const InsuranceStep = () => { +export const InsuranceStep = (props: StepProps) => { const insurance = useSelector(root => root.insurance); - const deadline = useSelector(state => getEditionDeadlines(state.edition as Edition).insurance); // edition cannot be null at this point + const deadline = useDeadlines().insurance; + const { t } = useTranslation(); - return + return

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

diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index 7a94c7f..e6f6c4c 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -9,9 +9,9 @@ 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 { ContactAction, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; +import { useDeadlines } from "@/hooks"; const PlanActions = () => { const status = useSelector(state => getSubmissionStatus(state.plan)); @@ -74,7 +74,7 @@ export const PlanStep = (props: StepProps) => { 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 deadlines = useDeadlines(); const { sent, declined, comment } = submission; diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx index f7646d9..9996ba4 100644 --- a/src/pages/steps/proposal.tsx +++ b/src/pages/steps/proposal.tsx @@ -6,12 +6,12 @@ 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, FileFind } from "mdi-material-ui/index"; import { ContactAction, Status } from "@/pages/steps/common"; +import { useDeadlines } from "@/hooks"; const ProposalActions = () => { const status = useSelector(state => getSubmissionStatus(state.proposal)); @@ -69,7 +69,7 @@ export const ProposalStep = (props: StepProps) => { 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 deadlines = useDeadlines(); const { sent, declined, comment } = submission; diff --git a/src/pages/steps/student.tsx b/src/pages/steps/student.tsx new file mode 100644 index 0000000..24eab0b --- /dev/null +++ b/src/pages/steps/student.tsx @@ -0,0 +1,39 @@ +import { Button, StepProps } from "@material-ui/core"; +import { route } from "@/routing"; +import { Link as RouterLink } from "react-router-dom"; +import { Actions, Step } from "@/components"; +import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { getMissingStudentData, Student } from "@/data"; +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { useDeadlines } from "@/hooks"; +import { AccountDetails } from "mdi-material-ui"; + +export const StudentStep = (props: StepProps) => { + const { t } = useTranslation(); + const student = useSelector(state => state.student); + const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); + const deadlines = useDeadlines(); + + return + { missingStudentData.length > 0 ? <> +

{ t('steps.personal-data.info') }

+ +
    + { missingStudentData.map(field =>
  • { t(`student.${ field }`) }
  • ) } +
+ + + : <> +

{ t('steps.personal-data.all-filled') }

+ + + + } +
+} diff --git a/src/pages/user/fill.tsx b/src/pages/user/fill.tsx new file mode 100644 index 0000000..de3cc1c --- /dev/null +++ b/src/pages/user/fill.tsx @@ -0,0 +1,31 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import React from "react"; +import { Page } from "@/pages/base"; +import { useTranslation } from "react-i18next"; +import { Container, Link, Typography } from "@material-ui/core"; +import StudentForm from "@/forms/user"; +import { Student } from "@/data"; +import { Link as RouterLink } from "react-router-dom"; +import { route } from "@/routing"; + +export const UserFillPage = () => { + const student = useSelector(state => state.student) as Student; + + const { t } = useTranslation(); + + return + + + { t("pages.my-internship.header") } + { t("pages.user-fill.title") } + + { t("pages.user-fill.title") } + + + + + +} + +export default UserFillPage; diff --git a/src/pages/user/login.tsx b/src/pages/user/login.tsx index 46a5a00..c7dd466 100644 --- a/src/pages/user/login.tsx +++ b/src/pages/user/login.tsx @@ -11,7 +11,7 @@ import api from "@/api"; import { UserActions } from "@/state/actions/user"; import { getAuthorizeUrl } from "@/api/user"; -const authorizeUser = (code: string) => async (dispatch: Dispatch, getState: () => AppState): Promise => { +const authorizeUser = (code?: string) => async (dispatch: Dispatch, getState: () => AppState): Promise => { const token = await api.user.login(code); dispatch({ @@ -34,7 +34,7 @@ export const UserLoginPage = () => { const query = new URLSearchParams(useLocation().search); const handleSampleLogin = async () => { - await dispatch(authorizeUser("test")); + await dispatch(authorizeUser()); history.push(route("home")); } diff --git a/src/pages/user/profile.tsx b/src/pages/user/profile.tsx new file mode 100644 index 0000000..5ebc8ba --- /dev/null +++ b/src/pages/user/profile.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Page } from "@/pages/base"; +import { useTranslation } from "react-i18next"; +import { useCurrentStudent } from "@/hooks"; +import { Box, Button, Container, Link, Paper, Typography } from "@material-ui/core"; +import { Student } from "@/data"; +import { Link as RouterLink } from "react-router-dom"; +import { route } from "@/routing"; +import { Actions } from "@/components"; +import { useVerticalSpacing } from "@/styles"; + +type StudentPreviewProps = { + student: Student; +} + +export const StudentPreview = ({ student }: StudentPreviewProps) => { + const { t } = useTranslation(); + + return <> + { student.name } { student.surname } + + { t('internship.intern.semester', { semester: student.semester }) } + { ", " } + { t('internship.intern.album', { album: student.albumNumber }) } + + ; +} + +export const UserProfilePage = () => { + const { t } = useTranslation(); + + const student = useCurrentStudent() as Student; + const spacing = useVerticalSpacing(3); + + return + + + { t("pages.my-internship.header") } + { t("pages.user-profile.title") } + + { t('pages.user-profile.title') } + + + + + + + + + + + + +} + +export default UserProfilePage; diff --git a/src/routing.tsx b/src/routing.tsx index 2a5eddd..f7d37e5 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -8,6 +8,8 @@ import { UserLoginPage } from "@/pages/user/login"; import { RegisterEditionPage } from "@/pages/edition/register"; import PickEditionPage from "@/pages/edition/pick"; import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware"; +import UserFillPage from "@/pages/user/fill"; +import UserProfilePage from "@/pages/user/profile"; type Route = { name?: string; @@ -30,7 +32,7 @@ export function processMiddlewares(middleware: Middleware
, middlewares: [ isLoggedInMiddleware ] }, + { name: "home", path: "/", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, // edition { name: "edition_register", path: "/edition/register", exact: true, content: () => , middlewares: [ isLoggedInMiddleware ] }, @@ -43,6 +45,8 @@ export const routes: Route[] = [ // user { name: "user_login", path: "/user/login", content: () => }, + { name: "user_fill", path: "/user/data", content: () => , middlewares: [ isLoggedInMiddleware ] }, + { name: "user_profile", path: "/user/profile", content: () => , middlewares: [ isLoggedInMiddleware ] }, // fallback route for 404 pages { name: "fallback", path: "*", content: () => } diff --git a/src/serialization/edition.ts b/src/serialization/edition.ts new file mode 100644 index 0000000..ae4555c --- /dev/null +++ b/src/serialization/edition.ts @@ -0,0 +1,31 @@ +import { Serializable, SerializationTransformer } from "@/serialization/types"; +import { Edition } from "@/data/edition"; +import { momentSerializationTransformer } from "@/serialization/moment"; +import { Moment } from "moment"; + +export const editionSerializationTransformer: SerializationTransformer = { + transform(subject: Edition, context?: unknown): Serializable { + return { + course: subject.course, + minimumInternshipHours: subject.minimumInternshipHours, + maximumInternshipHours: subject.maximumInternshipHours, + proposalDeadline: momentSerializationTransformer.transform(subject.proposalDeadline), + reportingEnd: momentSerializationTransformer.transform(subject.reportingEnd), + reportingStart: momentSerializationTransformer.transform(subject.reportingStart), + startDate: momentSerializationTransformer.transform(subject.startDate), + endDate: momentSerializationTransformer.transform(subject.endDate), + } + }, + reverseTransform(subject: Serializable, context?: unknown): Edition { + return { + course: subject.course, + minimumInternshipHours: subject.minimumInternshipHours, + maximumInternshipHours: subject.maximumInternshipHours, + proposalDeadline: momentSerializationTransformer.reverseTransform(subject.proposalDeadline) as Moment, + reportingEnd: momentSerializationTransformer.reverseTransform(subject.reportingEnd) as Moment, + reportingStart: momentSerializationTransformer.reverseTransform(subject.reportingStart) as Moment, + startDate: momentSerializationTransformer.reverseTransform(subject.startDate) as Moment, + endDate: momentSerializationTransformer.reverseTransform(subject.endDate) as Moment, + } + }, +} diff --git a/src/serialization/index.ts b/src/serialization/index.ts index e6efcb2..1f6c52f 100644 --- a/src/serialization/index.ts +++ b/src/serialization/index.ts @@ -1,3 +1,4 @@ export * from "./internship" export * from "./moment" export * from "./types" +export * from "./edition" diff --git a/src/state/reducer/edition.ts b/src/state/reducer/edition.ts index ef696f9..93847c2 100644 --- a/src/state/reducer/edition.ts +++ b/src/state/reducer/edition.ts @@ -1,14 +1,15 @@ import { Edition } from "@/data/edition"; import { EditionAction, EditionActions } from "@/state/actions/edition"; +import { editionSerializationTransformer, Serializable } from "@/serialization"; -export type EditionState = Edition | null; +export type EditionState = Serializable | null; const initialEditionState: EditionState = null; const editionReducer = (state: EditionState = initialEditionState, action: EditionAction): EditionState => { switch (action.type) { case EditionActions.Set: - return action.edition; + return editionSerializationTransformer.transform(action.edition); } return state; diff --git a/src/state/store.ts b/src/state/store.ts index 935b3b2..7168bf7 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -10,7 +10,7 @@ const store = createStore( { key: 'state', storage: sessionStorage, - blacklist: ['edition'] + blacklist: [] }, rootReducer ), diff --git a/translations/pl.yaml b/translations/pl.yaml index 2d7554f..d077211 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -11,7 +11,7 @@ left: jeszcze {{ left, humanize }} confirm: zatwierdź go-back: wstecz - +save: zapisz make-changes: wprowadź zmiany review: podgląd fix-errors: popraw uwagi @@ -39,8 +39,22 @@ pages: my-editions: "Moje praktyki" pick: "wybierz" register: "Zapisz się do edycji praktyk" + user-fill: + title: "Uzupełnij swoje dane" + user-profile: + title: "Moje dane" forms: + student: + fields: + first-name: Imię + last-name: Nazwisko + email: Kontaktowy adres e-mail + album-number: Numer albumu + semester: Aktualny semestr studiów + sections: + personal: "Dane osobowe" + studies: "Dane kierunkowe" internship: fields: start-date: Data rozpoczęcia praktyki @@ -127,11 +141,15 @@ internship: steps: personal-data: - header: "Uzupełnienie informacji" + header: "Uzupełnienie danych" 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 pełnomocnikiem ds. praktyk dla Twojego kierunku. - form: "Uzupełnij dane" + all-filled: > + Wypełniłeś wszystkie wymagane informacje o sobie. + actions: + form: "Uzupełnij dane" + info: $t(pages.user-profile.title) internship-proposal: header: "Zgłoszenie praktyki" info: diff --git a/webpack.config.js b/webpack.config.js index 6585967..201baa4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -59,7 +59,7 @@ const config = { port: parseInt(process.env.APP_PORT || "3000"), proxy: { "/api": { - target: "http://system-praktyk-front.localhost:8080/", + target: "https://system-praktyk.stg.kadet.net/api/", changeOrigin: true, pathRewrite: { "^/api": '' From 8b2523572d6e515647ac1752fa771a4c7a64495e Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 4 Oct 2020 14:18:25 +0200 Subject: [PATCH 3/5] Add internship registration submission --- package.json | 1 + src/api/companies.ts | 18 ++++++ src/api/dto/internship-registration.ts | 33 +++++++++++ src/api/dto/type.ts | 34 +++++++++++ src/api/edition.ts | 19 +++++- src/api/index.ts | 12 +++- src/api/internship.ts | 11 ++++ src/api/type.ts | 12 ++++ src/api/user.ts | 2 +- src/components/proposalPreview.tsx | 4 +- src/data/internship.ts | 49 ++-------------- src/forms/company.tsx | 24 ++++++-- src/forms/internship.tsx | 66 ++++++++++----------- src/forms/student.tsx | 7 ++- src/hooks/index.ts | 1 + src/hooks/providers.ts | 15 +++++ src/pages/edition/pick.tsx | 13 ++++- src/routing.tsx | 5 +- src/state/reducer/insurance.ts | 7 --- yarn.lock | 80 ++++++++++++++++++++++++++ 20 files changed, 309 insertions(+), 104 deletions(-) create mode 100644 src/api/companies.ts create mode 100644 src/api/dto/internship-registration.ts create mode 100644 src/api/dto/type.ts create mode 100644 src/api/internship.ts create mode 100644 src/api/type.ts create mode 100644 src/hooks/providers.ts diff --git a/package.json b/package.json index f2da2fd..df24a9f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "html-webpack-plugin": "4.0.0-beta.11", "i18next": "^19.6.0", "i18next-browser-languagedetector": "^5.0.0", + "jsonwebtoken": "^8.5.1", "material-ui-dropzone": "^3.3.0", "mdi-material-ui": "^6.17.0", "moment": "^2.26.0", diff --git a/src/api/companies.ts b/src/api/companies.ts new file mode 100644 index 0000000..838348f --- /dev/null +++ b/src/api/companies.ts @@ -0,0 +1,18 @@ +import { Company, Office } from "@/data"; +import { axios } from "@/api/index"; +import { prepare, query } from "@/routing"; + +export const COMPANY_SEARCH_ENDPOINT = '/companies'; +export const COMPANY_OFFICES_ENDPOINT = '/companies/:id' + +export async function search(name: string): Promise { + const companies = await axios.get(query(COMPANY_SEARCH_ENDPOINT, { Name: name })); + + return companies.data; +} + +export async function offices(id: string): Promise { + const response = await axios.get(prepare(COMPANY_OFFICES_ENDPOINT, { id })); + + return response.data; +} diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts new file mode 100644 index 0000000..b7a94a7 --- /dev/null +++ b/src/api/dto/internship-registration.ts @@ -0,0 +1,33 @@ +import { Identifiable, Internship, Mentor } from "@/data"; +import { OneWayTransformer } from "@/serialization"; +import { Nullable } from "@/helpers"; + +export interface InternshipRegistrationUpdateCompany { + id: string, + branchOffice: Identifiable, +} + +export interface InternshipRegistrationUpdate { + company: InternshipRegistrationUpdateCompany, + start: string, + end: string, + type: number, + mentor: Mentor, +} + +export const internshipRegistrationUpdateTransformer: OneWayTransformer, Nullable> = { + transform(subject: Nullable, context?: unknown): Nullable { + return { + start: subject?.startDate?.toISOString() || null, + end: subject?.endDate?.toISOString() || null, + type: parseInt(subject?.type?.id || "0"), + mentor: subject.mentor, + company: { + id: subject?.company?.id as string, + branchOffice: { + id: subject?.office?.id + }, + } + } + } +} diff --git a/src/api/dto/type.ts b/src/api/dto/type.ts new file mode 100644 index 0000000..213dcb1 --- /dev/null +++ b/src/api/dto/type.ts @@ -0,0 +1,34 @@ +import { Identifiable, InternshipType } from "@/data"; +import { Transformer } from "@/serialization"; + +export interface InternshipTypeDTO extends Identifiable { + label: string; + labelEng: string; + description?: string; + descriptionEng?: string; +} + +export const internshipTypeDtoTransformer: Transformer = { + transform(subject: InternshipTypeDTO, context?: unknown): InternshipType { + return { + id: subject.id, + label: { + pl: subject.label, + en: subject.labelEng + }, + description: subject.description ? { + pl: subject.description, + en: subject.descriptionEng || "" + } : undefined + } + }, + reverseTransform(subject: InternshipType, context?: unknown): InternshipTypeDTO { + return { + id: subject.id, + label: subject.label.pl, + labelEng: subject.label.en, + description: subject.description?.pl || undefined, + descriptionEng: subject.description?.en || undefined, + } + }, +} diff --git a/src/api/edition.ts b/src/api/edition.ts index 6164634..1a04c9b 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -5,7 +5,9 @@ import { EditionDTO, editionDtoTransformer, editionTeaserDtoTransformer } from " const EDITIONS_ENDPOINT = "/editions"; const EDITION_INFO_ENDPOINT = "/editions/:key"; -const REGISTER_ENDPOINT = "/register"; +const EDITION_CURRENT_ENDPOINT = "/editions/current"; +const EDITION_REGISTER_ENDPOINT = "/register"; +const EDITION_LOGIN_ENDPOINT = "/access/loginEdition"; export async function available() { const response = await axios.get(EDITIONS_ENDPOINT); @@ -15,7 +17,7 @@ export async function available() { export async function join(key: string): Promise { try { - await axios.post(REGISTER_ENDPOINT, JSON.stringify(key), { headers: { "Content-Type": "application/json" } }); + await axios.post(EDITION_REGISTER_ENDPOINT, JSON.stringify(key), { headers: { "Content-Type": "application/json" } }); return true; } catch (error) { console.error(error); @@ -29,3 +31,16 @@ export async function get(key: string): Promise { return editionDtoTransformer.transform(dto); } + +export async function current(): Promise { + const response = await axios.get(EDITION_CURRENT_ENDPOINT); + const dto = response.data; + + return editionDtoTransformer.transform(dto); +} + +export async function login(key: string): Promise { + const response = await axios.post(EDITION_LOGIN_ENDPOINT, JSON.stringify(key), { headers: { "Content-Type": "application/json" } }) + + return response.data; +} diff --git a/src/api/index.ts b/src/api/index.ts index 3432330..cb20750 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,8 +5,11 @@ import { UserState } from "@/state/reducer/user"; import * as user from "./user"; import * as edition from "./edition"; -import * as page from "./page" -import * as student from "./student" +import * as page from "./page"; +import * as student from "./student"; +import * as type from "./type"; +import * as companies from "./companies"; +import * as internship from "./internship"; export const axios = Axios.create({ baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/", @@ -33,7 +36,10 @@ const api = { user, edition, page, - student + student, + type, + companies, + internship, } export default api; diff --git a/src/api/internship.ts b/src/api/internship.ts new file mode 100644 index 0000000..3096b15 --- /dev/null +++ b/src/api/internship.ts @@ -0,0 +1,11 @@ +import { InternshipRegistrationUpdate } from "@/api/dto/internship-registration"; +import { axios } from "@/api/index"; +import { Nullable } from "@/helpers"; + +const INTERNSHIP_ENDPOINT = '/internshipRegistration'; + +export async function update(internship: Nullable): Promise { + const response = await axios.put(INTERNSHIP_ENDPOINT, internship); + + return true; +} diff --git a/src/api/type.ts b/src/api/type.ts new file mode 100644 index 0000000..aa6c577 --- /dev/null +++ b/src/api/type.ts @@ -0,0 +1,12 @@ +import { InternshipType } from "@/data"; +import { axios } from "@/api/index"; +import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; + +const AVAILABLE_INTERNSHIP_TYPES = '/internshipTypes'; + +export async function available(): Promise { + const response = await axios.get(AVAILABLE_INTERNSHIP_TYPES); + const dtos = response.data; + + return dtos.map(dto => internshipTypeDtoTransformer.transform(dto)); +} diff --git a/src/api/user.ts b/src/api/user.ts index 831c87b..cd2a0de 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -9,7 +9,7 @@ const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oaut export async function login(code?: string): Promise { const response = code - ? await axios.get(LOGIN_ENDPOINT, { params: { code }}) + ? await axios.post(LOGIN_ENDPOINT, JSON.stringify(code), { headers: { 'Content-Type': 'application/json' } }) : await axios.get(DEV_LOGIN_ENDPOINT); return response.data; diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx index 8a17cd0..b452875 100644 --- a/src/components/proposalPreview.tsx +++ b/src/components/proposalPreview.tsx @@ -1,4 +1,4 @@ -import { Internship, internshipTypeLabels } from "@/data"; +import { Internship } from "@/data"; import React from "react"; import { Typography } from "@material-ui/core"; import { useTranslation } from "react-i18next"; @@ -39,7 +39,7 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
- { internshipTypeLabels[proposal.type].label } + { proposal.type.label.pl }
diff --git a/src/data/internship.ts b/src/data/internship.ts index 4cd82ea..29733f4 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -1,52 +1,11 @@ import { Moment } from "moment"; -import { Identifiable } from "./common"; +import { Identifiable, Multilingual } from "./common"; import { Student } from "@/data/student"; import { Company, Office } from "@/data/company"; -export enum InternshipType { - FreeInternship = "FreeInternship", - GraduateInternship = "GraduateInternship", - FreeApprenticeship = "FreeApprenticeship", - PaidApprenticeship = "PaidApprenticeship", - ForeignInternship = "ForeignInternship", - UOP = "UOP", - UD = "UD", - UZ = "UZ", - Other = "Other", -} - -export const internshipTypeLabels: { [type in InternshipType]: { label: string, description?: string } } = { - [InternshipType.FreeInternship]: { - label: "Umowa o organizację praktyki", - description: "Praktyka bezpłatna" - }, - [InternshipType.GraduateInternship]: { - label: "Umowa o praktykę absolwencką" - }, - [InternshipType.FreeApprenticeship]: { - label: "Umowa o staż bezpłatny" - }, - [InternshipType.PaidApprenticeship]: { - label: "Umowa o staż płatny", - description: "np. przemysłowy" - }, - [InternshipType.ForeignInternship]: { - label: "Praktyka zagraniczna", - description: "np. IAESTE, ERASMUS" - }, - [InternshipType.UOP]: { - label: "Umowa o pracę" - }, - [InternshipType.UD]: { - label: "Umowa o dzieło (w tym B2B)" - }, - [InternshipType.UZ]: { - label: "Umowa o zlecenie (w tym B2B)" - }, - [InternshipType.Other]: { - label: "Inna", - description: "Należy wprowadzić samodzielnie" - }, +export interface InternshipType extends Identifiable { + label: Multilingual, + description?: Multilingual, } export interface InternshipProgramEntry extends Identifiable { diff --git a/src/forms/company.tsx b/src/forms/company.tsx index 74fbfdb..21a5300 100644 --- a/src/forms/company.tsx +++ b/src/forms/company.tsx @@ -1,12 +1,12 @@ -import React, { HTMLProps, useMemo } from "react"; +import React, { HTMLProps, useEffect, useMemo, useState } from "react"; import { Company, formatAddress, Office } from "@/data"; -import { sampleCompanies } from "@/provider/dummy"; import { Autocomplete } from "@material-ui/lab"; import { Grid, TextField, Typography } from "@material-ui/core"; import { InternshipFormValues } from "@/forms/internship"; import { useTranslation } from "react-i18next"; import { Field, useFormikContext } from "formik"; import { TextField as TextFieldFormik } from "formik-material-ui" +import api from "@/api"; export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps) => (
@@ -27,9 +27,15 @@ export const BranchForm: React.FC = () => { const { t } = useTranslation(); const disabled = useMemo(() => !values.companyName, [values.companyName]); - const offices = useMemo(() => values.company?.offices || [], [values.company]); + const [offices, setOffices] = useState([]); const canEdit = useMemo(() => !values.office && !disabled, [values.office, disabled]); + useEffect(() => { + (async () => { + setOffices(values.company?.id ? (await api.companies.offices(values.company?.id)) : []); + })() + }, [ values.company?.id ]) + const handleCityChange = (event: any, value: Office | string | null) => { if (typeof value === "string") { setValues({ @@ -143,8 +149,17 @@ export const CompanyForm: React.FunctionComponent = () => { const { values, setValues, errors, touched, setFieldTouched } = useFormikContext(); const { t } = useTranslation(); + const [input, setInput] = useState(""); + const [companies, setCompanies] = useState([]); + const canEdit = useMemo(() => !values.company, [values.company]); + useEffect(() => { + (async () => { + setCompanies(await api.companies.search(input)); + })() + }, [ input ]); + const handleCompanyChange = (event: any, value: Company | string | null) => { setFieldTouched("companyName", true); @@ -174,13 +189,14 @@ export const CompanyForm: React.FunctionComponent = () => { <> - typeof option === "string" ? option : option.name } renderOption={ company => } renderInput={ props => } onChange={ handleCompanyChange } value={ values.company || values.companyName } freeSolo + onInputChange={ (_, value) => setInput(value) } /> diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 10fb302..3ce409a 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -4,13 +4,13 @@ import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; import { CompanyForm } from "@/forms/company"; import { StudentForm } from "@/forms/student"; import { sampleStudent } from "@/provider/dummy/student"; -import { Company, Internship, InternshipType, internshipTypeLabels, Office, Student } from "@/data"; +import { Company, Internship, InternshipType, Office, Student } from "@/data"; import { Nullable } from "@/helpers"; import moment, { Moment } from "moment"; import { computeWorkingHours } from "@/utils/date"; import { Autocomplete } from "@material-ui/lab"; import { emptyInternship } from "@/provider/dummy/internship"; -import { InternshipProposalActions, useDispatch } from "@/state/actions"; +import { useDispatch } from "@/state/actions"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { AppState } from "@/state/reducer"; @@ -22,8 +22,9 @@ import { Field, Form, Formik, useFormikContext } from "formik"; import * as Yup from "yup"; import { Transformer } from "@/serialization"; import { TextField as TextFieldFormik } from "formik-material-ui" -import { Edition } from "@/data/edition"; -import { useUpdateEffect } from "@/hooks"; +import { useCurrentEdition, useCurrentStudent, useInternshipTypes, useUpdateEffect } from "@/hooks"; +import { internshipRegistrationUpdateTransformer } from "@/api/dto/internship-registration"; +import api from "@/api"; export type InternshipFormValues = { startDate: Moment | null; @@ -73,13 +74,11 @@ const emptyInternshipValues: InternshipFormValues = { workingHours: 40, } -export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType } & HTMLProps) => { - const info = internshipTypeLabels[type]; - +export const InternshipTypeItem = ({ internshipType: type, ...props }: { internshipType: InternshipType } & HTMLProps) => { return (
-
{ info.label }
- { info.description && { info.description } } +
{ type.label.pl }
+ { type.description && { type.description.pl } }
) } @@ -88,25 +87,27 @@ const InternshipProgramForm = () => { const { t } = useTranslation(); const { values, handleBlur, setFieldValue, errors } = useFormikContext(); + const types = useInternshipTypes(); + return ( } - getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label } - renderOption={ (option: InternshipType) => } - options={ Object.values(InternshipType) as InternshipType[] } + getOptionLabel={ (option: InternshipType) => option.label.pl } + renderOption={ (option: InternshipType) => } + options={ types } disableClearable value={ values.kind || undefined } onChange={ (_, value) => setFieldValue("kind", value) } onBlur={ handleBlur } /> - - { - values.kind === InternshipType.Other && - - } - + {/**/} + {/* {*/} + {/* values.kind === InternshipType.Other &&*/} + {/* */} + {/* }*/} + {/**/} ) } @@ -240,19 +241,20 @@ const converter: Transformer, InternshipFormValues, Interns } export const InternshipForm: React.FunctionComponent = () => { + const student = useCurrentStudent(); + const initialInternship = useSelector>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, office: null, company: null, mentor: null, - intern: sampleStudent + intern: student }); - const edition = useSelector(state => state.edition as Edition); + const edition = useCurrentEdition(); const { t } = useTranslation(); - const dispatch = useDispatch(); const history = useHistory(); @@ -268,7 +270,7 @@ export const InternshipForm: React.FunctionComponent = () => { .required(t("validation.required")) .matches(/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/, t("validation.phone")), hours: Yup.number() - .min(edition.minimumInternshipHours, t("validation.internship.minimum-hours", { hours: edition.minimumInternshipHours })), + .min(edition?.minimumInternshipHours || 0, t("validation.internship.minimum-hours", { hours: edition?.minimumInternshipHours || 0 })), companyName: Yup.string().when("company", { is: null, then: Yup.string().required(t("validation.required")) @@ -283,10 +285,10 @@ export const InternshipForm: React.FunctionComponent = () => { city: Yup.string().required(t("validation.required")), postalCode: Yup.string().required(t("validation.required")), building: Yup.string().required(t("validation.required")), - kindOther: Yup.string().when("kind", { - is: (values: InternshipFormValues) => values?.kind === InternshipType.Other, - then: Yup.string().required(t("validation.required")) - }) + // kindOther: Yup.string().when("kind", { + // is: (values: InternshipFormValues) => values?.kind === InternshipType.Other, + // then: Yup.string().required(t("validation.required")) + // }) }) const values = converter.transform(initialInternship); @@ -294,14 +296,12 @@ export const InternshipForm: React.FunctionComponent = () => { const handleSubmit = (values: InternshipFormValues) => { setConfirmDialogOpen(false); - dispatch({ - type: InternshipProposalActions.Send, - internship: converter.reverseTransform(values, { - internship: initialInternship as Internship, - }) as Internship - }); + const internship = converter.reverseTransform(values, { internship: initialInternship as Internship }); + const update = internshipRegistrationUpdateTransformer.transform(internship); - history.push(route("home")) + api.internship.update(update); + + // history.push(route("home")) } const InnerForm = () => { diff --git a/src/forms/student.tsx b/src/forms/student.tsx index 1c8ee26..189c3c4 100644 --- a/src/forms/student.tsx +++ b/src/forms/student.tsx @@ -2,14 +2,15 @@ import { Course } from "@/data"; import { Button, Grid, TextField } from "@material-ui/core"; import { Alert, Autocomplete } from "@material-ui/lab"; import React from "react"; -import { sampleCourse } from "@/provider/dummy/student"; import { useTranslation } from "react-i18next"; import { useFormikContext } from "formik"; import { InternshipFormValues } from "@/forms/internship"; +import { useCurrentEdition } from "@/hooks"; export const StudentForm = () => { const { t } = useTranslation(); const { values: { student } } = useFormikContext(); + const course = useCurrentEdition()?.course as Course; return <> @@ -26,8 +27,8 @@ export const StudentForm = () => { course.name } renderInput={ props => } - options={[ sampleCourse ]} - value={ student.course } + options={[ course ]} + value={ course } disabled /> diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 544e1f0..3c9050a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from "./useProxyState" export * from "./useUpdateEffect" export * from "./useAsync" export * from "./state" +export * from "./providers" diff --git a/src/hooks/providers.ts b/src/hooks/providers.ts new file mode 100644 index 0000000..2f0d8f6 --- /dev/null +++ b/src/hooks/providers.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; +import api from "@/api"; +import { InternshipType } from "@/data"; + +export const useInternshipTypes = () => { + const [types, setTypes] = useState([]); + + useEffect(() => { + (async () => { + setTypes(await api.type.available()); + })() + }, []) + + return types; +} diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx index 5275408..a1261c0 100644 --- a/src/pages/edition/pick.tsx +++ b/src/pages/edition/pick.tsx @@ -11,7 +11,7 @@ import api from "@/api"; import { Section } from "@/components/section"; import { useVerticalSpacing } from "@/styles"; import { Alert } from "@material-ui/lab"; -import { EditionActions, useDispatch } from "@/state/actions"; +import { EditionActions, useDispatch, UserActions } from "@/state/actions"; export const PickEditionPage = () => { const { t } = useTranslation(); @@ -23,12 +23,19 @@ export const PickEditionPage = () => { const classes = useVerticalSpacing(3); const pickEditionHandler = (id: string) => async () => { - const edition = await api.edition.get(id); + const token = await api.edition.login(id); - if (!edition) { + if (!token) { return; } + await dispatch({ + type: UserActions.Login, + token, + }) + + const edition = await api.edition.current(); + dispatch({ type: EditionActions.Set, edition diff --git a/src/routing.tsx b/src/routing.tsx index f7d37e5..8b88172 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -67,7 +67,10 @@ export function route(name: string, params: URLParams = {}) { } export const query = (url: string, params: URLParams) => { - const query = Object.entries(params).map(([name, value]) => `${ name }=${ encodeURIComponent(value) }`).join("&"); + const query = Object.entries(params) + .filter(([_, value]) => !!value) + .map(([name, value]) => `${ name }=${ encodeURIComponent(value) }`) + .join("&"); return url + (query.length > 0 ? `?${ query }` : ''); } diff --git a/src/state/reducer/insurance.ts b/src/state/reducer/insurance.ts index 61cabc9..9e99ae2 100644 --- a/src/state/reducer/insurance.ts +++ b/src/state/reducer/insurance.ts @@ -1,7 +1,6 @@ import { Reducer } from "react"; import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance"; import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions"; -import { InternshipType } from "@/data"; export type InsuranceState = { required: boolean; @@ -21,12 +20,6 @@ export const insuranceReducer: Reducer Date: Wed, 7 Oct 2020 22:25:55 +0200 Subject: [PATCH 4/5] Add obtaining info about current internship status --- src/api/dto/internship-registration.ts | 53 ++++++++++++++++++++++++-- src/api/dto/mentor.ts | 28 ++++++++++++++ src/api/internship.ts | 10 ++++- src/pages/main.tsx | 18 ++++++++- src/state/actions/proposal.ts | 3 ++ src/state/reducer/proposal.ts | 12 ++++++ 6 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 src/api/dto/mentor.ts diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts index b7a94a7..09659c6 100644 --- a/src/api/dto/internship-registration.ts +++ b/src/api/dto/internship-registration.ts @@ -1,6 +1,18 @@ -import { Identifiable, Internship, Mentor } from "@/data"; -import { OneWayTransformer } from "@/serialization"; +import { Company, Identifiable, Internship, Mentor, Office } from "@/data"; +import { momentSerializationTransformer, OneWayTransformer } from "@/serialization"; import { Nullable } from "@/helpers"; +import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor"; +import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; +import { Moment } from "moment"; +import { sampleStudent } from "@/provider/dummy"; + +export enum SubmissionState { + Draft, + Submitted, + Accepted, + Rejected, + Archival, +} export interface InternshipRegistrationUpdateCompany { id: string, @@ -12,7 +24,21 @@ export interface InternshipRegistrationUpdate { start: string, end: string, type: number, - mentor: Mentor, + mentor: MentorDTO, +} + +export interface InternshipRegistrationDTO extends Identifiable { + start: string; + end: string; + type: InternshipTypeDTO, + state: SubmissionState, + mentor: MentorDTO, + company: Company, + branchAddress: Office, +} + +export interface InternshipInfoDTO { + internshipRegistration: InternshipRegistrationDTO; } export const internshipRegistrationUpdateTransformer: OneWayTransformer, Nullable> = { @@ -21,7 +47,7 @@ export const internshipRegistrationUpdateTransformer: OneWayTransformer = { + transform(dto: InternshipRegistrationDTO, context?: unknown): Internship { + return { + id: dto.id, + office: dto.branchAddress, + company: dto.company, + mentor: mentorDtoTransformer.transform(dto.mentor), + startDate: momentSerializationTransformer.reverseTransform(dto.start) as Moment, + endDate: momentSerializationTransformer.reverseTransform(dto.end) as Moment, + type: internshipTypeDtoTransformer.transform(dto.type), + hours: 0, + isAccepted: dto.state === SubmissionState.Accepted, + lengthInWeeks: 0, + program: [], + intern: sampleStudent, // fixme + }; + } +} diff --git a/src/api/dto/mentor.ts b/src/api/dto/mentor.ts new file mode 100644 index 0000000..2d5f03a --- /dev/null +++ b/src/api/dto/mentor.ts @@ -0,0 +1,28 @@ +import { Transformer } from "@/serialization"; +import { Mentor } from "@/data"; + +export interface MentorDTO { + firstName: string; + lastName: string; + email: string; + phoneNumber: string; +} + +export const mentorDtoTransformer: Transformer = { + reverseTransform(subject: Mentor, context?: unknown): MentorDTO { + return { + firstName: subject.name, + lastName: subject.surname, + email: subject.email, + phoneNumber: subject.phone || "", + } + }, + transform(subject: MentorDTO, context?: unknown): Mentor { + return { + name: subject.firstName, + surname: subject.lastName, + email: subject.email, + phone: subject.phoneNumber, + } + } +} diff --git a/src/api/internship.ts b/src/api/internship.ts index 3096b15..cc4dda0 100644 --- a/src/api/internship.ts +++ b/src/api/internship.ts @@ -1,4 +1,4 @@ -import { InternshipRegistrationUpdate } from "@/api/dto/internship-registration"; +import { InternshipInfoDTO, InternshipRegistrationUpdate } from "@/api/dto/internship-registration"; import { axios } from "@/api/index"; import { Nullable } from "@/helpers"; @@ -9,3 +9,11 @@ export async function update(internship: Nullable) return true; } + +export async function get(): Promise { + const response = await axios.get(INTERNSHIP_ENDPOINT); + + console.log(response); + + return response.data; +} diff --git a/src/pages/main.tsx b/src/pages/main.tsx index e6d9cf2..e204644 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { Page } from "@/pages/base"; import { Container, Stepper, Typography } from "@material-ui/core"; import { Redirect } from "react-router-dom"; @@ -14,6 +14,9 @@ import { InsuranceState } from "@/state/reducer/insurance"; import { InsuranceStep } from "@/pages/steps/insurance"; import { StudentStep } from "@/pages/steps/student"; import { useDeadlines } from "@/hooks"; +import api from "@/api"; +import { InternshipProposalActions, useDispatch } from "@/state/actions"; +import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration"; export const MainPage = () => { const { t } = useTranslation(); @@ -22,6 +25,19 @@ export const MainPage = () => { const deadlines = useDeadlines(); const insurance = useSelector(root => root.insurance); + const dispatch = useDispatch(); + + useEffect(() => { + (async () => { + const internship = await api.internship.get(); + + dispatch({ + type: InternshipProposalActions.Receive, + state: internship.internshipRegistration.state, + internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration), + }) + })() + }, []) if (!student) { return ; diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index ad8102a..2db7692 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -6,6 +6,7 @@ import { SaveSubmissionAction, SendSubmissionAction } from "@/state/actions/submission"; +import { SubmissionState } from "@/api/dto/internship-registration"; export enum InternshipProposalActions { Send = "SEND_PROPOSAL", @@ -26,6 +27,8 @@ export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAc } export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction { + internship: Internship; + state: SubmissionState, } export interface SaveProposalAction extends SaveSubmissionAction { diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts index 783983c..d20eeb4 100644 --- a/src/state/reducer/proposal.ts +++ b/src/state/reducer/proposal.ts @@ -11,6 +11,7 @@ import { } from "@/state/reducer/submission"; import { Reducer } from "react"; import { SubmissionAction } from "@/state/actions/submission"; +import { SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration"; export type InternshipProposalState = SubmissionState & MayRequireDeanApproval & { proposal: Serializable | null; @@ -43,6 +44,17 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter ...state, proposal: internshipSerializationTransformer.transform(action.internship), } + case InternshipProposalActions.Receive: + return { + ...state, + accepted: action.state === ApiSubmissionState.Accepted, + sent: [ + ApiSubmissionState.Accepted, + ApiSubmissionState.Rejected, + ApiSubmissionState.Submitted + ].includes(action.state), + proposal: internshipSerializationTransformer.transform(action.internship), + } default: return state; } From 5879efc978a73ce5e010025ccf0aaeb0e9c07295 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 18 Oct 2020 13:48:05 +0200 Subject: [PATCH 5/5] Fix multiple issues with forms --- src/api/dto/internship-registration.ts | 46 ++++++++++++++++++-------- src/api/index.ts | 2 ++ src/api/internship.ts | 5 +-- src/api/upload.ts | 23 +++++++++++++ src/forms/company.tsx | 12 +++++-- src/forms/internship.tsx | 10 +++--- src/forms/plan.tsx | 7 ++-- src/forms/student.tsx | 2 +- src/middleware.tsx | 8 ++--- src/pages/internship/proposal.tsx | 8 ++--- src/pages/user/profile.tsx | 2 +- src/state/reducer/edition.ts | 6 +++- 12 files changed, 96 insertions(+), 35 deletions(-) create mode 100644 src/api/upload.ts diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts index 09659c6..b415db9 100644 --- a/src/api/dto/internship-registration.ts +++ b/src/api/dto/internship-registration.ts @@ -1,4 +1,4 @@ -import { Company, Identifiable, Internship, Mentor, Office } from "@/data"; +import { Address, Company, Identifiable, Internship, Mentor, Office } from "@/data"; import { momentSerializationTransformer, OneWayTransformer } from "@/serialization"; import { Nullable } from "@/helpers"; import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor"; @@ -7,24 +7,34 @@ import { Moment } from "moment"; import { sampleStudent } from "@/provider/dummy"; export enum SubmissionState { - Draft, - Submitted, - Accepted, - Rejected, - Archival, + Draft = "Draft", + Submitted = "Submitted", + Accepted = "Accepted", + Rejected = "Rejected", + Archival = "Archival", +} + +export interface NewBranchOffice extends Address { } export interface InternshipRegistrationUpdateCompany { id: string, - branchOffice: Identifiable, + branchOffice: Identifiable | NewBranchOffice, +} + +export interface NewCompany { + nip: string; + name: string; + branchOffice: NewBranchOffice | null; } export interface InternshipRegistrationUpdate { - company: InternshipRegistrationUpdateCompany, + company: InternshipRegistrationUpdateCompany | NewCompany, start: string, end: string, type: number, mentor: MentorDTO, + hours: number, } export interface InternshipRegistrationDTO extends Identifiable { @@ -35,8 +45,11 @@ export interface InternshipRegistrationDTO extends Identifiable { mentor: MentorDTO, company: Company, branchAddress: Office, + declaredHours: number, } +const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id }; + export interface InternshipInfoDTO { internshipRegistration: InternshipRegistrationDTO; } @@ -48,12 +61,17 @@ export const internshipRegistrationUpdateTransformer: OneWayTransformer): Promise { - const response = await axios.put(INTERNSHIP_ENDPOINT, internship); + const response = await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); return true; } diff --git a/src/api/upload.ts b/src/api/upload.ts new file mode 100644 index 0000000..e27735a --- /dev/null +++ b/src/api/upload.ts @@ -0,0 +1,23 @@ +import { Identifiable } from "@/data"; +import { axios } from "@/api/index"; + +export enum UploadType { + Ipp = "IppScan", + DeanConsent = "DeanConsent", + Insurance = "NnwInsurance", +} + +const CREATE_DOCUMENT_ENDPOINT = '/document'; +const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan'; + +interface Document extends Identifiable { + description?: string; + type: UploadType; +} + +export async function create(type: UploadType, content: File) +{ + const response = await axios.post(CREATE_DOCUMENT_ENDPOINT, { type }); + + console.log(response.data); +} diff --git a/src/forms/company.tsx b/src/forms/company.tsx index 21a5300..246b1c2 100644 --- a/src/forms/company.tsx +++ b/src/forms/company.tsx @@ -103,7 +103,7 @@ export const BranchForm: React.FC = () => { onInputChange={ handleCityInput } onBlur={ ev => setFieldTouched("city", true) } inputValue={ values.city } - value={ values.office ? values.office : null } + value={ values.office ? values.office : values.city } freeSolo />
@@ -155,9 +155,15 @@ export const CompanyForm: React.FunctionComponent = () => { const canEdit = useMemo(() => !values.company, [values.company]); useEffect(() => { + if (!input || values.companyName == input) { + return; + } + (async () => { setCompanies(await api.companies.search(input)); })() + + setValues({ ...values, company: null, companyName: input }, true) }, [ input ]); const handleCompanyChange = (event: any, value: Company | string | null) => { @@ -194,7 +200,9 @@ export const CompanyForm: React.FunctionComponent = () => { renderOption={ company => } renderInput={ props => } - onChange={ handleCompanyChange } value={ values.company || values.companyName } + onChange={ handleCompanyChange } + value={ values.company || values.companyName } + inputValue={ input } freeSolo onInputChange={ (_, value) => setInput(value) } /> diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 3ce409a..c36b3a3 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -97,7 +97,7 @@ const InternshipProgramForm = () => { renderOption={ (option: InternshipType) => } options={ types } disableClearable - value={ values.kind || undefined } + value={ values.kind || null as any } onChange={ (_, value) => setFieldValue("kind", value) } onBlur={ handleBlur } /> @@ -234,7 +234,7 @@ const converter: Transformer, InternshipFormValues, Interns nip: form.companyNip, offices: [], }, - hours: form.hours as number, + hours: form.hours ? form.hours : 0, type: form.kind as InternshipType, } } @@ -299,6 +299,8 @@ export const InternshipForm: React.FunctionComponent = () => { const internship = converter.reverseTransform(values, { internship: initialInternship as Internship }); const update = internshipRegistrationUpdateTransformer.transform(internship); + console.log(update); + api.internship.update(update); // history.push(route("home")) @@ -308,8 +310,8 @@ export const InternshipForm: React.FunctionComponent = () => { const { submitForm, validateForm } = useFormikContext(); const handleSubmitConfirmation = async () => { - const errors = await validateForm(); - + // const errors = await validateForm(); + const errors = {}; if (Object.keys(errors).length == 0) { setConfirmDialogOpen(true); } else { diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 0c96203..1626d03 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -7,7 +7,9 @@ import { route } from "@/routing"; import React, { useState } from "react"; import { Plan } from "@/data"; import { useTranslation } from "react-i18next"; -import { InternshipPlanActions, useDispatch } from "@/state/actions"; +import { useDispatch } from "@/state/actions"; +import { UploadType } from "@/api/upload"; +import api from "@/api"; export const PlanForm = () => { const { t } = useTranslation(); @@ -18,7 +20,8 @@ export const PlanForm = () => { const history = useHistory(); const handleSubmit = () => { - dispatch({ type: InternshipPlanActions.Send, plan }); + api.upload.create(UploadType.Ipp, null as any); + // dispatch({ type: InternshipPlanActions.Send, plan }); history.push(route("home")) } diff --git a/src/forms/student.tsx b/src/forms/student.tsx index 189c3c4..efc4842 100644 --- a/src/forms/student.tsx +++ b/src/forms/student.tsx @@ -33,7 +33,7 @@ export const StudentForm = () => { />
- + skontaktuj się z opiekunem }> diff --git a/src/middleware.tsx b/src/middleware.tsx index 166de95..621d234 100644 --- a/src/middleware.tsx +++ b/src/middleware.tsx @@ -5,21 +5,21 @@ import { Redirect } from "react-router-dom"; import React from "react"; import { UserState } from "@/state/reducer/user"; -export const isReadyMiddleware: Middleware = next => isLoggedInMiddleware(() => { +export const isReadyMiddleware: Middleware = Next => isLoggedInMiddleware(() => { const ready = useSelector(isReady); if (ready) { - return <>{ next() }; + return ; } return ; }) -export const isLoggedInMiddleware: Middleware = next => { +export const isLoggedInMiddleware: Middleware = Next => { const user = useSelector(state => state.user) as UserState; if (user.loggedIn) { - return <>{ next() }; + return ; } return ; diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index 939998b..1b3a814 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -115,10 +115,6 @@ export const InternshipProposalPreviewPage = () => { { proposal && } - - + + diff --git a/src/pages/user/profile.tsx b/src/pages/user/profile.tsx index 5ebc8ba..54bca0e 100644 --- a/src/pages/user/profile.tsx +++ b/src/pages/user/profile.tsx @@ -47,7 +47,7 @@ export const UserProfilePage = () => { - diff --git a/src/state/reducer/edition.ts b/src/state/reducer/edition.ts index 93847c2..43084d4 100644 --- a/src/state/reducer/edition.ts +++ b/src/state/reducer/edition.ts @@ -1,15 +1,19 @@ import { Edition } from "@/data/edition"; import { EditionAction, EditionActions } from "@/state/actions/edition"; import { editionSerializationTransformer, Serializable } from "@/serialization"; +import { LoginAction, LogoutAction, UserActions } from "@/state/actions"; export type EditionState = Serializable | null; const initialEditionState: EditionState = null; -const editionReducer = (state: EditionState = initialEditionState, action: EditionAction): EditionState => { +const editionReducer = (state: EditionState = initialEditionState, action: EditionAction | LogoutAction | LoginAction): EditionState => { switch (action.type) { case EditionActions.Set: return editionSerializationTransformer.transform(action.edition); + case UserActions.Login: + case UserActions.Logout: + return initialEditionState; } return state;