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..b7a94a7 --- /dev/null +++ b/src/api/dto/internship-registration.ts @@ -0,0 +1,33 @@ +import { Identifiable, Internship, Mentor } from "@/data"; +import { OneWayTransformer } from "@/serialization"; +import { Nullable } from "@/helpers"; + +export interface InternshipRegistrationUpdateCompany { + id: string, + branchOffice: Identifiable, +} + +export interface InternshipRegistrationUpdate { + company: InternshipRegistrationUpdateCompany, + start: string, + end: string, + type: number, + mentor: Mentor, +} + +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: subject.mentor, + company: { + id: subject?.company?.id as string, + branchOffice: { + id: subject?.office?.id + }, + } + } + } +} 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..cb20750 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,8 +5,11 @@ 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"; export const axios = Axios.create({ baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/", @@ -33,7 +36,10 @@ const api = { user, edition, page, - student + student, + type, + companies, + internship, } export default api; diff --git a/src/api/internship.ts b/src/api/internship.ts new file mode 100644 index 0000000..3096b15 --- /dev/null +++ b/src/api/internship.ts @@ -0,0 +1,11 @@ +import { InternshipRegistrationUpdate } from "@/api/dto/internship-registration"; +import { axios } from "@/api/index"; +import { Nullable } from "@/helpers"; + +const INTERNSHIP_ENDPOINT = '/internshipRegistration'; + +export async function update(internship: Nullable): Promise { + const response = await axios.put(INTERNSHIP_ENDPOINT, internship); + + return true; +} 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/user.ts b/src/api/user.ts index 831c87b..cd2a0de 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -9,7 +9,7 @@ const AUTHORIZE_URL = process.env.AUTHORIZE || "https://logowanie.pg.edu.pl/oaut export async function login(code?: string): Promise { const response = code - ? await axios.get(LOGIN_ENDPOINT, { params: { 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 8a17cd0..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"; @@ -39,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/forms/company.tsx b/src/forms/company.tsx index 74fbfdb..21a5300 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({ @@ -143,8 +149,17 @@ 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(() => { + (async () => { + setCompanies(await api.companies.search(input)); + })() + }, [ input ]); + const handleCompanyChange = (event: any, value: Company | string | null) => { setFieldTouched("companyName", true); @@ -174,13 +189,14 @@ export const CompanyForm: React.FunctionComponent = () => { <> - typeof option === "string" ? option : option.name } renderOption={ company => } renderInput={ props => } onChange={ handleCompanyChange } value={ values.company || values.companyName } freeSolo + onInputChange={ (_, value) => setInput(value) } /> diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 10fb302..3ce409a 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 } onChange={ (_, value) => setFieldValue("kind", value) } onBlur={ handleBlur } /> - - { - values.kind === InternshipType.Other && - - } - + {/**/} + {/* {*/} + {/* values.kind === InternshipType.Other &&*/} + {/* */} + {/* }*/} + {/**/} ) } @@ -240,19 +241,20 @@ const converter: Transformer, InternshipFormValues, Interns } 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,14 +296,12 @@ 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")) + api.internship.update(update); + + // history.push(route("home")) } const InnerForm = () => { diff --git a/src/forms/student.tsx b/src/forms/student.tsx index 1c8ee26..189c3c4 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,8 +27,8 @@ export const StudentForm = () => { course.name } renderInput={ props => } - options={[ sampleCourse ]} - value={ student.course } + options={[ course ]} + value={ course } disabled /> diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 544e1f0..3c9050a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,3 +2,4 @@ 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/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/routing.tsx b/src/routing.tsx index f7d37e5..8b88172 100644 --- a/src/routing.tsx +++ b/src/routing.tsx @@ -67,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/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