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

This commit is contained in:
Kacper Donat 2020-10-18 14:01:31 +02:00
commit 3ead0158ad
48 changed files with 889 additions and 177 deletions

View File

@ -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",

18
src/api/companies.ts Normal file
View File

@ -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;
}

View File

@ -0,0 +1,96 @@
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";
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
import { Moment } from "moment";
import { sampleStudent } from "@/provider/dummy";
export enum SubmissionState {
Draft = "Draft",
Submitted = "Submitted",
Accepted = "Accepted",
Rejected = "Rejected",
Archival = "Archival",
}
export interface NewBranchOffice extends Address {
}
export interface InternshipRegistrationUpdateCompany {
id: string,
branchOffice: Identifiable | NewBranchOffice,
}
export interface NewCompany {
nip: string;
name: string;
branchOffice: NewBranchOffice | null;
}
export interface InternshipRegistrationUpdate {
company: InternshipRegistrationUpdateCompany | NewCompany,
start: string,
end: string,
type: number,
mentor: MentorDTO,
hours: number,
}
export interface InternshipRegistrationDTO extends Identifiable {
start: string;
end: string;
type: InternshipTypeDTO,
state: SubmissionState,
mentor: MentorDTO,
company: Company,
branchAddress: Office,
declaredHours: number,
}
const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id };
export interface InternshipInfoDTO {
internshipRegistration: InternshipRegistrationDTO;
}
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: mentorDtoTransformer.reverseTransform(subject.mentor as Mentor),
company: subject?.company?.id ? {
id: subject?.company?.id as string,
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,
}
}
}
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: dto.declaredHours,
isAccepted: dto.state === SubmissionState.Accepted,
lengthInWeeks: 0,
program: [],
intern: sampleStudent, // fixme
};
}
}

28
src/api/dto/mentor.ts Normal file
View File

@ -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,
}
}
}

34
src/api/dto/type.ts Normal file
View File

@ -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,
}
},
}

View File

@ -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;
}

View File

@ -5,8 +5,12 @@ 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";
import * as upload from "./upload";
export const axios = Axios.create({
baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/",
@ -33,7 +37,11 @@ const api = {
user,
edition,
page,
student
student,
type,
companies,
internship,
upload
}
export default api;

20
src/api/internship.ts Normal file
View File

@ -0,0 +1,20 @@
import { InternshipInfoDTO, InternshipRegistrationUpdate } from "@/api/dto/internship-registration";
import { axios } from "@/api/index";
import { Nullable } from "@/helpers";
const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration';
const INTERNSHIP_ENDPOINT = '/internship';
export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<boolean> {
const response = await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship);
return true;
}
export async function get(): Promise<InternshipInfoDTO> {
const response = await axios.get<InternshipInfoDTO>(INTERNSHIP_ENDPOINT);
console.log(response);
return response.data;
}

View File

@ -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;
}

12
src/api/type.ts Normal file
View File

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

23
src/api/upload.ts Normal file
View File

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

View File

@ -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.post<string>(LOGIN_ENDPOINT, JSON.stringify(code), { headers: { 'Content-Type': 'application/json' } })
: await axios.get<string>(DEV_LOGIN_ENDPOINT);
return response.data;
}

View File

@ -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";
@ -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>
@ -43,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>

View File

@ -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 {

View File

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

View File

@ -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({
@ -97,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>
@ -143,8 +149,23 @@ 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(() => {
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) => {
setFieldTouched("companyName", true);
@ -174,13 +195,16 @@ 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 }
onChange={ handleCompanyChange }
value={ values.company || values.companyName }
inputValue={ input }
freeSolo
onInputChange={ (_, value) => setInput(value) }
/>
</Grid>
<Grid item md={ 4 }>

View File

@ -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 }
value={ values.kind || null as any }
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>
)
}
@ -233,26 +234,27 @@ 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,
}
}
}
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,22 +296,22 @@ 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"))
console.log(update);
api.internship.update(update);
// history.push(route("home"))
}
const InnerForm = () => {
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 {

View File

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

View File

@ -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,13 +27,13 @@ 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>
<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> }>

133
src/forms/user.tsx Normal file
View 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;

View File

@ -1,3 +1,5 @@
export * from "./useProxyState"
export * from "./useUpdateEffect"
export * from "./useAsync"
export * from "./state"
export * from "./providers"

15
src/hooks/providers.ts Normal file
View File

@ -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;
}

18
src/hooks/state.ts Normal file
View 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);
}

View File

@ -1,15 +1,26 @@
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) {
return next();
return <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("user_login") } />;
}

View File

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

View File

@ -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

View File

@ -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">

View File

@ -1,51 +1,50 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect } 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 { 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();
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 dispatch = useDispatch();
const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]);
useEffect(() => {
(async () => {
const internship = await api.internship.get();
useEffect(() => void api.edition.available())
dispatch({
type: InternshipProposalActions.Receive,
state: internship.internshipRegistration.state,
internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration),
})
})()
}, [])
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"/>;

View File

@ -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 />

View File

@ -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;

View File

@ -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;

View 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
View 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;

View File

@ -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"));
}

View 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 component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</Container>
</Page>
}
export default UserProfilePage;

View File

@ -7,7 +7,9 @@ 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";
import UserFillPage from "@/pages/user/fill";
import UserProfilePage from "@/pages/user/profile";
type Route = {
name?: string;
@ -33,16 +35,18 @@ export const routes: Route[] = [
{ name: "home", path: "/", exact: true, content: () => <MainPage/>, middlewares: [ isReadyMiddleware ] },
// 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/> },
{ 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/> }
@ -63,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 }` : '');
}

View 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,
}
},
}

View File

@ -1,3 +1,4 @@
export * from "./internship"
export * from "./moment"
export * from "./types"
export * from "./edition"

View File

@ -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> {

View File

@ -1,14 +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 = Edition | null;
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 action.edition;
return editionSerializationTransformer.transform(action.edition);
case UserActions.Login:
case UserActions.Logout:
return initialEditionState;
}
return state;

View File

@ -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:

View File

@ -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;
}

View File

@ -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 {

View File

@ -10,7 +10,7 @@ const store = createStore(
{
key: 'state',
storage: sessionStorage,
blacklist: ['edition']
blacklist: []
},
rootReducer
),

View File

@ -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:

View File

@ -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": ''

View File

@ -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"