diff --git a/src/api/dto/edition.ts b/src/api/dto/edition.ts index 94c3101..2753f39 100644 --- a/src/api/dto/edition.ts +++ b/src/api/dto/edition.ts @@ -20,7 +20,7 @@ export interface EditionTeaserDTO extends Identifiable { export const editionTeaserDtoTransformer: OneWayTransformer> = { transform(subject: EditionTeaserDTO, context?: undefined): Subset { - return { + return subject && { id: subject.id, startDate: moment(subject.editionStart), endDate: moment(subject.editionFinish), diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts index b415db9..34c61b0 100644 --- a/src/api/dto/internship-registration.ts +++ b/src/api/dto/internship-registration.ts @@ -5,6 +5,7 @@ import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor"; import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; import { Moment } from "moment"; import { sampleStudent } from "@/provider/dummy"; +import { UploadType } from "@/api/upload"; export enum SubmissionState { Draft = "Draft", @@ -48,10 +49,17 @@ export interface InternshipRegistrationDTO extends Identifiable { declaredHours: number, } +export interface InternshipDocument extends Identifiable { + description: null, + type: UploadType, + state: SubmissionState, +} + const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id }; export interface InternshipInfoDTO { internshipRegistration: InternshipRegistrationDTO; + documentation: InternshipDocument[], } export const internshipRegistrationUpdateTransformer: OneWayTransformer, Nullable> = { diff --git a/src/api/edition.ts b/src/api/edition.ts index 5f7951e..b420620 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -1,7 +1,8 @@ import { axios } from "@/api/index"; import { Edition } from "@/data/edition"; import { prepare } from "@/routing"; -import { EditionDTO, editionDtoTransformer, editionTeaserDtoTransformer } from "@/api/dto/edition"; +import { EditionDTO, editionDtoTransformer, EditionTeaserDTO, editionTeaserDtoTransformer } from "@/api/dto/edition"; +import { Subset } from "@/helpers"; const EDITIONS_ENDPOINT = "/editions"; const EDITION_INFO_ENDPOINT = "/editions/:key"; @@ -29,11 +30,15 @@ export async function join(key: string): Promise { } } -export async function get(key: string): Promise { - const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); +export async function get(key: string): Promise | null> { + if (!key) { + return null; + } + + const response = await axios.get(prepare(EDITION_INFO_ENDPOINT, { key })); const dto = response.data; - return editionDtoTransformer.transform(dto); + return editionTeaserDtoTransformer.transform(dto); } export async function current(): Promise { diff --git a/src/api/internship.ts b/src/api/internship.ts index 5741950..961c789 100644 --- a/src/api/internship.ts +++ b/src/api/internship.ts @@ -6,15 +6,12 @@ const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration'; const INTERNSHIP_ENDPOINT = '/internship'; export async function update(internship: Nullable): Promise { - const response = await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); + await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); return true; } export async function get(): Promise { const response = await axios.get(INTERNSHIP_ENDPOINT); - - console.log(response); - return response.data; } diff --git a/src/api/upload.ts b/src/api/upload.ts index e27735a..81fa7b1 100644 --- a/src/api/upload.ts +++ b/src/api/upload.ts @@ -1,5 +1,6 @@ -import { Identifiable } from "@/data"; import { axios } from "@/api/index"; +import { InternshipDocument } from "@/api/dto/internship-registration"; +import { prepare } from "@/routing"; export enum UploadType { Ipp = "IppScan", @@ -10,14 +11,15 @@ export enum UploadType { 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) { + const response = await axios.post(CREATE_DOCUMENT_ENDPOINT, { type }); + return response.data; } -export async function create(type: UploadType, content: File) -{ - const response = await axios.post(CREATE_DOCUMENT_ENDPOINT, { type }); +export async function upload(document: InternshipDocument, file: File) { + const data = new FormData(); + data.append('documentScan', file) - console.log(response.data); + const response = await axios.put(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }), data); + return true; } diff --git a/src/data/internship.ts b/src/data/internship.ts index 29733f4..d3e946e 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -26,10 +26,6 @@ export interface Internship extends Identifiable { office: Office; } -export interface Plan extends Identifiable { - -} - export interface Mentor { name: string; surname: string; diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 1626d03..b7f2021 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -5,24 +5,39 @@ import { Actions } from "@/components"; import { Link as RouterLink, useHistory } from "react-router-dom"; import { route } from "@/routing"; import React, { useState } from "react"; -import { Plan } from "@/data"; import { useTranslation } from "react-i18next"; -import { useDispatch } from "@/state/actions"; +import { InternshipPlanActions, useDispatch } from "@/state/actions"; import { UploadType } from "@/api/upload"; import api from "@/api"; +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { InternshipDocument } from "@/api/dto/internship-registration"; export const PlanForm = () => { const { t } = useTranslation(); - const [plan, setPlan] = useState({}); + const [file, setFile] = useState(); const dispatch = useDispatch(); const history = useHistory(); - const handleSubmit = () => { - api.upload.create(UploadType.Ipp, null as any); - // dispatch({ type: InternshipPlanActions.Send, plan }); - history.push(route("home")) + const document = useSelector(state => state.plan.document); + + const handleSubmit = async () => { + if (!file) { + return; + } + + let destination: InternshipDocument = document as any; + + if (!destination) { + destination = await api.upload.create(UploadType.Ipp); + dispatch({ type: InternshipPlanActions.Send, document: destination }); + } + + await api.upload.upload(destination, file); + + history.push("/"); } return @@ -35,7 +50,7 @@ export const PlanForm = () => { - + setFile(files[0]) }/> { t('forms.plan.dropzone-help') } diff --git a/src/helpers.ts b/src/helpers.ts index fc5e38e..34dae24 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -12,3 +12,17 @@ export interface DOMEvent extends Event { export function delay(time: number) { return new Promise(resolve => setTimeout(resolve, time)); } + +export function throttle(decorated: (...args: TArgs) => void, time: number = 150) { + let timeout: number | undefined; + return function (this: any, ...args: TArgs): void { + if (typeof timeout !== 'undefined') { + window.clearTimeout(timeout); + } + + timeout = window.setTimeout(() => { + timeout = undefined; + decorated.call(this, ...args); + }, time); + } +} diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 8b83d9b..d7fbf9b 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -30,6 +30,8 @@ export function useAsync(supplier: Promise | (() => Promise< setLoading(false); } }).catch(error => { + console.error(error) + if (semaphore.value == myMagicNumber) { setError(error); setLoading(false); diff --git a/src/hooks/useDebouncedEffect.ts b/src/hooks/useDebouncedEffect.ts new file mode 100644 index 0000000..d50c368 --- /dev/null +++ b/src/hooks/useDebouncedEffect.ts @@ -0,0 +1,10 @@ +import { DependencyList, EffectCallback, useCallback, useEffect } from "react"; + +export function useDebouncedEffect(effect: EffectCallback, deps: DependencyList, time: number = 150) { + const callback = useCallback(effect, deps); + + useEffect(() => { + const timeout = window.setTimeout(() => callback(), time); + return () => window.clearTimeout(timeout); + }, [ callback, time ]) +} diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx index ea04030..635d225 100644 --- a/src/pages/edition/pick.tsx +++ b/src/pages/edition/pick.tsx @@ -11,7 +11,27 @@ import api from "@/api"; import { Section } from "@/components/section"; import { useVerticalSpacing } from "@/styles"; import { Alert } from "@material-ui/lab"; -import { EditionActions, useDispatch, UserActions } from "@/state/actions"; +import { AppDispatch, EditionActions, useDispatch, UserActions } from "@/state/actions"; + +export const loginToEdition = (id: string) => async (dispatch: AppDispatch) => { + const token = await api.edition.login(id); + + if (!token) { + return; + } + + await dispatch({ + type: UserActions.Login, + token, + }) + + const edition = await api.edition.current(); + + dispatch({ + type: EditionActions.Set, + edition + }) +} export const PickEditionPage = () => { const { t } = useTranslation(); @@ -23,24 +43,7 @@ export const PickEditionPage = () => { const classes = useVerticalSpacing(3); const pickEditionHandler = (id: string) => async () => { - const token = await api.edition.login(id); - - if (!token) { - return; - } - - await dispatch({ - type: UserActions.Login, - token, - }) - - const edition = await api.edition.current(); - - dispatch({ - type: EditionActions.Set, - edition - }) - + await dispatch(loginToEdition(id)); history.push("/"); } diff --git a/src/pages/edition/register.tsx b/src/pages/edition/register.tsx index 7c69834..634636d 100644 --- a/src/pages/edition/register.tsx +++ b/src/pages/edition/register.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Page } from "@/pages/base"; import { useTranslation } from "react-i18next"; import { Box, Button, CircularProgress, Container, TextField, Typography } from "@material-ui/core"; @@ -9,24 +9,33 @@ import { Edition } from "@/data/edition"; import { useAsyncState } from "@/hooks"; import { Label, Section } from "@/components/section"; import { Alert } from "@material-ui/lab"; +import { Subset } from "@/helpers"; +import { useDispatch } from "@/state/actions"; +import { loginToEdition } from "@/pages/edition/pick"; +import { useHistory } from "react-router-dom"; +import { useDebouncedEffect } from "@/hooks/useDebouncedEffect"; export const RegisterEditionPage = () => { const { t } = useTranslation(); const [key, setKey] = useState(""); - const [{ value: edition, isLoading }, setEdition] = useAsyncState(undefined); + const [{ value: edition, isLoading }, setEdition] = useAsyncState | null>(undefined); const classes = useVerticalSpacing(3); + const dispatch = useDispatch(); + const history = useHistory(); - useEffect(() => { + useDebouncedEffect(() => { setEdition(api.edition.get(key)); }, [ key ]) - const handleRegister = () => { + const handleRegister = async () => { try { - api.edition.join(key); + await api.edition.join(key); + await dispatch(loginToEdition(key)); + history.push("/"); } catch (error) { - + console.log(error); } } @@ -37,7 +46,7 @@ export const RegisterEditionPage = () => { const Edition = () => edition ?
- { edition.course.name } + { edition.course?.name } { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index e204644..6141ecd 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -15,8 +15,29 @@ 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 { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions"; import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration"; +import { UploadType } from "@/api/upload"; + +export const updateInternshipInfo = async (dispatch: AppDispatch) => { + const internship = await api.internship.get(); + + dispatch({ + type: InternshipProposalActions.Receive, + state: internship.internshipRegistration.state, + internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration), + }) + + const plan = internship.documentation.find(doc => doc.type === UploadType.Ipp); + + if (plan) { + dispatch({ + type: InternshipPlanActions.Receive, + document: plan, + state: plan.state, + }) + } +} export const MainPage = () => { const { t } = useTranslation(); @@ -28,15 +49,7 @@ export const MainPage = () => { const dispatch = useDispatch(); useEffect(() => { - (async () => { - const internship = await api.internship.get(); - - dispatch({ - type: InternshipProposalActions.Receive, - state: internship.internshipRegistration.state, - internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration), - }) - })() + dispatch(updateInternshipInfo); }, []) if (!student) { diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 043b99b..0fc0534 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -37,7 +37,8 @@ export const Actions = { ...StudentActions, } export type Actions = typeof Actions; +export type AppDispatch = ThunkDispatch; -export const useDispatch = () => useReduxDispatch>() +export const useDispatch = () => useReduxDispatch() export default Actions; diff --git a/src/state/actions/plan.ts b/src/state/actions/plan.ts index fcabfbe..90a0cdb 100644 --- a/src/state/actions/plan.ts +++ b/src/state/actions/plan.ts @@ -1,4 +1,3 @@ -import { Plan } from "@/data"; import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, @@ -7,6 +6,8 @@ import { SendSubmissionAction } from "@/state/actions/submission"; +import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration"; + export enum InternshipPlanActions { Send = "SEND_PLAN", Save = "SAVE_PLAN", @@ -16,7 +17,7 @@ export enum InternshipPlanActions { } export interface SendPlanAction extends SendSubmissionAction { - plan: Plan; + document: InternshipDocument; } export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction { @@ -26,10 +27,12 @@ export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction } export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction { + document: InternshipDocument; + state: SubmissionState; } export interface SavePlanAction extends SaveSubmissionAction { - plan: Plan; + document: InternshipDocument; } export type InternshipPlanAction diff --git a/src/state/reducer/plan.ts b/src/state/reducer/plan.ts index d092583..b89f661 100644 --- a/src/state/reducer/plan.ts +++ b/src/state/reducer/plan.ts @@ -1,5 +1,4 @@ -import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions"; -import { Plan } from "@/data"; +import { InternshipPlanAction, InternshipPlanActions, InternshipProposalActions } from "@/state/actions"; import { Serializable } from "@/serialization/types"; import { createSubmissionReducer, @@ -10,19 +9,18 @@ import { } from "@/state/reducer/submission"; import { Reducer } from "react"; import { SubmissionAction } from "@/state/actions/submission"; +import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration"; export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & { - plan: Serializable | null; + document: Serializable | null; } const defaultInternshipPlanState: InternshipPlanState = { ...defaultDeanApprovalsState, ...defaultSubmissionState, - plan: null, + document: null, } -export const getInternshipPlan = ({ plan }: InternshipPlanState): Plan | null => plan; - const internshipPlanSubmissionReducer: Reducer = createSubmissionReducer({ [InternshipPlanActions.Approve]: SubmissionAction.Approve, [InternshipPlanActions.Decline]: SubmissionAction.Decline, @@ -39,8 +37,20 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla case InternshipPlanActions.Send: return { ...state, - plan: action.plan, + document: action.document, } + case InternshipPlanActions.Receive: + return { + ...state, + accepted: action.state === ApiSubmissionState.Accepted, + sent: [ + ApiSubmissionState.Accepted, + ApiSubmissionState.Rejected, + ApiSubmissionState.Submitted + ].includes(action.state), + document: action.document, + } + default: return state; }