From ff2e9c8b82aac24543d017e5006c1de0614b4eba Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> 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<any, any> = next => { +export const isReadyMiddleware: Middleware<any, any> = next => isLoggedInMiddleware(() => { const ready = useSelector(isReady); if (ready) { @@ -12,4 +13,14 @@ export const isReadyMiddleware: Middleware<any, any> = next => { } return <Redirect to={ route("edition_pick") } />; +}) + +export const isLoggedInMiddleware: Middleware<any, any> = next => { + const user = useSelector<AppState>(state => state.user) as UserState; + + if (user.loggedIn) { + return next(); + } + + return <Redirect to={ route("login") } />; } 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<TArgs extends any[]>(middleware: Middleware<a } export const routes: Route[] = [ - { name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isReadyMiddleware ] }, + { name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isLoggedInMiddleware ] }, // edition - { name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/> }, - { name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/> }, + { name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] }, + { name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/>, middlewares: [ isLoggedInMiddleware ] }, // internship { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/>, middlewares: [ isReadyMiddleware ] }, { name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/>, middlewares: [ isReadyMiddleware ] }, - { name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/> }, + { name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/>, middlewares: [ isReadyMiddleware ] }, // user { name: "user_login", path: "/user/login", content: () => <UserLoginPage/> }, 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<UserState, UserAction> = (state = initialUserState, action) => { +const userReducer = (state: UserState = initialUserState, action: UserAction): UserState => { switch (action.type) { case UserActions.Login: return { -- 2.45.2 From 411603e3a1859a5219f7c279e0c9bdc3aee9964e Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> 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<Student> { return studentDtoTransfer.transform(dto); } +export async function update(student: Student): Promise<Student> { + 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<string> { - const response = await axios.get<string>(LOGIN_ENDPOINT, { params: { code }}); +export async function login(code?: string): Promise<string> { + const response = code + ? await axios.get<string>(LOGIN_ENDPOINT, { params: { code }}) + : await axios.get<string>(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 <div className={ classNames("proposal", classes.root) }> <div> - <Typography className="proposal__primary">{ proposal.intern.name } { proposal.intern.surname }</Typography> - <Typography className="proposal__secondary"> - { t('internship.intern.semester', { semester: proposal.intern.semester }) } - { ", " } - { t('internship.intern.album', { album: proposal.intern.albumNumber }) } - </Typography> + <StudentPreview student={ proposal.intern } /> </div> <Section> 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<Nullable<Student>, StudentFormValues, { current: Student }> = { + transform(subject: Nullable<Student>, 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<Student> { + 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<StudentFormValues>({ + 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 <form onSubmit={ handleSubmit }> + <Typography variant="subtitle1">{ t("forms.student.sections.personal") }</Typography> + <Grid container> + <Grid item md={ 6 }> + <Field component={ TextFieldFormik } + name="firstName" + label={ t("forms.student.fields.first-name") } + fullWidth + /> + </Grid> + <Grid item md={ 6 }> + <Field component={ TextFieldFormik } + name="lastName" + label={ t("forms.student.fields.last-name") } + fullWidth + /> + </Grid> + <Grid item> + <Field component={ TextFieldFormik } + name="email" + label={ t("forms.student.fields.email") } + fullWidth + /> + </Grid> + </Grid> + <Typography variant="subtitle1">{ t("forms.student.sections.studies")}</Typography> + <Grid container> + <Grid item md={ 6 }> + <Field component={ TextFieldFormik } + name="albumNumber" + label={ t("forms.student.fields.album-number") } + fullWidth + /> + </Grid> + <Grid item md={ 6 }> + <Field component={ TextFieldFormik } + name="semester" + label={ t("forms.student.fields.semester") } + fullWidth + /> + </Grid> + </Grid> + <Actions> + <Button variant="contained" type="submit" color="primary">{ t("save") }</Button> + </Actions> + </form> + } + + return <Formik initialValues={ initialValues } onSubmit={ handleFormSubmit } validationSchema={ validationSchema }> + <InnerForm /> + </Formik> +} + +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<AppState, Student | null>( + state => state.student +) + +export const useCurrentEdition = () => useSelector<AppState, Edition | null>( + 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<any, any> = next => isLoggedInMiddlew const ready = useSelector(isReady); if (ready) { - return next(); + return <>{ next() }</>; } return <Redirect to={ route("edition_pick") } />; @@ -19,8 +19,8 @@ export const isLoggedInMiddleware: Middleware<any, any> = next => { const user = useSelector<AppState>(state => state.user) as UserState; if (user.loggedIn) { - return next(); + return <>{ next() }</>; } - return <Redirect to={ route("login") } />; + return <Redirect to={ route("user_login") } />; } 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) => { </Box> } -Page.Header = ({ children, maxWidth = false, ...props }: PageHeaderProps) => +Page.Header = ({ children, maxWidth = undefined, ...props }: PageHeaderProps) => <section {...props} className={classNames("page__header", props.className)}> <Container maxWidth={ maxWidth }> { 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<AppState, Student | null>(state => state.student); - const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + const deadlines = useDeadlines(); const insurance = useSelector<AppState, InsuranceState>(root => root.insurance); - const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); - - useEffect(() => void api.edition.available()) - if (!student) { return <Redirect to={ route("user_login") }/>; } function *getSteps() { - yield <Step label={ t('steps.personal-data.header') } completed={ missingStudentData.length === 0 } until={ deadlines.personalData } key="personal-data"> - { missingStudentData.length > 0 && <> - <p>{ t('steps.personal-data.info') }</p> - - <ul> - { missingStudentData.map(field => <li key={ field }>{ t(`student.${ field }`) }</li>) } - </ul> - - <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }> - { t('steps.personal-data.form') } - </Button> - </> } - </Step>; - + yield <StudentStep key="student"/>; yield <ProposalStep key="proposal"/>; yield <PlanStep key="plan"/>; 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<AppState, InsuranceState>(root => root.insurance); - const deadline = useSelector<AppState, Moment | undefined>(state => getEditionDeadlines(state.edition as Edition).insurance); // edition cannot be null at this point + const deadline = useDeadlines().insurance; + const { t } = useTranslation(); - return <Step label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }> + return <Step { ...props } label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }> <p>{ t(`steps.insurance.instructions`) }</p> <Actions> <ContactAction /> 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<AppState, SubmissionStatus>(state => getSubmissionStatus(state.plan)); @@ -74,7 +74,7 @@ export const PlanStep = (props: StepProps) => { const submission = useSelector<AppState, SubmissionState>(state => state.plan); const status = getSubmissionStatus(submission); - const deadlines = useSelector<AppState, Deadlines>(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<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal)); @@ -69,7 +69,7 @@ export const ProposalStep = (props: StepProps) => { const submission = useSelector<AppState, SubmissionState>(state => state.proposal); const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal)); - const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + 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<AppState, Student | null>(state => state.student); + const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); + const deadlines = useDeadlines(); + + return <Step {...props} label={ t('steps.personal-data.header') } completed={ missingStudentData.length === 0 } until={ deadlines.personalData }> + { missingStudentData.length > 0 ? <> + <p>{ t('steps.personal-data.info') }</p> + + <ul> + { missingStudentData.map(field => <li key={ field }>{ t(`student.${ field }`) }</li>) } + </ul> + + <Button to={ route("user_fill") } variant="contained" color="primary" component={ RouterLink }> + { t('steps.personal-data.actions.form') } + </Button> + </> : <> + <p>{ t('steps.personal-data.all-filled') }</p> + <Actions> + <Button to={ route("user_profile") } variant="outlined" color="primary" component={ RouterLink } startIcon={ <AccountDetails /> }> + { t('steps.personal-data.actions.info') } + </Button> + </Actions> + </> } + </Step> +} 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<AppState>(state => state.student) as Student; + + const { t } = useTranslation(); + + return <Page> + <Page.Header maxWidth="md"> + <Page.Breadcrumbs> + <Link component={ RouterLink } to={ route("home") }>{ t("pages.my-internship.header") }</Link> + <Typography color="textPrimary">{ t("pages.user-fill.title") }</Typography> + </Page.Breadcrumbs> + <Page.Title>{ t("pages.user-fill.title") }</Page.Title> + </Page.Header> + <Container> + <StudentForm student={ student } /> + </Container> + </Page> +} + +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<Action>, getState: () => AppState): Promise<void> => { +const authorizeUser = (code?: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => { 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 <> + <Typography className="proposal__primary">{ student.name } { student.surname }</Typography> + <Typography className="proposal__secondary"> + { t('internship.intern.semester', { semester: student.semester }) } + { ", " } + { t('internship.intern.album', { album: student.albumNumber }) } + </Typography> + </>; +} + +export const UserProfilePage = () => { + const { t } = useTranslation(); + + const student = useCurrentStudent() as Student; + const spacing = useVerticalSpacing(3); + + return <Page> + <Page.Header> + <Page.Breadcrumbs> + <Link component={ RouterLink } to={ route("home") }>{ t("pages.my-internship.header") }</Link> + <Typography color="textPrimary">{ t("pages.user-profile.title") }</Typography> + </Page.Breadcrumbs> + <Page.Title>{ t('pages.user-profile.title') }</Page.Title> + </Page.Header> + <Container className={ spacing.root }> + <Paper> + <Box p={2}> + <StudentPreview student={ student } /> + </Box> + </Paper> + <Actions> + <Button variant="contained" component={ RouterLink } to={ route("home") }> + { t('go-back') } + </Button> + </Actions> + </Container> + </Page> +} + +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<TArgs extends any[]>(middleware: Middleware<a } export const routes: Route[] = [ - { name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isLoggedInMiddleware ] }, + { name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isReadyMiddleware ] }, // edition { name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] }, @@ -43,6 +45,8 @@ export const routes: Route[] = [ // user { name: "user_login", path: "/user/login", content: () => <UserLoginPage/> }, + { name: "user_fill", path: "/user/data", content: () => <UserFillPage/>, middlewares: [ isLoggedInMiddleware ] }, + { name: "user_profile", path: "/user/profile", content: () => <UserProfilePage/>, middlewares: [ isLoggedInMiddleware ] }, // fallback route for 404 pages { name: "fallback", path: "*", content: () => <FallbackPage/> } 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<Edition> = { + transform(subject: Edition, context?: unknown): Serializable<Edition> { + 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<Edition>, 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<Edition> | 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": '' -- 2.45.2 From 8b2523572d6e515647ac1752fa771a4c7a64495e Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> 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<Company[]> { + const companies = await axios.get<Company[]>(query(COMPANY_SEARCH_ENDPOINT, { Name: name })); + + return companies.data; +} + +export async function offices(id: string): Promise<Office[]> { + const response = await axios.get<Office[]>(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<Internship>, Nullable<InternshipRegistrationUpdate>> = { + transform(subject: Nullable<Internship>, context?: unknown): Nullable<InternshipRegistrationUpdate> { + 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<InternshipTypeDTO, InternshipType> = { + 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<boolean> { 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<Edition | null> { return editionDtoTransformer.transform(dto); } + +export async function current(): Promise<Edition> { + const response = await axios.get<EditionDTO>(EDITION_CURRENT_ENDPOINT); + const dto = response.data; + + return editionDtoTransformer.transform(dto); +} + +export async function login(key: string): Promise<string> { + const response = await axios.post<string>(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<InternshipRegistrationUpdate>): Promise<boolean> { + 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<InternshipType[]> { + const response = await axios.get<InternshipTypeDTO[]>(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<string> { const response = code - ? await axios.get<string>(LOGIN_ENDPOINT, { params: { code }}) + ? await axios.post<string>(LOGIN_ENDPOINT, JSON.stringify(code), { headers: { 'Content-Type': 'application/json' } }) : await axios.get<string>(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) => { <Section> <Label>{ t('internship.sections.kind') }</Label> - <Typography className="proposal__primary">{ internshipTypeLabels[proposal.type].label }</Typography> + <Typography className="proposal__primary">{ proposal.type.label.pl }</Typography> </Section> <Section> 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<string>, + description?: Multilingual<string>, } 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<any>) => ( <div className="company-item" { ...props }> @@ -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<Office[]>([]); 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<InternshipFormValues>(); const { t } = useTranslation(); + const [input, setInput] = useState<string>(""); + const [companies, setCompanies] = useState<Company[]>([]); + 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 = () => { <> <Grid container> <Grid item> - <Autocomplete options={ sampleCompanies } + <Autocomplete options={ companies } getOptionLabel={ option => typeof option === "string" ? option : option.name } renderOption={ company => <CompanyItem company={ company }/> } renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.company-name") } fullWidth error={ touched.companyName && !!errors.companyName } helperText={ touched.companyName && errors.companyName }/> } onChange={ handleCompanyChange } value={ values.company || values.companyName } freeSolo + onInputChange={ (_, value) => setInput(value) } /> </Grid> <Grid item md={ 4 }> 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<any>) => { - const info = internshipTypeLabels[type]; - +export const InternshipTypeItem = ({ internshipType: type, ...props }: { internshipType: InternshipType } & HTMLProps<any>) => { return ( <div className="internship=type-item" { ...props }> - <div>{ info.label }</div> - { info.description && <Typography variant="caption">{ info.description }</Typography> } + <div>{ type.label.pl }</div> + { type.description && <Typography variant="caption">{ type.description.pl }</Typography> } </div> ) } @@ -88,25 +87,27 @@ const InternshipProgramForm = () => { const { t } = useTranslation(); const { values, handleBlur, setFieldValue, errors } = useFormikContext<InternshipFormValues>(); + const types = useInternshipTypes(); + return ( <Grid container> <Grid item md={ 4 }> <Autocomplete renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.kind") } fullWidth error={ !!errors.kind } helperText={ errors.kind }/> } - getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label } - renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> } - options={ Object.values(InternshipType) as InternshipType[] } + getOptionLabel={ (option: InternshipType) => option.label.pl } + renderOption={ (option: InternshipType) => <InternshipTypeItem internshipType={ option }/> } + options={ types } disableClearable value={ values.kind || undefined } onChange={ (_, value) => setFieldValue("kind", value) } onBlur={ handleBlur } /> </Grid> - <Grid item md={ 8 }> - { - values.kind === InternshipType.Other && - <Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } /> - } - </Grid> + {/*<Grid item md={ 8 }>*/} + {/* {*/} + {/* values.kind === InternshipType.Other &&*/} + {/* <Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } />*/} + {/* }*/} + {/*</Grid>*/} </Grid> ) } @@ -240,19 +241,20 @@ const converter: Transformer<Nullable<Internship>, InternshipFormValues, Interns } export const InternshipForm: React.FunctionComponent = () => { + const student = useCurrentStudent(); + const initialInternship = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, office: null, company: null, mentor: null, - intern: sampleStudent + intern: student }); - const edition = useSelector<AppState, Edition>(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<InternshipFormValues>(); + const course = useCurrentEdition()?.course as Course; return <> <Grid container> @@ -26,8 +27,8 @@ export const StudentForm = () => { <Autocomplete getOptionLabel={ (course: Course) => course.name } renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.course") } fullWidth/> } - options={[ sampleCourse ]} - value={ student.course } + options={[ course ]} + value={ course } disabled /> </Grid> 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<InternshipType[]>([]); + + 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<InsuranceState, InsuranceAction | Interns case InternshipProposalActions.Send: return { ...state, - required: [ - InternshipType.FreeApprenticeship, - InternshipType.FreeInternship, - InternshipType.PaidApprenticeship, - InternshipType.GraduateInternship, - ].includes(action.internship.type) } case InsuranceActions.Signed: diff --git a/yarn.lock b/yarn.lock index f385c39..e5383ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2258,6 +2258,11 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.2, browserslist@^4. node-releases "^1.1.53" pkg-up "^2.0.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -3433,6 +3438,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -5219,6 +5231,22 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5299,6 +5327,23 @@ jss@^10.0.3, jss@^10.3.0: is-in-browser "^1.1.3" tiny-warning "^1.0.2" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -5432,6 +5477,36 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5447,6 +5522,11 @@ lodash.omit@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" integrity sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA= +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" -- 2.45.2 From 263be22901cd81d3591d909a1d5e2c19c6ce6a3c Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> 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<Internship>, Nullable<InternshipRegistrationUpdate>> = { @@ -21,7 +47,7 @@ export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable start: subject?.startDate?.toISOString() || null, end: subject?.endDate?.toISOString() || null, type: parseInt(subject?.type?.id || "0"), - mentor: subject.mentor, + mentor: mentorDtoTransformer.reverseTransform(subject.mentor as Mentor), company: { id: subject?.company?.id as string, branchOffice: { @@ -31,3 +57,22 @@ export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable } } } + +export const internshipRegistrationDtoTransformer: OneWayTransformer<InternshipRegistrationDTO, Internship> = { + 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<MentorDTO, Mentor> = { + 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<InternshipRegistrationUpdate>) return true; } + +export async function get(): Promise<InternshipInfoDTO> { + const response = await axios.get<InternshipInfoDTO>(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<AppState, InsuranceState>(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 <Redirect to={ route("user_login") }/>; 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<InternshipProposalActions.Receive> { + internship: Internship; + state: SubmissionState, } export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> { 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<Internship> | 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; } -- 2.45.2 From 5879efc978a73ce5e010025ccf0aaeb0e9c07295 Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> 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<Nullable end: subject?.endDate?.toISOString() || null, type: parseInt(subject?.type?.id || "0"), mentor: mentorDtoTransformer.reverseTransform(subject.mentor as Mentor), - company: { + company: subject?.company?.id ? { id: subject?.company?.id as string, - branchOffice: { - id: subject?.office?.id - }, - } + branchOffice: subject?.office?.id + ? reference(subject?.office) as Identifiable + : subject?.office?.address as NewBranchOffice, + } : { + name: subject?.company?.name as string, + nip: subject?.company?.nip as string, + branchOffice: subject?.office?.address as NewBranchOffice + }, + hours: subject?.hours, } } } @@ -68,7 +86,7 @@ export const internshipRegistrationDtoTransformer: OneWayTransformer<InternshipR startDate: momentSerializationTransformer.reverseTransform(dto.start) as Moment, endDate: momentSerializationTransformer.reverseTransform(dto.end) as Moment, type: internshipTypeDtoTransformer.transform(dto.type), - hours: 0, + hours: dto.declaredHours, isAccepted: dto.state === SubmissionState.Accepted, lengthInWeeks: 0, program: [], diff --git a/src/api/index.ts b/src/api/index.ts index cb20750..5f76ba3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,6 +10,7 @@ import * as student from "./student"; import * as type from "./type"; import * as companies from "./companies"; import * as internship from "./internship"; +import * as upload from "./upload"; export const axios = Axios.create({ baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/", @@ -40,6 +41,7 @@ const api = { type, companies, internship, + upload } export default api; diff --git a/src/api/internship.ts b/src/api/internship.ts index cc4dda0..5741950 100644 --- a/src/api/internship.ts +++ b/src/api/internship.ts @@ -2,10 +2,11 @@ import { InternshipInfoDTO, InternshipRegistrationUpdate } from "@/api/dto/inter import { axios } from "@/api/index"; import { Nullable } from "@/helpers"; -const INTERNSHIP_ENDPOINT = '/internshipRegistration'; +const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration'; +const INTERNSHIP_ENDPOINT = '/internship'; export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<boolean> { - 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<Document>(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 /> </Grid> @@ -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 => <CompanyItem company={ company }/> } renderInput={ props => <TextField { ...props } label={ t("forms.internship.fields.company-name") } fullWidth error={ touched.companyName && !!errors.companyName } helperText={ touched.companyName && errors.companyName }/> } - 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) => <InternshipTypeItem internshipType={ option }/> } 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<Nullable<Internship>, 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 = () => { /> </Grid> <Grid item md={3}> - <TextField label={ t("forms.internship.fields.semester") } value={ student.semester } disabled fullWidth/> + <TextField label={ t("forms.internship.fields.semester") } value={ student.semester || "" } disabled fullWidth/> </Grid> <Grid item> <Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }> 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<any, any> = next => isLoggedInMiddleware(() => { +export const isReadyMiddleware: Middleware<any, any> = Next => isLoggedInMiddleware(() => { const ready = useSelector(isReady); if (ready) { - return <>{ next() }</>; + return <Next />; } return <Redirect to={ route("edition_pick") } />; }) -export const isLoggedInMiddleware: Middleware<any, any> = next => { +export const isLoggedInMiddleware: Middleware<any, any> = Next => { const user = useSelector<AppState>(state => state.user) as UserState; if (user.loggedIn) { - return <>{ next() }</>; + return <Next />; } return <Redirect to={ route("user_login") } />; 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 && <ProposalPreview proposal={ proposal } /> } <Actions> - <Button component={ RouterLink } to={ route("home") } variant="contained" color="primary"> - { t('go-back') } - </Button> - <ButtonGroup color="primary" variant="contained"> <Button onClick={ handleAcceptWithoutComment } startIcon={ <StickerCheckOutline /> }> { t('accept-without-comments') } @@ -134,6 +130,10 @@ export const InternshipProposalPreviewPage = () => { <Button onClick={ handleDiscardAction } color="secondary" startIcon={ <StickerRemoveOutline /> }> { t('discard') } </Button> + + <Button component={ RouterLink } to={ route("home") }> + { t('go-back') } + </Button> </Actions> </Container> <Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md"> 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 = () => { </Box> </Paper> <Actions> - <Button variant="contained" component={ RouterLink } to={ route("home") }> + <Button component={ RouterLink } to={ route("home") }> { t('go-back') } </Button> </Actions> 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<Edition> | 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; -- 2.45.2