diff --git a/package.json b/package.json index f2da2fd..df24a9f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "html-webpack-plugin": "4.0.0-beta.11", "i18next": "^19.6.0", "i18next-browser-languagedetector": "^5.0.0", + "jsonwebtoken": "^8.5.1", "material-ui-dropzone": "^3.3.0", "mdi-material-ui": "^6.17.0", "moment": "^2.26.0", diff --git a/src/api/companies.ts b/src/api/companies.ts new file mode 100644 index 0000000..838348f --- /dev/null +++ b/src/api/companies.ts @@ -0,0 +1,18 @@ +import { Company, Office } from "@/data"; +import { axios } from "@/api/index"; +import { prepare, query } from "@/routing"; + +export const COMPANY_SEARCH_ENDPOINT = '/companies'; +export const COMPANY_OFFICES_ENDPOINT = '/companies/:id' + +export async function search(name: string): Promise { + const companies = await axios.get(query(COMPANY_SEARCH_ENDPOINT, { Name: name })); + + return companies.data; +} + +export async function offices(id: string): Promise { + const response = await axios.get(prepare(COMPANY_OFFICES_ENDPOINT, { id })); + + return response.data; +} diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts new file mode 100644 index 0000000..b415db9 --- /dev/null +++ b/src/api/dto/internship-registration.ts @@ -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> = { + transform(subject: Nullable, context?: unknown): Nullable { + 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 = { + 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 + }; + } +} diff --git a/src/api/dto/mentor.ts b/src/api/dto/mentor.ts new file mode 100644 index 0000000..2d5f03a --- /dev/null +++ b/src/api/dto/mentor.ts @@ -0,0 +1,28 @@ +import { Transformer } from "@/serialization"; +import { Mentor } from "@/data"; + +export interface MentorDTO { + firstName: string; + lastName: string; + email: string; + phoneNumber: string; +} + +export const mentorDtoTransformer: Transformer = { + reverseTransform(subject: Mentor, context?: unknown): MentorDTO { + return { + firstName: subject.name, + lastName: subject.surname, + email: subject.email, + phoneNumber: subject.phone || "", + } + }, + transform(subject: MentorDTO, context?: unknown): Mentor { + return { + name: subject.firstName, + surname: subject.lastName, + email: subject.email, + phone: subject.phoneNumber, + } + } +} diff --git a/src/api/dto/type.ts b/src/api/dto/type.ts new file mode 100644 index 0000000..213dcb1 --- /dev/null +++ b/src/api/dto/type.ts @@ -0,0 +1,34 @@ +import { Identifiable, InternshipType } from "@/data"; +import { Transformer } from "@/serialization"; + +export interface InternshipTypeDTO extends Identifiable { + label: string; + labelEng: string; + description?: string; + descriptionEng?: string; +} + +export const internshipTypeDtoTransformer: Transformer = { + transform(subject: InternshipTypeDTO, context?: unknown): InternshipType { + return { + id: subject.id, + label: { + pl: subject.label, + en: subject.labelEng + }, + description: subject.description ? { + pl: subject.description, + en: subject.descriptionEng || "" + } : undefined + } + }, + reverseTransform(subject: InternshipType, context?: unknown): InternshipTypeDTO { + return { + id: subject.id, + label: subject.label.pl, + labelEng: subject.label.en, + description: subject.description?.pl || undefined, + descriptionEng: subject.description?.en || undefined, + } + }, +} diff --git a/src/api/edition.ts b/src/api/edition.ts index 6164634..1a04c9b 100644 --- a/src/api/edition.ts +++ b/src/api/edition.ts @@ -5,7 +5,9 @@ import { EditionDTO, editionDtoTransformer, editionTeaserDtoTransformer } from " const EDITIONS_ENDPOINT = "/editions"; const EDITION_INFO_ENDPOINT = "/editions/:key"; -const REGISTER_ENDPOINT = "/register"; +const EDITION_CURRENT_ENDPOINT = "/editions/current"; +const EDITION_REGISTER_ENDPOINT = "/register"; +const EDITION_LOGIN_ENDPOINT = "/access/loginEdition"; export async function available() { const response = await axios.get(EDITIONS_ENDPOINT); @@ -15,7 +17,7 @@ export async function available() { export async function join(key: string): Promise { 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 { return editionDtoTransformer.transform(dto); } + +export async function current(): Promise { + const response = await axios.get(EDITION_CURRENT_ENDPOINT); + const dto = response.data; + + return editionDtoTransformer.transform(dto); +} + +export async function login(key: string): Promise { + const response = await axios.post(EDITION_LOGIN_ENDPOINT, JSON.stringify(key), { headers: { "Content-Type": "application/json" } }) + + return response.data; +} diff --git a/src/api/index.ts b/src/api/index.ts index 3432330..5f76ba3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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; diff --git a/src/api/internship.ts b/src/api/internship.ts new file mode 100644 index 0000000..5741950 --- /dev/null +++ b/src/api/internship.ts @@ -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): Promise { + const response = await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); + + return true; +} + +export async function get(): Promise { + const response = await axios.get(INTERNSHIP_ENDPOINT); + + console.log(response); + + return response.data; +} diff --git a/src/api/page.tsx b/src/api/page.ts similarity index 100% rename from src/api/page.tsx rename to src/api/page.ts diff --git a/src/api/student.ts b/src/api/student.ts index e93a861..49ad468 100644 --- a/src/api/student.ts +++ b/src/api/student.ts @@ -11,3 +11,9 @@ export async function current(): Promise { return studentDtoTransfer.transform(dto); } +export async function update(student: Student): Promise { + const dto = studentDtoTransfer.reverseTransform(student); + const response = await axios.put(CURRENT_STUDENT_ENDPOINT, dto); + + return student; +} diff --git a/src/api/type.ts b/src/api/type.ts new file mode 100644 index 0000000..aa6c577 --- /dev/null +++ b/src/api/type.ts @@ -0,0 +1,12 @@ +import { InternshipType } from "@/data"; +import { axios } from "@/api/index"; +import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; + +const AVAILABLE_INTERNSHIP_TYPES = '/internshipTypes'; + +export async function available(): Promise { + const response = await axios.get(AVAILABLE_INTERNSHIP_TYPES); + const dtos = response.data; + + return dtos.map(dto => internshipTypeDtoTransformer.transform(dto)); +} diff --git a/src/api/upload.ts b/src/api/upload.ts new file mode 100644 index 0000000..e27735a --- /dev/null +++ b/src/api/upload.ts @@ -0,0 +1,23 @@ +import { Identifiable } from "@/data"; +import { axios } from "@/api/index"; + +export enum UploadType { + Ipp = "IppScan", + DeanConsent = "DeanConsent", + Insurance = "NnwInsurance", +} + +const CREATE_DOCUMENT_ENDPOINT = '/document'; +const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan'; + +interface Document extends Identifiable { + description?: string; + type: UploadType; +} + +export async function create(type: UploadType, content: File) +{ + const response = await axios.post(CREATE_DOCUMENT_ENDPOINT, { type }); + + console.log(response.data); +} diff --git a/src/api/user.ts b/src/api/user.ts index e930190..cd2a0de 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,13 +1,16 @@ import { axios } from "@/api/index"; import { query, route } from "@/routing"; -const LOGIN_ENDPOINT = "/access/login" +const LOGIN_ENDPOINT = "/access/login"; +const DEV_LOGIN_ENDPOINT = "/dev/login"; const CLIENT_ID = process.env.LOGIN_CLIENT_ID || "PraktykiClientId"; const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oauth2.0/authorize"; -export async function login(code: string): Promise { - const response = await axios.get(LOGIN_ENDPOINT, { params: { code }}); +export async function login(code?: string): Promise { + const response = code + ? await axios.post(LOGIN_ENDPOINT, JSON.stringify(code), { headers: { 'Content-Type': 'application/json' } }) + : await axios.get(DEV_LOGIN_ENDPOINT); return response.data; } diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx index 3babfc7..b452875 100644 --- a/src/components/proposalPreview.tsx +++ b/src/components/proposalPreview.tsx @@ -1,4 +1,4 @@ -import { Internship, internshipTypeLabels } from "@/data"; +import { Internship } from "@/data"; import React from "react"; import { Typography } from "@material-ui/core"; import { useTranslation } from "react-i18next"; @@ -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
- { proposal.intern.name } { proposal.intern.surname } - - { t('internship.intern.semester', { semester: proposal.intern.semester }) } - { ", " } - { t('internship.intern.album', { album: proposal.intern.albumNumber }) } - +
@@ -43,7 +39,7 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
- { internshipTypeLabels[proposal.type].label } + { proposal.type.label.pl }
diff --git a/src/data/internship.ts b/src/data/internship.ts index 4cd82ea..29733f4 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -1,52 +1,11 @@ import { Moment } from "moment"; -import { Identifiable } from "./common"; +import { Identifiable, Multilingual } from "./common"; import { Student } from "@/data/student"; import { Company, Office } from "@/data/company"; -export enum InternshipType { - FreeInternship = "FreeInternship", - GraduateInternship = "GraduateInternship", - FreeApprenticeship = "FreeApprenticeship", - PaidApprenticeship = "PaidApprenticeship", - ForeignInternship = "ForeignInternship", - UOP = "UOP", - UD = "UD", - UZ = "UZ", - Other = "Other", -} - -export const internshipTypeLabels: { [type in InternshipType]: { label: string, description?: string } } = { - [InternshipType.FreeInternship]: { - label: "Umowa o organizację praktyki", - description: "Praktyka bezpłatna" - }, - [InternshipType.GraduateInternship]: { - label: "Umowa o praktykę absolwencką" - }, - [InternshipType.FreeApprenticeship]: { - label: "Umowa o staż bezpłatny" - }, - [InternshipType.PaidApprenticeship]: { - label: "Umowa o staż płatny", - description: "np. przemysłowy" - }, - [InternshipType.ForeignInternship]: { - label: "Praktyka zagraniczna", - description: "np. IAESTE, ERASMUS" - }, - [InternshipType.UOP]: { - label: "Umowa o pracę" - }, - [InternshipType.UD]: { - label: "Umowa o dzieło (w tym B2B)" - }, - [InternshipType.UZ]: { - label: "Umowa o zlecenie (w tym B2B)" - }, - [InternshipType.Other]: { - label: "Inna", - description: "Należy wprowadzić samodzielnie" - }, +export interface InternshipType extends Identifiable { + label: Multilingual, + description?: Multilingual, } export interface InternshipProgramEntry extends Identifiable { diff --git a/src/data/student.ts b/src/data/student.ts index d649fbd..fe84901 100644 --- a/src/data/student.ts +++ b/src/data/student.ts @@ -23,6 +23,6 @@ export function getMissingStudentData(student: Student): (keyof Student)[] { !!student.email || "email", !!student.albumNumber || "albumNumber", !!student.semester || "semester", - !!student.course || "course", + // !!student.course || "course", ].filter(x => x !== true) as (keyof Student)[]; } diff --git a/src/forms/company.tsx b/src/forms/company.tsx index 74fbfdb..246b1c2 100644 --- a/src/forms/company.tsx +++ b/src/forms/company.tsx @@ -1,12 +1,12 @@ -import React, { HTMLProps, useMemo } from "react"; +import React, { HTMLProps, useEffect, useMemo, useState } from "react"; import { Company, formatAddress, Office } from "@/data"; -import { sampleCompanies } from "@/provider/dummy"; import { Autocomplete } from "@material-ui/lab"; import { Grid, TextField, Typography } from "@material-ui/core"; import { InternshipFormValues } from "@/forms/internship"; import { useTranslation } from "react-i18next"; import { Field, useFormikContext } from "formik"; import { TextField as TextFieldFormik } from "formik-material-ui" +import api from "@/api"; export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps) => (
@@ -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([]); 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 /> @@ -143,8 +149,23 @@ export const CompanyForm: React.FunctionComponent = () => { const { values, setValues, errors, touched, setFieldTouched } = useFormikContext(); const { t } = useTranslation(); + const [input, setInput] = useState(""); + const [companies, setCompanies] = useState([]); + 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 = () => { <> - typeof option === "string" ? option : option.name } renderOption={ company => } renderInput={ props => } - onChange={ handleCompanyChange } value={ values.company || values.companyName } + onChange={ handleCompanyChange } + value={ values.company || values.companyName } + inputValue={ input } freeSolo + onInputChange={ (_, value) => setInput(value) } /> diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 10fb302..c36b3a3 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -4,13 +4,13 @@ import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; import { CompanyForm } from "@/forms/company"; import { StudentForm } from "@/forms/student"; import { sampleStudent } from "@/provider/dummy/student"; -import { Company, Internship, InternshipType, internshipTypeLabels, Office, Student } from "@/data"; +import { Company, Internship, InternshipType, Office, Student } from "@/data"; import { Nullable } from "@/helpers"; import moment, { Moment } from "moment"; import { computeWorkingHours } from "@/utils/date"; import { Autocomplete } from "@material-ui/lab"; import { emptyInternship } from "@/provider/dummy/internship"; -import { InternshipProposalActions, useDispatch } from "@/state/actions"; +import { useDispatch } from "@/state/actions"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { AppState } from "@/state/reducer"; @@ -22,8 +22,9 @@ import { Field, Form, Formik, useFormikContext } from "formik"; import * as Yup from "yup"; import { Transformer } from "@/serialization"; import { TextField as TextFieldFormik } from "formik-material-ui" -import { Edition } from "@/data/edition"; -import { useUpdateEffect } from "@/hooks"; +import { useCurrentEdition, useCurrentStudent, useInternshipTypes, useUpdateEffect } from "@/hooks"; +import { internshipRegistrationUpdateTransformer } from "@/api/dto/internship-registration"; +import api from "@/api"; export type InternshipFormValues = { startDate: Moment | null; @@ -73,13 +74,11 @@ const emptyInternshipValues: InternshipFormValues = { workingHours: 40, } -export const InternshipTypeItem = ({ type, ...props }: { type: InternshipType } & HTMLProps) => { - const info = internshipTypeLabels[type]; - +export const InternshipTypeItem = ({ internshipType: type, ...props }: { internshipType: InternshipType } & HTMLProps) => { return (
-
{ info.label }
- { info.description && { info.description } } +
{ type.label.pl }
+ { type.description && { type.description.pl } }
) } @@ -88,25 +87,27 @@ const InternshipProgramForm = () => { const { t } = useTranslation(); const { values, handleBlur, setFieldValue, errors } = useFormikContext(); + const types = useInternshipTypes(); + return ( } - getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label } - renderOption={ (option: InternshipType) => } - options={ Object.values(InternshipType) as InternshipType[] } + getOptionLabel={ (option: InternshipType) => option.label.pl } + renderOption={ (option: InternshipType) => } + options={ types } disableClearable - value={ values.kind || undefined } + value={ values.kind || null as any } onChange={ (_, value) => setFieldValue("kind", value) } onBlur={ handleBlur } /> - - { - values.kind === InternshipType.Other && - - } - + {/**/} + {/* {*/} + {/* values.kind === InternshipType.Other &&*/} + {/* */} + {/* }*/} + {/**/} ) } @@ -233,26 +234,27 @@ const converter: Transformer, 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>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, office: null, company: null, mentor: null, - intern: sampleStudent + intern: student }); - const edition = useSelector(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 { diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 0c96203..1626d03 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -7,7 +7,9 @@ import { route } from "@/routing"; import React, { useState } from "react"; import { Plan } from "@/data"; import { useTranslation } from "react-i18next"; -import { InternshipPlanActions, useDispatch } from "@/state/actions"; +import { useDispatch } from "@/state/actions"; +import { UploadType } from "@/api/upload"; +import api from "@/api"; export const PlanForm = () => { const { t } = useTranslation(); @@ -18,7 +20,8 @@ export const PlanForm = () => { const history = useHistory(); const handleSubmit = () => { - dispatch({ type: InternshipPlanActions.Send, plan }); + api.upload.create(UploadType.Ipp, null as any); + // dispatch({ type: InternshipPlanActions.Send, plan }); history.push(route("home")) } diff --git a/src/forms/student.tsx b/src/forms/student.tsx index 1c8ee26..efc4842 100644 --- a/src/forms/student.tsx +++ b/src/forms/student.tsx @@ -2,14 +2,15 @@ import { Course } from "@/data"; import { Button, Grid, TextField } from "@material-ui/core"; import { Alert, Autocomplete } from "@material-ui/lab"; import React from "react"; -import { sampleCourse } from "@/provider/dummy/student"; import { useTranslation } from "react-i18next"; import { useFormikContext } from "formik"; import { InternshipFormValues } from "@/forms/internship"; +import { useCurrentEdition } from "@/hooks"; export const StudentForm = () => { const { t } = useTranslation(); const { values: { student } } = useFormikContext(); + const course = useCurrentEdition()?.course as Course; return <> @@ -26,13 +27,13 @@ export const StudentForm = () => { course.name } renderInput={ props => } - options={[ sampleCourse ]} - value={ student.course } + options={[ course ]} + value={ course } disabled /> - + skontaktuj się z opiekunem }> diff --git a/src/forms/user.tsx b/src/forms/user.tsx new file mode 100644 index 0000000..9b450bc --- /dev/null +++ b/src/forms/user.tsx @@ -0,0 +1,133 @@ +import { Student } from "@/data"; +import { Transformer } from "@/serialization"; +import React, { useMemo } from "react"; +import { Field, Formik, useFormikContext } from "formik"; +import api from "@/api"; +import { Button, Grid, Typography } from "@material-ui/core"; +import { TextField as TextFieldFormik } from "formik-material-ui"; +import { useTranslation } from "react-i18next"; +import { Actions } from "@/components"; +import { Nullable } from "@/helpers"; +import * as Yup from "yup"; +import { StudentActions, useDispatch } from "@/state/actions"; + +interface StudentFormValues { + firstName: string; + lastName: string; + email: string; + albumNumber: number | ""; + semester: number | ""; +} + +type StudentFormProps = { + student: Student; +} + +const studentToFormValuesTransformer: Transformer, StudentFormValues, { current: Student }> = { + transform(subject: Nullable, 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 { + 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({ + 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
+ { t("forms.student.sections.personal") } + + + + + + + + + + + + { t("forms.student.sections.studies")} + + + + + + + + + + + +
+ } + + return + + +} + +export default StudentForm; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 848cfd7..3c9050a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,5 @@ export * from "./useProxyState" export * from "./useUpdateEffect" export * from "./useAsync" +export * from "./state" +export * from "./providers" diff --git a/src/hooks/providers.ts b/src/hooks/providers.ts new file mode 100644 index 0000000..2f0d8f6 --- /dev/null +++ b/src/hooks/providers.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; +import api from "@/api"; +import { InternshipType } from "@/data"; + +export const useInternshipTypes = () => { + const [types, setTypes] = useState([]); + + useEffect(() => { + (async () => { + setTypes(await api.type.available()); + })() + }, []) + + return types; +} diff --git a/src/hooks/state.ts b/src/hooks/state.ts new file mode 100644 index 0000000..d024dcd --- /dev/null +++ b/src/hooks/state.ts @@ -0,0 +1,18 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { Edition, getEditionDeadlines } from "@/data/edition"; +import { editionSerializationTransformer } from "@/serialization"; +import { Student } from "@/data"; + +export const useCurrentStudent = () => useSelector( + state => state.student +) + +export const useCurrentEdition = () => useSelector( + state => state.edition && editionSerializationTransformer.reverseTransform(state.edition) +) + +export const useDeadlines = () => { + const edition = useCurrentEdition() as Edition; + return getEditionDeadlines(edition); +} diff --git a/src/middleware.tsx b/src/middleware.tsx index 7d16ca3..621d234 100644 --- a/src/middleware.tsx +++ b/src/middleware.tsx @@ -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 = next => { +export const isReadyMiddleware: Middleware = Next => isLoggedInMiddleware(() => { const ready = useSelector(isReady); if (ready) { - return next(); + return ; } return ; +}) + +export const isLoggedInMiddleware: Middleware = Next => { + const user = useSelector(state => state.user) as UserState; + + if (user.loggedIn) { + return ; + } + + return ; } diff --git a/src/pages/base.tsx b/src/pages/base.tsx index be27987..bc6e5cc 100644 --- a/src/pages/base.tsx +++ b/src/pages/base.tsx @@ -21,7 +21,7 @@ export const Page = ({ title, children, ...props }: PageProps) => { } -Page.Header = ({ children, maxWidth = false, ...props }: PageHeaderProps) => +Page.Header = ({ children, maxWidth = undefined, ...props }: PageHeaderProps) =>
{ children } diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx index 5275408..a1261c0 100644 --- a/src/pages/edition/pick.tsx +++ b/src/pages/edition/pick.tsx @@ -11,7 +11,7 @@ import api from "@/api"; import { Section } from "@/components/section"; import { useVerticalSpacing } from "@/styles"; import { Alert } from "@material-ui/lab"; -import { EditionActions, useDispatch } from "@/state/actions"; +import { EditionActions, useDispatch, UserActions } from "@/state/actions"; export const PickEditionPage = () => { const { t } = useTranslation(); @@ -23,12 +23,19 @@ export const PickEditionPage = () => { const classes = useVerticalSpacing(3); const pickEditionHandler = (id: string) => async () => { - const edition = await api.edition.get(id); + const token = await api.edition.login(id); - if (!edition) { + if (!token) { return; } + await dispatch({ + type: UserActions.Login, + token, + }) + + const edition = await api.edition.current(); + dispatch({ type: EditionActions.Set, edition diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index 939998b..1b3a814 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -115,10 +115,6 @@ export const InternshipProposalPreviewPage = () => { { proposal && } - - + + diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 28215eb..e204644 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -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(state => state.student); - const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + const deadlines = useDeadlines(); const insurance = useSelector(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 ; } function *getSteps() { - yield - { missingStudentData.length > 0 && <> -

{ t('steps.personal-data.info') }

- -
    - { missingStudentData.map(field =>
  • { t(`student.${ field }`) }
  • ) } -
- - - } -
; - + yield ; yield ; yield ; diff --git a/src/pages/steps/insurance.tsx b/src/pages/steps/insurance.tsx index 56e1a41..b005389 100644 --- a/src/pages/steps/insurance.tsx +++ b/src/pages/steps/insurance.tsx @@ -4,16 +4,17 @@ import { InsuranceState } from "@/state/reducer/insurance"; import { Actions, Step } from "@/components"; import { useTranslation } from "react-i18next"; import React from "react"; -import { Edition, getEditionDeadlines } from "@/data/edition"; -import { Moment } from "moment"; import { ContactAction } from "@/pages/steps/common"; +import { useDeadlines } from "@/hooks"; +import { StepProps } from "@material-ui/core"; -export const InsuranceStep = () => { +export const InsuranceStep = (props: StepProps) => { const insurance = useSelector(root => root.insurance); - const deadline = useSelector(state => getEditionDeadlines(state.edition as Edition).insurance); // edition cannot be null at this point + const deadline = useDeadlines().insurance; + const { t } = useTranslation(); - return + return

{ t(`steps.insurance.instructions`) }

diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index 7a94c7f..e6f6c4c 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -9,9 +9,9 @@ import { Link as RouterLink } from "react-router-dom"; import { Actions, Step } from "@/components"; import React, { HTMLProps } from "react"; import { Alert, AlertTitle } from "@material-ui/lab"; -import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; import { ContactAction, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; +import { useDeadlines } from "@/hooks"; const PlanActions = () => { const status = useSelector(state => getSubmissionStatus(state.plan)); @@ -74,7 +74,7 @@ export const PlanStep = (props: StepProps) => { const submission = useSelector(state => state.plan); const status = getSubmissionStatus(submission); - const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + const deadlines = useDeadlines(); const { sent, declined, comment } = submission; diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx index f7646d9..9996ba4 100644 --- a/src/pages/steps/proposal.tsx +++ b/src/pages/steps/proposal.tsx @@ -6,12 +6,12 @@ import React, { HTMLProps } from "react"; import { InternshipProposalState } from "@/state/reducer/proposal"; import { Alert, AlertTitle } from "@material-ui/lab"; import { Box, Button, ButtonProps, StepProps } from "@material-ui/core"; -import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; import { Actions, Step } from "@/components"; import { route } from "@/routing"; import { Link as RouterLink } from "react-router-dom"; import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index"; import { ContactAction, Status } from "@/pages/steps/common"; +import { useDeadlines } from "@/hooks"; const ProposalActions = () => { const status = useSelector(state => getSubmissionStatus(state.proposal)); @@ -69,7 +69,7 @@ export const ProposalStep = (props: StepProps) => { const submission = useSelector(state => state.proposal); const status = useSelector(state => getSubmissionStatus(state.proposal)); - const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point + const deadlines = useDeadlines(); const { sent, declined, comment } = submission; diff --git a/src/pages/steps/student.tsx b/src/pages/steps/student.tsx new file mode 100644 index 0000000..24eab0b --- /dev/null +++ b/src/pages/steps/student.tsx @@ -0,0 +1,39 @@ +import { Button, StepProps } from "@material-ui/core"; +import { route } from "@/routing"; +import { Link as RouterLink } from "react-router-dom"; +import { Actions, Step } from "@/components"; +import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { getMissingStudentData, Student } from "@/data"; +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import { useDeadlines } from "@/hooks"; +import { AccountDetails } from "mdi-material-ui"; + +export const StudentStep = (props: StepProps) => { + const { t } = useTranslation(); + const student = useSelector(state => state.student); + const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]); + const deadlines = useDeadlines(); + + return + { missingStudentData.length > 0 ? <> +

{ t('steps.personal-data.info') }

+ +
    + { missingStudentData.map(field =>
  • { t(`student.${ field }`) }
  • ) } +
+ + + : <> +

{ t('steps.personal-data.all-filled') }

+ + + + } +
+} diff --git a/src/pages/user/fill.tsx b/src/pages/user/fill.tsx new file mode 100644 index 0000000..de3cc1c --- /dev/null +++ b/src/pages/user/fill.tsx @@ -0,0 +1,31 @@ +import { useSelector } from "react-redux"; +import { AppState } from "@/state/reducer"; +import React from "react"; +import { Page } from "@/pages/base"; +import { useTranslation } from "react-i18next"; +import { Container, Link, Typography } from "@material-ui/core"; +import StudentForm from "@/forms/user"; +import { Student } from "@/data"; +import { Link as RouterLink } from "react-router-dom"; +import { route } from "@/routing"; + +export const UserFillPage = () => { + const student = useSelector(state => state.student) as Student; + + const { t } = useTranslation(); + + return + + + { t("pages.my-internship.header") } + { t("pages.user-fill.title") } + + { t("pages.user-fill.title") } + + + + + +} + +export default UserFillPage; diff --git a/src/pages/user/login.tsx b/src/pages/user/login.tsx index 46a5a00..c7dd466 100644 --- a/src/pages/user/login.tsx +++ b/src/pages/user/login.tsx @@ -11,7 +11,7 @@ import api from "@/api"; import { UserActions } from "@/state/actions/user"; import { getAuthorizeUrl } from "@/api/user"; -const authorizeUser = (code: string) => async (dispatch: Dispatch, getState: () => AppState): Promise => { +const authorizeUser = (code?: string) => async (dispatch: Dispatch, getState: () => AppState): Promise => { const token = await api.user.login(code); dispatch({ @@ -34,7 +34,7 @@ export const UserLoginPage = () => { const query = new URLSearchParams(useLocation().search); const handleSampleLogin = async () => { - await dispatch(authorizeUser("test")); + await dispatch(authorizeUser()); history.push(route("home")); } diff --git a/src/pages/user/profile.tsx b/src/pages/user/profile.tsx new file mode 100644 index 0000000..54bca0e --- /dev/null +++ b/src/pages/user/profile.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Page } from "@/pages/base"; +import { useTranslation } from "react-i18next"; +import { useCurrentStudent } from "@/hooks"; +import { Box, Button, Container, Link, Paper, Typography } from "@material-ui/core"; +import { Student } from "@/data"; +import { Link as RouterLink } from "react-router-dom"; +import { route } from "@/routing"; +import { Actions } from "@/components"; +import { useVerticalSpacing } from "@/styles"; + +type StudentPreviewProps = { + student: Student; +} + +export const StudentPreview = ({ student }: StudentPreviewProps) => { + const { t } = useTranslation(); + + return <> + { student.name } { student.surname } + + { t('internship.intern.semester', { semester: student.semester }) } + { ", " } + { t('internship.intern.album', { album: student.albumNumber }) } + + ; +} + +export const UserProfilePage = () => { + const { t } = useTranslation(); + + const student = useCurrentStudent() as Student; + const spacing = useVerticalSpacing(3); + + return + + + { t("pages.my-internship.header") } + { t("pages.user-profile.title") } + + { t('pages.user-profile.title') } + + + + + + + + + + + + +} + +export default UserProfilePage; diff --git a/src/routing.tsx b/src/routing.tsx index f9ef81f..8b88172 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -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: () => , middlewares: [ isReadyMiddleware ] }, // edition - { name: "edition_register", path: "/edition/register", exact: true, content: () => }, - { name: "edition_pick", path: "/edition/pick", exact: true, content: () => }, + { name: "edition_register", path: "/edition/register", exact: true, content: () => , middlewares: [ isLoggedInMiddleware ] }, + { name: "edition_pick", path: "/edition/pick", exact: true, content: () => , middlewares: [ isLoggedInMiddleware ] }, // internship { name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, { name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, - { name: "internship_plan", path: "/internship/plan", exact: true, content: () => }, + { name: "internship_plan", path: "/internship/plan", exact: true, content: () => , middlewares: [ isReadyMiddleware ] }, // user { name: "user_login", path: "/user/login", content: () => }, + { name: "user_fill", path: "/user/data", content: () => , middlewares: [ isLoggedInMiddleware ] }, + { name: "user_profile", path: "/user/profile", content: () => , middlewares: [ isLoggedInMiddleware ] }, // fallback route for 404 pages { name: "fallback", path: "*", content: () => } @@ -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 }` : ''); } diff --git a/src/serialization/edition.ts b/src/serialization/edition.ts new file mode 100644 index 0000000..ae4555c --- /dev/null +++ b/src/serialization/edition.ts @@ -0,0 +1,31 @@ +import { Serializable, SerializationTransformer } from "@/serialization/types"; +import { Edition } from "@/data/edition"; +import { momentSerializationTransformer } from "@/serialization/moment"; +import { Moment } from "moment"; + +export const editionSerializationTransformer: SerializationTransformer = { + transform(subject: Edition, context?: unknown): Serializable { + 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, context?: unknown): Edition { + return { + course: subject.course, + minimumInternshipHours: subject.minimumInternshipHours, + maximumInternshipHours: subject.maximumInternshipHours, + proposalDeadline: momentSerializationTransformer.reverseTransform(subject.proposalDeadline) as Moment, + reportingEnd: momentSerializationTransformer.reverseTransform(subject.reportingEnd) as Moment, + reportingStart: momentSerializationTransformer.reverseTransform(subject.reportingStart) as Moment, + startDate: momentSerializationTransformer.reverseTransform(subject.startDate) as Moment, + endDate: momentSerializationTransformer.reverseTransform(subject.endDate) as Moment, + } + }, +} diff --git a/src/serialization/index.ts b/src/serialization/index.ts index e6efcb2..1f6c52f 100644 --- a/src/serialization/index.ts +++ b/src/serialization/index.ts @@ -1,3 +1,4 @@ export * from "./internship" export * from "./moment" export * from "./types" +export * from "./edition" diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index ad8102a..2db7692 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -6,6 +6,7 @@ import { SaveSubmissionAction, SendSubmissionAction } from "@/state/actions/submission"; +import { SubmissionState } from "@/api/dto/internship-registration"; export enum InternshipProposalActions { Send = "SEND_PROPOSAL", @@ -26,6 +27,8 @@ export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAc } export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction { + internship: Internship; + state: SubmissionState, } export interface SaveProposalAction extends SaveSubmissionAction { diff --git a/src/state/reducer/edition.ts b/src/state/reducer/edition.ts index ef696f9..43084d4 100644 --- a/src/state/reducer/edition.ts +++ b/src/state/reducer/edition.ts @@ -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 | 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; diff --git a/src/state/reducer/insurance.ts b/src/state/reducer/insurance.ts index 61cabc9..9e99ae2 100644 --- a/src/state/reducer/insurance.ts +++ b/src/state/reducer/insurance.ts @@ -1,7 +1,6 @@ import { Reducer } from "react"; import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance"; import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions"; -import { InternshipType } from "@/data"; export type InsuranceState = { required: boolean; @@ -21,12 +20,6 @@ export const insuranceReducer: Reducer | 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; } diff --git a/src/state/reducer/user.ts b/src/state/reducer/user.ts index ae868a6..1757986 100644 --- a/src/state/reducer/user.ts +++ b/src/state/reducer/user.ts @@ -1,4 +1,3 @@ -import { Reducer } from "react"; import { UserAction, UserActions } from "@/state/actions/user"; export type UserState = { @@ -10,7 +9,7 @@ const initialUserState: UserState = { loggedIn: false, } -const userReducer: Reducer = (state = initialUserState, action) => { +const userReducer = (state: UserState = initialUserState, action: UserAction): UserState => { switch (action.type) { case UserActions.Login: return { diff --git a/src/state/store.ts b/src/state/store.ts index 935b3b2..7168bf7 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -10,7 +10,7 @@ const store = createStore( { key: 'state', storage: sessionStorage, - blacklist: ['edition'] + blacklist: [] }, rootReducer ), diff --git a/translations/pl.yaml b/translations/pl.yaml index 2d7554f..d077211 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -11,7 +11,7 @@ left: jeszcze {{ left, humanize }} confirm: zatwierdź go-back: wstecz - +save: zapisz make-changes: wprowadź zmiany review: podgląd fix-errors: popraw uwagi @@ -39,8 +39,22 @@ pages: my-editions: "Moje praktyki" pick: "wybierz" register: "Zapisz się do edycji praktyk" + user-fill: + title: "Uzupełnij swoje dane" + user-profile: + title: "Moje dane" forms: + student: + fields: + first-name: Imię + last-name: Nazwisko + email: Kontaktowy adres e-mail + album-number: Numer albumu + semester: Aktualny semestr studiów + sections: + personal: "Dane osobowe" + studies: "Dane kierunkowe" internship: fields: start-date: Data rozpoczęcia praktyki @@ -127,11 +141,15 @@ internship: steps: personal-data: - header: "Uzupełnienie informacji" + header: "Uzupełnienie danych" info: > Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz problem z uzupełnieniem tych informacji - skontaktuj się z pełnomocnikiem ds. praktyk dla Twojego kierunku. - form: "Uzupełnij dane" + all-filled: > + Wypełniłeś wszystkie wymagane informacje o sobie. + actions: + form: "Uzupełnij dane" + info: $t(pages.user-profile.title) internship-proposal: header: "Zgłoszenie praktyki" info: diff --git a/webpack.config.js b/webpack.config.js index 6585967..201baa4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -59,7 +59,7 @@ const config = { port: parseInt(process.env.APP_PORT || "3000"), proxy: { "/api": { - target: "http://system-praktyk-front.localhost:8080/", + target: "https://system-praktyk.stg.kadet.net/api/", changeOrigin: true, pathRewrite: { "^/api": '' diff --git a/yarn.lock b/yarn.lock index f385c39..e5383ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2258,6 +2258,11 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.2, browserslist@^4. node-releases "^1.1.53" pkg-up "^2.0.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -3433,6 +3438,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -5219,6 +5231,22 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5299,6 +5327,23 @@ jss@^10.0.3, jss@^10.3.0: is-in-browser "^1.1.3" tiny-warning "^1.0.2" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -5432,6 +5477,36 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5447,6 +5522,11 @@ lodash.omit@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" integrity sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA= +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"