Merge pull request 'feature/student_data_form' (#18) from feature/student_data_form into master

This commit is contained in:
Kacper Donat 2020-11-06 20:03:36 +01:00
commit 3b90fb7c61
17 changed files with 176 additions and 81 deletions

View File

@ -20,7 +20,7 @@ export interface EditionTeaserDTO extends Identifiable {
export const editionTeaserDtoTransformer: OneWayTransformer<EditionTeaserDTO, Subset<Edition>> = { export const editionTeaserDtoTransformer: OneWayTransformer<EditionTeaserDTO, Subset<Edition>> = {
transform(subject: EditionTeaserDTO, context?: undefined): Subset<Edition> { transform(subject: EditionTeaserDTO, context?: undefined): Subset<Edition> {
return { return subject && {
id: subject.id, id: subject.id,
startDate: moment(subject.editionStart), startDate: moment(subject.editionStart),
endDate: moment(subject.editionFinish), endDate: moment(subject.editionFinish),

View File

@ -5,6 +5,7 @@ import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor";
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
import { Moment } from "moment"; import { Moment } from "moment";
import { sampleStudent } from "@/provider/dummy"; import { sampleStudent } from "@/provider/dummy";
import { UploadType } from "@/api/upload";
export enum SubmissionState { export enum SubmissionState {
Draft = "Draft", Draft = "Draft",
@ -48,10 +49,17 @@ export interface InternshipRegistrationDTO extends Identifiable {
declaredHours: number, declaredHours: number,
} }
export interface InternshipDocument extends Identifiable {
description: null,
type: UploadType,
state: SubmissionState,
}
const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id }; const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id };
export interface InternshipInfoDTO { export interface InternshipInfoDTO {
internshipRegistration: InternshipRegistrationDTO; internshipRegistration: InternshipRegistrationDTO;
documentation: InternshipDocument[],
} }
export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable<Internship>, Nullable<InternshipRegistrationUpdate>> = { export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable<Internship>, Nullable<InternshipRegistrationUpdate>> = {

View File

@ -1,7 +1,8 @@
import { axios } from "@/api/index"; import { axios } from "@/api/index";
import { Edition } from "@/data/edition"; import { Edition } from "@/data/edition";
import { prepare } from "@/routing"; 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 EDITIONS_ENDPOINT = "/editions";
const EDITION_INFO_ENDPOINT = "/editions/:key"; const EDITION_INFO_ENDPOINT = "/editions/:key";
@ -10,9 +11,13 @@ const EDITION_REGISTER_ENDPOINT = "/register";
const EDITION_LOGIN_ENDPOINT = "/access/loginEdition"; const EDITION_LOGIN_ENDPOINT = "/access/loginEdition";
export async function available() { export async function available() {
const response = await axios.get(EDITIONS_ENDPOINT); try {
const response = await axios.get(EDITIONS_ENDPOINT);
return (response.data || []).map(editionTeaserDtoTransformer.transform); return (response.data || []).map(editionTeaserDtoTransformer.transform);
} catch (e) {
return [];
}
} }
export async function join(key: string): Promise<boolean> { export async function join(key: string): Promise<boolean> {
@ -25,11 +30,15 @@ export async function join(key: string): Promise<boolean> {
} }
} }
export async function get(key: string): Promise<Edition | null> { export async function get(key: string): Promise<Subset<Edition> | null> {
const response = await axios.get<EditionDTO>(prepare(EDITION_INFO_ENDPOINT, { key })); if (!key) {
return null;
}
const response = await axios.get<EditionTeaserDTO>(prepare(EDITION_INFO_ENDPOINT, { key }));
const dto = response.data; const dto = response.data;
return editionDtoTransformer.transform(dto); return editionTeaserDtoTransformer.transform(dto);
} }
export async function current(): Promise<Edition> { export async function current(): Promise<Edition> {

View File

@ -6,15 +6,12 @@ const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration';
const INTERNSHIP_ENDPOINT = '/internship'; const INTERNSHIP_ENDPOINT = '/internship';
export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<boolean> { export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<boolean> {
const response = await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship);
return true; return true;
} }
export async function get(): Promise<InternshipInfoDTO> { export async function get(): Promise<InternshipInfoDTO> {
const response = await axios.get<InternshipInfoDTO>(INTERNSHIP_ENDPOINT); const response = await axios.get<InternshipInfoDTO>(INTERNSHIP_ENDPOINT);
console.log(response);
return response.data; return response.data;
} }

View File

@ -1,5 +1,6 @@
import { Identifiable } from "@/data";
import { axios } from "@/api/index"; import { axios } from "@/api/index";
import { InternshipDocument } from "@/api/dto/internship-registration";
import { prepare } from "@/routing";
export enum UploadType { export enum UploadType {
Ipp = "IppScan", Ipp = "IppScan",
@ -10,14 +11,15 @@ export enum UploadType {
const CREATE_DOCUMENT_ENDPOINT = '/document'; const CREATE_DOCUMENT_ENDPOINT = '/document';
const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan'; const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan';
interface Document extends Identifiable { export async function create(type: UploadType) {
description?: string; const response = await axios.post<InternshipDocument>(CREATE_DOCUMENT_ENDPOINT, { type });
type: UploadType; return response.data;
} }
export async function create(type: UploadType, content: File) export async function upload(document: InternshipDocument, file: File) {
{ const data = new FormData();
const response = await axios.post<Document>(CREATE_DOCUMENT_ENDPOINT, { type }); 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;
} }

View File

@ -26,10 +26,6 @@ export interface Internship extends Identifiable {
office: Office; office: Office;
} }
export interface Plan extends Identifiable {
}
export interface Mentor { export interface Mentor {
name: string; name: string;
surname: string; surname: string;

View File

@ -5,24 +5,39 @@ import { Actions } from "@/components";
import { Link as RouterLink, useHistory } from "react-router-dom"; import { Link as RouterLink, useHistory } from "react-router-dom";
import { route } from "@/routing"; import { route } from "@/routing";
import React, { useState } from "react"; import React, { useState } from "react";
import { Plan } from "@/data";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch } from "@/state/actions"; import { InternshipPlanActions, useDispatch } from "@/state/actions";
import { UploadType } from "@/api/upload"; import { UploadType } from "@/api/upload";
import api from "@/api"; import api from "@/api";
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { InternshipDocument } from "@/api/dto/internship-registration";
export const PlanForm = () => { export const PlanForm = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [plan, setPlan] = useState<Plan>({}); const [file, setFile] = useState<File>();
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const handleSubmit = () => { const document = useSelector<AppState>(state => state.plan.document);
api.upload.create(UploadType.Ipp, null as any);
// dispatch({ type: InternshipPlanActions.Send, plan }); const handleSubmit = async () => {
history.push(route("home")) 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 <Grid container> return <Grid container>
@ -35,7 +50,7 @@ export const PlanForm = () => {
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
<DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/> <DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") } onChange={ files => setFile(files[0]) }/>
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText> <FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -12,3 +12,17 @@ export interface DOMEvent<TTarget extends EventTarget> extends Event {
export function delay(time: number) { export function delay(time: number) {
return new Promise(resolve => setTimeout(resolve, time)); return new Promise(resolve => setTimeout(resolve, time));
} }
export function throttle<TArgs extends any[]>(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);
}
}

View File

@ -30,6 +30,8 @@ export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<
setLoading(false); setLoading(false);
} }
}).catch(error => { }).catch(error => {
console.error(error)
if (semaphore.value == myMagicNumber) { if (semaphore.value == myMagicNumber) {
setError(error); setError(error);
setLoading(false); setLoading(false);
@ -40,8 +42,10 @@ export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<
useEffect(() => { useEffect(() => {
if (typeof supplier === "function") { if (typeof supplier === "function") {
setPromise(supplier()); setPromise(supplier());
} else {
setPromise(supplier);
} }
}, []) }, [ supplier ])
return { return {
isLoading, isLoading,

View File

@ -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 ])
}

View File

@ -1,5 +1,5 @@
import { Page } from "@/pages/base"; import { Page } from "@/pages/base";
import React from "react"; import React, { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Box, Button, CircularProgress, Container, Typography } from "@material-ui/core"; import { Box, Button, CircularProgress, Container, Typography } from "@material-ui/core";
import { Actions } from "@/components"; import { Actions } from "@/components";
@ -11,11 +11,31 @@ import api from "@/api";
import { Section } from "@/components/section"; import { Section } from "@/components/section";
import { useVerticalSpacing } from "@/styles"; import { useVerticalSpacing } from "@/styles";
import { Alert } from "@material-ui/lab"; 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 = () => { export const PickEditionPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { value: editions, isLoading } = useAsync(() => api.edition.available()); const { value: editions, isLoading } = useAsync(useCallback(() => api.edition.available(), []));
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
@ -23,24 +43,7 @@ export const PickEditionPage = () => {
const classes = useVerticalSpacing(3); const classes = useVerticalSpacing(3);
const pickEditionHandler = (id: string) => async () => { const pickEditionHandler = (id: string) => async () => {
const token = await api.edition.login(id); await dispatch(loginToEdition(id));
if (!token) {
return;
}
await dispatch({
type: UserActions.Login,
token,
})
const edition = await api.edition.current();
dispatch({
type: EditionActions.Set,
edition
})
history.push("/"); history.push("/");
} }

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { Page } from "@/pages/base"; import { Page } from "@/pages/base";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Box, Button, CircularProgress, Container, TextField, Typography } from "@material-ui/core"; 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 { useAsyncState } from "@/hooks";
import { Label, Section } from "@/components/section"; import { Label, Section } from "@/components/section";
import { Alert } from "@material-ui/lab"; 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 = () => { export const RegisterEditionPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [key, setKey] = useState<string>(""); const [key, setKey] = useState<string>("");
const [{ value: edition, isLoading }, setEdition] = useAsyncState<Edition | null>(undefined); const [{ value: edition, isLoading }, setEdition] = useAsyncState<Subset<Edition> | null>(undefined);
const classes = useVerticalSpacing(3); const classes = useVerticalSpacing(3);
const dispatch = useDispatch();
const history = useHistory();
useEffect(() => { useDebouncedEffect(() => {
setEdition(api.edition.get(key)); setEdition(api.edition.get(key));
}, [ key ]) }, [ key ])
const handleRegister = () => { const handleRegister = async () => {
try { try {
api.edition.join(key); await api.edition.join(key);
await dispatch(loginToEdition(key));
history.push("/");
} catch (error) { } catch (error) {
console.log(error);
} }
} }
@ -37,7 +46,7 @@ export const RegisterEditionPage = () => {
const Edition = () => edition const Edition = () => edition
? <Section> ? <Section>
<Label>{ t("forms.edition-register.edition" ) }</Label> <Label>{ t("forms.edition-register.edition" ) }</Label>
<Typography className="proposal__primary">{ edition.course.name }</Typography> <Typography className="proposal__primary">{ edition.course?.name }</Typography>
<Typography className="proposal__secondary"> <Typography className="proposal__secondary">
{ t('internship.date-range', { start: edition.startDate, end: edition.endDate }) } { t('internship.date-range', { start: edition.startDate, end: edition.endDate }) }
</Typography> </Typography>

View File

@ -15,8 +15,29 @@ import { InsuranceStep } from "@/pages/steps/insurance";
import { StudentStep } from "@/pages/steps/student"; import { StudentStep } from "@/pages/steps/student";
import { useDeadlines } from "@/hooks"; import { useDeadlines } from "@/hooks";
import api from "@/api"; 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 { 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 = () => { export const MainPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -28,15 +49,7 @@ export const MainPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
(async () => { dispatch(updateInternshipInfo);
const internship = await api.internship.get();
dispatch({
type: InternshipProposalActions.Receive,
state: internship.internshipRegistration.state,
internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration),
})
})()
}, []) }, [])
if (!student) { if (!student) {

View File

@ -37,7 +37,8 @@ export const Actions = {
...StudentActions, ...StudentActions,
} }
export type Actions = typeof Actions; export type Actions = typeof Actions;
export type AppDispatch = ThunkDispatch<AppState, any, Action>;
export const useDispatch = () => useReduxDispatch<ThunkDispatch<AppState, any, Action>>() export const useDispatch = () => useReduxDispatch<AppDispatch>()
export default Actions; export default Actions;

View File

@ -1,4 +1,3 @@
import { Plan } from "@/data";
import { import {
ReceiveSubmissionApproveAction, ReceiveSubmissionApproveAction,
ReceiveSubmissionDeclineAction, ReceiveSubmissionDeclineAction,
@ -7,6 +6,8 @@ import {
SendSubmissionAction SendSubmissionAction
} from "@/state/actions/submission"; } from "@/state/actions/submission";
import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration";
export enum InternshipPlanActions { export enum InternshipPlanActions {
Send = "SEND_PLAN", Send = "SEND_PLAN",
Save = "SAVE_PLAN", Save = "SAVE_PLAN",
@ -16,7 +17,7 @@ export enum InternshipPlanActions {
} }
export interface SendPlanAction extends SendSubmissionAction<InternshipPlanActions.Send> { export interface SendPlanAction extends SendSubmissionAction<InternshipPlanActions.Send> {
plan: Plan; document: InternshipDocument;
} }
export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction<InternshipPlanActions.Approve> { export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction<InternshipPlanActions.Approve> {
@ -26,10 +27,12 @@ export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction
} }
export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction<InternshipPlanActions.Receive> { export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction<InternshipPlanActions.Receive> {
document: InternshipDocument;
state: SubmissionState;
} }
export interface SavePlanAction extends SaveSubmissionAction<InternshipPlanActions.Save> { export interface SavePlanAction extends SaveSubmissionAction<InternshipPlanActions.Save> {
plan: Plan; document: InternshipDocument;
} }
export type InternshipPlanAction export type InternshipPlanAction

View File

@ -1,5 +1,4 @@
import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions"; import { InternshipPlanAction, InternshipPlanActions, InternshipProposalActions } from "@/state/actions";
import { Plan } from "@/data";
import { Serializable } from "@/serialization/types"; import { Serializable } from "@/serialization/types";
import { import {
createSubmissionReducer, createSubmissionReducer,
@ -10,19 +9,18 @@ import {
} from "@/state/reducer/submission"; } from "@/state/reducer/submission";
import { Reducer } from "react"; import { Reducer } from "react";
import { SubmissionAction } from "@/state/actions/submission"; import { SubmissionAction } from "@/state/actions/submission";
import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & { export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & {
plan: Serializable<Plan> | null; document: Serializable<InternshipDocument> | null;
} }
const defaultInternshipPlanState: InternshipPlanState = { const defaultInternshipPlanState: InternshipPlanState = {
...defaultDeanApprovalsState, ...defaultDeanApprovalsState,
...defaultSubmissionState, ...defaultSubmissionState,
plan: null, document: null,
} }
export const getInternshipPlan = ({ plan }: InternshipPlanState): Plan | null => plan;
const internshipPlanSubmissionReducer: Reducer<InternshipPlanState, InternshipPlanAction> = createSubmissionReducer({ const internshipPlanSubmissionReducer: Reducer<InternshipPlanState, InternshipPlanAction> = createSubmissionReducer({
[InternshipPlanActions.Approve]: SubmissionAction.Approve, [InternshipPlanActions.Approve]: SubmissionAction.Approve,
[InternshipPlanActions.Decline]: SubmissionAction.Decline, [InternshipPlanActions.Decline]: SubmissionAction.Decline,
@ -39,8 +37,20 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla
case InternshipPlanActions.Send: case InternshipPlanActions.Send:
return { return {
...state, ...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: default:
return state; return state;
} }

View File

@ -39,6 +39,7 @@ pages:
my-editions: "Moje praktyki" my-editions: "Moje praktyki"
pick: "wybierz" pick: "wybierz"
register: "Zapisz się do edycji praktyk" register: "Zapisz się do edycji praktyk"
no-editions: "Brak edycji do wyboru, zarejestruj się do edycji praktyk przyciskiem poniżej."
user-fill: user-fill:
title: "Uzupełnij swoje dane" title: "Uzupełnij swoje dane"
user-profile: user-profile: