Add ability to fill student data
This commit is contained in:
parent
ff2e9c8b82
commit
411603e3a1
@ -11,3 +11,9 @@ export async function current(): Promise<Student> {
|
|||||||
return studentDtoTransfer.transform(dto);
|
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;
|
||||||
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { axios } from "@/api/index";
|
import { axios } from "@/api/index";
|
||||||
import { query, route } from "@/routing";
|
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 CLIENT_ID = process.env.LOGIN_CLIENT_ID || "PraktykiClientId";
|
||||||
const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oauth2.0/authorize";
|
const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oauth2.0/authorize";
|
||||||
|
|
||||||
export async function login(code: string): Promise<string> {
|
export async function login(code?: string): Promise<string> {
|
||||||
const response = await axios.get<string>(LOGIN_ENDPOINT, { params: { code }});
|
const response = code
|
||||||
|
? await axios.get<string>(LOGIN_ENDPOINT, { params: { code }})
|
||||||
|
: await axios.get<string>(DEV_LOGIN_ENDPOINT);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import classNames from "classnames";
|
|||||||
import { useVerticalSpacing } from "@/styles";
|
import { useVerticalSpacing } from "@/styles";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Label, Section } from "@/components/section";
|
import { Label, Section } from "@/components/section";
|
||||||
|
import { StudentPreview } from "@/pages/user/profile";
|
||||||
|
|
||||||
export type ProposalPreviewProps = {
|
export type ProposalPreviewProps = {
|
||||||
proposal: Internship;
|
proposal: Internship;
|
||||||
@ -19,12 +20,7 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
|
|||||||
|
|
||||||
return <div className={ classNames("proposal", classes.root) }>
|
return <div className={ classNames("proposal", classes.root) }>
|
||||||
<div>
|
<div>
|
||||||
<Typography className="proposal__primary">{ proposal.intern.name } { proposal.intern.surname }</Typography>
|
<StudentPreview student={ proposal.intern } />
|
||||||
<Typography className="proposal__secondary">
|
|
||||||
{ t('internship.intern.semester', { semester: proposal.intern.semester }) }
|
|
||||||
{ ", " }
|
|
||||||
{ t('internship.intern.album', { album: proposal.intern.albumNumber }) }
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
|
@ -23,6 +23,6 @@ export function getMissingStudentData(student: Student): (keyof Student)[] {
|
|||||||
!!student.email || "email",
|
!!student.email || "email",
|
||||||
!!student.albumNumber || "albumNumber",
|
!!student.albumNumber || "albumNumber",
|
||||||
!!student.semester || "semester",
|
!!student.semester || "semester",
|
||||||
!!student.course || "course",
|
// !!student.course || "course",
|
||||||
].filter(x => x !== true) as (keyof Student)[];
|
].filter(x => x !== true) as (keyof Student)[];
|
||||||
}
|
}
|
||||||
|
133
src/forms/user.tsx
Normal file
133
src/forms/user.tsx
Normal file
@ -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;
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./useProxyState"
|
export * from "./useProxyState"
|
||||||
export * from "./useUpdateEffect"
|
export * from "./useUpdateEffect"
|
||||||
export * from "./useAsync"
|
export * from "./useAsync"
|
||||||
|
export * from "./state"
|
||||||
|
18
src/hooks/state.ts
Normal file
18
src/hooks/state.ts
Normal file
@ -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);
|
||||||
|
}
|
@ -9,7 +9,7 @@ export const isReadyMiddleware: Middleware<any, any> = next => isLoggedInMiddlew
|
|||||||
const ready = useSelector(isReady);
|
const ready = useSelector(isReady);
|
||||||
|
|
||||||
if (ready) {
|
if (ready) {
|
||||||
return next();
|
return <>{ next() }</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Redirect to={ route("edition_pick") } />;
|
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;
|
const user = useSelector<AppState>(state => state.user) as UserState;
|
||||||
|
|
||||||
if (user.loggedIn) {
|
if (user.loggedIn) {
|
||||||
return next();
|
return <>{ next() }</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Redirect to={ route("login") } />;
|
return <Redirect to={ route("user_login") } />;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ export const Page = ({ title, children, ...props }: PageProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
Page.Header = ({ children, maxWidth = false, ...props }: PageHeaderProps) =>
|
Page.Header = ({ children, maxWidth = undefined, ...props }: PageHeaderProps) =>
|
||||||
<section {...props} className={classNames("page__header", props.className)}>
|
<section {...props} className={classNames("page__header", props.className)}>
|
||||||
<Container maxWidth={ maxWidth }>
|
<Container maxWidth={ maxWidth }>
|
||||||
{ children }
|
{ children }
|
||||||
|
@ -1,51 +1,34 @@
|
|||||||
import React, { useEffect, useMemo } from "react";
|
import React from "react";
|
||||||
import { Page } from "@/pages/base";
|
import { Page } from "@/pages/base";
|
||||||
import { Button, Container, Stepper, Typography } from "@material-ui/core";
|
import { Container, Stepper, Typography } from "@material-ui/core";
|
||||||
import { Link as RouterLink, Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { AppState } from "@/state/reducer";
|
import { AppState } from "@/state/reducer";
|
||||||
import { getMissingStudentData, Student } from "@/data";
|
import { Student } from "@/data";
|
||||||
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
|
|
||||||
import { Step } from "@/components";
|
import { Step } from "@/components";
|
||||||
import { ProposalStep } from "@/pages/steps/proposal";
|
import { ProposalStep } from "@/pages/steps/proposal";
|
||||||
import { PlanStep } from "@/pages/steps/plan";
|
import { PlanStep } from "@/pages/steps/plan";
|
||||||
import { InsuranceState } from "@/state/reducer/insurance";
|
import { InsuranceState } from "@/state/reducer/insurance";
|
||||||
import { InsuranceStep } from "@/pages/steps/insurance";
|
import { InsuranceStep } from "@/pages/steps/insurance";
|
||||||
import api from "@/api";
|
import { StudentStep } from "@/pages/steps/student";
|
||||||
|
import { useDeadlines } from "@/hooks";
|
||||||
|
|
||||||
export const MainPage = () => {
|
export const MainPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const student = useSelector<AppState, Student | null>(state => state.student);
|
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 insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
|
||||||
|
|
||||||
const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]);
|
|
||||||
|
|
||||||
useEffect(() => void api.edition.available())
|
|
||||||
|
|
||||||
if (!student) {
|
if (!student) {
|
||||||
return <Redirect to={ route("user_login") }/>;
|
return <Redirect to={ route("user_login") }/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function *getSteps() {
|
function *getSteps() {
|
||||||
yield <Step label={ t('steps.personal-data.header') } completed={ missingStudentData.length === 0 } until={ deadlines.personalData } key="personal-data">
|
yield <StudentStep key="student"/>;
|
||||||
{ 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 <ProposalStep key="proposal"/>;
|
yield <ProposalStep key="proposal"/>;
|
||||||
yield <PlanStep key="plan"/>;
|
yield <PlanStep key="plan"/>;
|
||||||
|
|
||||||
|
@ -4,16 +4,17 @@ import { InsuranceState } from "@/state/reducer/insurance";
|
|||||||
import { Actions, Step } from "@/components";
|
import { Actions, Step } from "@/components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Edition, getEditionDeadlines } from "@/data/edition";
|
|
||||||
import { Moment } from "moment";
|
|
||||||
import { ContactAction } from "@/pages/steps/common";
|
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 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();
|
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>
|
<p>{ t(`steps.insurance.instructions`) }</p>
|
||||||
<Actions>
|
<Actions>
|
||||||
<ContactAction />
|
<ContactAction />
|
||||||
|
@ -9,9 +9,9 @@ import { Link as RouterLink } from "react-router-dom";
|
|||||||
import { Actions, Step } from "@/components";
|
import { Actions, Step } from "@/components";
|
||||||
import React, { HTMLProps } from "react";
|
import React, { HTMLProps } from "react";
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||||
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
|
|
||||||
import { ContactAction, Status } from "@/pages/steps/common";
|
import { ContactAction, Status } from "@/pages/steps/common";
|
||||||
import { Description as DescriptionIcon } from "@material-ui/icons";
|
import { Description as DescriptionIcon } from "@material-ui/icons";
|
||||||
|
import { useDeadlines } from "@/hooks";
|
||||||
|
|
||||||
const PlanActions = () => {
|
const PlanActions = () => {
|
||||||
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.plan));
|
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 submission = useSelector<AppState, SubmissionState>(state => state.plan);
|
||||||
|
|
||||||
const status = getSubmissionStatus(submission);
|
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;
|
const { sent, declined, comment } = submission;
|
||||||
|
|
||||||
|
@ -6,12 +6,12 @@ import React, { HTMLProps } from "react";
|
|||||||
import { InternshipProposalState } from "@/state/reducer/proposal";
|
import { InternshipProposalState } from "@/state/reducer/proposal";
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||||
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
|
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
|
||||||
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
|
|
||||||
import { Actions, Step } from "@/components";
|
import { Actions, Step } from "@/components";
|
||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index";
|
import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index";
|
||||||
import { ContactAction, Status } from "@/pages/steps/common";
|
import { ContactAction, Status } from "@/pages/steps/common";
|
||||||
|
import { useDeadlines } from "@/hooks";
|
||||||
|
|
||||||
const ProposalActions = () => {
|
const ProposalActions = () => {
|
||||||
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
|
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 submission = useSelector<AppState, SubmissionState>(state => state.proposal);
|
||||||
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(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;
|
const { sent, declined, comment } = submission;
|
||||||
|
|
||||||
|
39
src/pages/steps/student.tsx
Normal file
39
src/pages/steps/student.tsx
Normal file
@ -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>
|
||||||
|
}
|
31
src/pages/user/fill.tsx
Normal file
31
src/pages/user/fill.tsx
Normal file
@ -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;
|
@ -11,7 +11,7 @@ import api from "@/api";
|
|||||||
import { UserActions } from "@/state/actions/user";
|
import { UserActions } from "@/state/actions/user";
|
||||||
import { getAuthorizeUrl } from "@/api/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);
|
const token = await api.user.login(code);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -34,7 +34,7 @@ export const UserLoginPage = () => {
|
|||||||
const query = new URLSearchParams(useLocation().search);
|
const query = new URLSearchParams(useLocation().search);
|
||||||
|
|
||||||
const handleSampleLogin = async () => {
|
const handleSampleLogin = async () => {
|
||||||
await dispatch(authorizeUser("test"));
|
await dispatch(authorizeUser());
|
||||||
|
|
||||||
history.push(route("home"));
|
history.push(route("home"));
|
||||||
}
|
}
|
||||||
|
58
src/pages/user/profile.tsx
Normal file
58
src/pages/user/profile.tsx
Normal file
@ -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;
|
@ -8,6 +8,8 @@ import { UserLoginPage } from "@/pages/user/login";
|
|||||||
import { RegisterEditionPage } from "@/pages/edition/register";
|
import { RegisterEditionPage } from "@/pages/edition/register";
|
||||||
import PickEditionPage from "@/pages/edition/pick";
|
import PickEditionPage from "@/pages/edition/pick";
|
||||||
import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
||||||
|
import UserFillPage from "@/pages/user/fill";
|
||||||
|
import UserProfilePage from "@/pages/user/profile";
|
||||||
|
|
||||||
type Route = {
|
type Route = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -30,7 +32,7 @@ export function processMiddlewares<TArgs extends any[]>(middleware: Middleware<a
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const routes: Route[] = [
|
export const routes: Route[] = [
|
||||||
{ name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isLoggedInMiddleware ] },
|
{ name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isReadyMiddleware ] },
|
||||||
|
|
||||||
// edition
|
// edition
|
||||||
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||||
@ -43,6 +45,8 @@ export const routes: Route[] = [
|
|||||||
|
|
||||||
// user
|
// user
|
||||||
{ name: "user_login", path: "/user/login", content: () => <UserLoginPage/> },
|
{ 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
|
// fallback route for 404 pages
|
||||||
{ name: "fallback", path: "*", content: () => <FallbackPage/> }
|
{ name: "fallback", path: "*", content: () => <FallbackPage/> }
|
||||||
|
31
src/serialization/edition.ts
Normal file
31
src/serialization/edition.ts
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./internship"
|
export * from "./internship"
|
||||||
export * from "./moment"
|
export * from "./moment"
|
||||||
export * from "./types"
|
export * from "./types"
|
||||||
|
export * from "./edition"
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import { EditionAction, EditionActions } from "@/state/actions/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 initialEditionState: EditionState = null;
|
||||||
|
|
||||||
const editionReducer = (state: EditionState = initialEditionState, action: EditionAction): EditionState => {
|
const editionReducer = (state: EditionState = initialEditionState, action: EditionAction): EditionState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case EditionActions.Set:
|
case EditionActions.Set:
|
||||||
return action.edition;
|
return editionSerializationTransformer.transform(action.edition);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
@ -10,7 +10,7 @@ const store = createStore(
|
|||||||
{
|
{
|
||||||
key: 'state',
|
key: 'state',
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
blacklist: ['edition']
|
blacklist: []
|
||||||
},
|
},
|
||||||
rootReducer
|
rootReducer
|
||||||
),
|
),
|
||||||
|
@ -11,7 +11,7 @@ left: jeszcze {{ left, humanize }}
|
|||||||
|
|
||||||
confirm: zatwierdź
|
confirm: zatwierdź
|
||||||
go-back: wstecz
|
go-back: wstecz
|
||||||
|
save: zapisz
|
||||||
make-changes: wprowadź zmiany
|
make-changes: wprowadź zmiany
|
||||||
review: podgląd
|
review: podgląd
|
||||||
fix-errors: popraw uwagi
|
fix-errors: popraw uwagi
|
||||||
@ -39,8 +39,22 @@ 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"
|
||||||
|
user-fill:
|
||||||
|
title: "Uzupełnij swoje dane"
|
||||||
|
user-profile:
|
||||||
|
title: "Moje dane"
|
||||||
|
|
||||||
forms:
|
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:
|
internship:
|
||||||
fields:
|
fields:
|
||||||
start-date: Data rozpoczęcia praktyki
|
start-date: Data rozpoczęcia praktyki
|
||||||
@ -127,11 +141,15 @@ internship:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
personal-data:
|
personal-data:
|
||||||
header: "Uzupełnienie informacji"
|
header: "Uzupełnienie danych"
|
||||||
info: >
|
info: >
|
||||||
Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz
|
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.
|
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:
|
internship-proposal:
|
||||||
header: "Zgłoszenie praktyki"
|
header: "Zgłoszenie praktyki"
|
||||||
info:
|
info:
|
||||||
|
@ -59,7 +59,7 @@ const config = {
|
|||||||
port: parseInt(process.env.APP_PORT || "3000"),
|
port: parseInt(process.env.APP_PORT || "3000"),
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://system-praktyk-front.localhost:8080/",
|
target: "https://system-praktyk.stg.kadet.net/api/",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
"^/api": ''
|
"^/api": ''
|
||||||
|
Loading…
Reference in New Issue
Block a user