diff --git a/package.json b/package.json
index df24a9f..7610207 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"css-loader": "3.4.2",
"date-holidays": "^1.5.3",
"file-loader": "4.3.0",
+ "filesize": "^6.1.0",
"formik": "^2.1.5",
"formik-material-ui": "^3.0.0-alpha.0",
"html-webpack-plugin": "4.0.0-beta.11",
@@ -39,7 +40,8 @@
"jsonwebtoken": "^8.5.1",
"material-ui-dropzone": "^3.3.0",
"mdi-material-ui": "^6.17.0",
- "moment": "^2.26.0",
+ "moment-timezone": "^2.26.0",
+ "moment-timezone": "^0.5.31",
"node-sass": "^4.14.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"postcss-flexbugs-fixes": "4.1.0",
diff --git a/public/index.html b/public/index.html
index ecae82c..987754d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,15 +1,16 @@
-
-
-
-
-
- Zgłoszenie praktyki studenckiej
-
-
-
-
-
-
+
+
+
+
+
+ Zgłoszenie praktyki studenckiej
+
+
+
+
+
+
+
diff --git a/src/api/dto/edition.ts b/src/api/dto/edition.ts
index 2753f39..20d47fd 100644
--- a/src/api/dto/edition.ts
+++ b/src/api/dto/edition.ts
@@ -1,15 +1,21 @@
-import { Identifiable } from "@/data";
+import { Identifiable, InternshipProgramEntry } from "@/data";
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
import { OneWayTransformer, Transformer } from "@/serialization";
import { Edition } from "@/data/edition";
-import moment from "moment";
+import moment from "moment-timezone";
import { Subset } from "@/helpers";
+export interface ProgramEntryDTO extends Identifiable {
+ description: string;
+ descriptionEng: string;
+}
+
export interface EditionDTO extends Identifiable {
editionStart: string,
editionFinish: string,
reportingStart: string,
course: CourseDTO,
+ availableSubjects: ProgramEntryDTO[],
}
export interface EditionTeaserDTO extends Identifiable {
@@ -39,6 +45,7 @@ export const editionDtoTransformer: Transformer = {
editionStart: subject.startDate.toISOString(),
course: courseDtoTransformer.reverseTransform(subject.course),
reportingStart: subject.reportingStart.toISOString(),
+ availableSubjects: [],
};
},
transform(subject: EditionDTO, context: undefined): Edition {
@@ -55,3 +62,19 @@ export const editionDtoTransformer: Transformer = {
};
}
}
+
+export const programEntryDtoTransformer: Transformer = {
+ transform(subject: ProgramEntryDTO, context: never): InternshipProgramEntry {
+ return {
+ id: subject.id,
+ description: subject.description,
+ }
+ },
+ reverseTransform(subject: InternshipProgramEntry, context: never): ProgramEntryDTO {
+ return {
+ id: subject.id,
+ description: subject.description,
+ descriptionEng: "",
+ }
+ },
+}
diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts
index 34c61b0..acb869a 100644
--- a/src/api/dto/internship-registration.ts
+++ b/src/api/dto/internship-registration.ts
@@ -3,9 +3,10 @@ import { momentSerializationTransformer, OneWayTransformer } from "@/serializati
import { Nullable } from "@/helpers";
import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor";
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
import { sampleStudent } from "@/provider/dummy";
import { UploadType } from "@/api/upload";
+import { ProgramEntryDTO, programEntryDtoTransformer } from "@/api/dto/edition";
export enum SubmissionState {
Draft = "Draft",
@@ -36,6 +37,7 @@ export interface InternshipRegistrationUpdate {
type: number,
mentor: MentorDTO,
hours: number,
+ subjects: string[],
}
export interface InternshipRegistrationDTO extends Identifiable {
@@ -47,6 +49,7 @@ export interface InternshipRegistrationDTO extends Identifiable {
company: Company,
branchAddress: Office,
declaredHours: number,
+ subjects: { subject: ProgramEntryDTO }[],
}
export interface InternshipDocument extends Identifiable {
@@ -65,8 +68,8 @@ export interface InternshipInfoDTO {
export const internshipRegistrationUpdateTransformer: OneWayTransformer, Nullable> = {
transform(subject: Nullable, context?: unknown): Nullable {
return {
- start: subject?.startDate?.toISOString() || null,
- end: subject?.endDate?.toISOString() || null,
+ start: momentSerializationTransformer.transform(subject?.startDate) || null,
+ end: momentSerializationTransformer.transform(subject?.endDate) || null,
type: parseInt(subject?.type?.id || "0"),
mentor: mentorDtoTransformer.reverseTransform(subject.mentor as Mentor),
company: subject?.company?.id ? {
@@ -80,6 +83,7 @@ export const internshipRegistrationUpdateTransformer: OneWayTransformer program.id as string) || [],
}
}
}
@@ -97,7 +101,7 @@ export const internshipRegistrationDtoTransformer: OneWayTransformer programEntryDtoTransformer.transform(subject.subject)),
intern: sampleStudent, // fixme
};
}
diff --git a/src/api/edition.ts b/src/api/edition.ts
index b420620..62af39c 100644
--- a/src/api/edition.ts
+++ b/src/api/edition.ts
@@ -1,8 +1,9 @@
import { axios } from "@/api/index";
import { Edition } from "@/data/edition";
import { prepare } from "@/routing";
-import { EditionDTO, editionDtoTransformer, EditionTeaserDTO, editionTeaserDtoTransformer } from "@/api/dto/edition";
+import { EditionDTO, editionDtoTransformer, EditionTeaserDTO, editionTeaserDtoTransformer, programEntryDtoTransformer } from "@/api/dto/edition";
import { Subset } from "@/helpers";
+import { InternshipProgramEntry } from "@/data";
const EDITIONS_ENDPOINT = "/editions";
const EDITION_INFO_ENDPOINT = "/editions/:key";
@@ -41,11 +42,17 @@ export async function get(key: string): Promise | null> {
return editionTeaserDtoTransformer.transform(dto);
}
-export async function current(): Promise {
+export async function current(): Promise<{
+ edition: Edition,
+ program: InternshipProgramEntry[],
+}> {
const response = await axios.get(EDITION_CURRENT_ENDPOINT);
const dto = response.data;
- return editionDtoTransformer.transform(dto);
+ return {
+ edition: editionDtoTransformer.transform(dto),
+ program: dto.availableSubjects.map(programEntryDtoTransformer.transform as any),
+ };
}
export async function login(key: string): Promise {
diff --git a/src/api/internship.ts b/src/api/internship.ts
index 961c789..ba09dfa 100644
--- a/src/api/internship.ts
+++ b/src/api/internship.ts
@@ -1,14 +1,43 @@
-import { InternshipInfoDTO, InternshipRegistrationUpdate } from "@/api/dto/internship-registration";
+import { InternshipInfoDTO, InternshipRegistrationUpdate, SubmissionState } 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 {
- await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship);
+export type ValidationMessage = {
+ key: string;
+ parameters: { [name: string]: string },
+}
- return true;
+export class ValidationError extends Error {
+ public readonly messages: ValidationMessage[];
+
+ constructor(messages: ValidationMessage[], message: string = "There were validation errors.") {
+ super(message);
+ Object.setPrototypeOf(this, ValidationError.prototype);
+
+ this.messages = messages;
+ }
+}
+
+interface UpdateResponse {
+ status: SubmissionState;
+ errors?: string[];
+}
+
+export async function update(internship: Nullable): Promise {
+ const response = (await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship)).data;
+
+ if (response.status == SubmissionState.Draft) {
+ throw new ValidationError(
+ response.errors?.map(
+ msg => ({ key: msg, parameters: {} })
+ ) || []
+ );
+ }
+
+ return response.status;
}
export async function get(): Promise {
diff --git a/src/api/upload.ts b/src/api/upload.ts
index 81fa7b1..4657fce 100644
--- a/src/api/upload.ts
+++ b/src/api/upload.ts
@@ -1,6 +1,7 @@
import { axios } from "@/api/index";
import { InternshipDocument } from "@/api/dto/internship-registration";
import { prepare } from "@/routing";
+import { Identifiable } from "@/data";
export enum UploadType {
Ipp = "IppScan",
@@ -8,6 +9,12 @@ export enum UploadType {
Insurance = "NnwInsurance",
}
+export interface DocumentFileInfo extends Identifiable {
+ filename: string;
+ size: number;
+ mime: string;
+}
+
const CREATE_DOCUMENT_ENDPOINT = '/document';
const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan';
@@ -23,3 +30,8 @@ export async function upload(document: InternshipDocument, file: File) {
const response = await axios.put(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }), data);
return true;
}
+
+export async function fileinfo(document: InternshipDocument): Promise {
+ const response = await axios.get(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }));
+ return response.data;
+}
diff --git a/src/app.tsx b/src/app.tsx
index a3bb940..607090a 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -9,12 +9,11 @@ import '@/styles/overrides.scss'
import '@/styles/header.scss'
import '@/styles/footer.scss'
import classNames from "classnames";
-import { Edition } from "@/data/edition";
import { SettingActions } from "@/state/actions/settings";
import { useDispatch, UserActions } from "@/state/actions";
import { getLocale, Locale } from "@/state/reducer/settings";
import i18n from "@/i18n";
-import moment from "moment";
+import moment from "moment-timezone";
import { Container } from "@material-ui/core";
const UserMenu = (props: HTMLProps) => {
@@ -61,8 +60,6 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps)
}
function App() {
- const dispatch = useDispatch();
- const edition = useSelector(state => state.edition);
const { t } = useTranslation();
const locale = useSelector(state => getLocale(state.settings));
@@ -73,27 +70,29 @@ function App() {
}, [ locale ])
return <>
-
-
-
-

-
-
-
-
-
-
+
+
+
+
+

+
+
+
+
+
+
+
{
diff --git a/src/components/acceptance-action.tsx b/src/components/acceptance-action.tsx
new file mode 100644
index 0000000..670e2b9
--- /dev/null
+++ b/src/components/acceptance-action.tsx
@@ -0,0 +1,115 @@
+import React, { useState } from "react";
+import { Button, ButtonGroup, Dialog, DialogActions, DialogContent, DialogTitle, Menu, MenuItem, TextField, Typography } from "@material-ui/core";
+import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
+import { useTranslation } from "react-i18next";
+import { useVerticalSpacing } from "@/styles";
+import { createPortal } from "react-dom";
+
+type AcceptanceActionsProps = {
+ onAccept: (comment?: string) => void;
+ onDiscard: (comment: string) => void;
+ label: string;
+}
+
+export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActionsProps) {
+ const { t } = useTranslation();
+
+ const [isDiscardModalOpen, setDiscardModelOpen] = useState(false);
+ const [isAcceptModalOpen, setAcceptModelOpen] = useState(false);
+
+ const [comment, setComment] = useState("");
+ const [menuAnchor, setMenuAnchor] = useState(null);
+
+ const classes = useVerticalSpacing(3);
+
+ const handleAccept = () => {
+ onAccept(comment);
+ }
+
+ const handleDiscard = () => {
+ onDiscard(comment);
+ }
+
+ const handleAcceptModalClose = () => {
+ setAcceptModelOpen(false);
+ }
+
+ const handleDiscardModalClose = () => {
+ setDiscardModelOpen(false);
+ }
+
+ const handleDiscardAction = () => {
+ setDiscardModelOpen(true);
+ }
+
+ const handleAcceptMenuOpen = (ev: React.MouseEvent) => {
+ setMenuAnchor(ev.currentTarget);
+ }
+
+ const handleAcceptMenuClose = () => {
+ setMenuAnchor(null);
+ }
+
+ const handleAcceptWithComment = () => {
+ setAcceptModelOpen(true);
+ setMenuAnchor(null);
+ }
+
+ const handleAcceptWithoutComment = () => {
+ onAccept();
+ }
+
+ return <>
+
+ }>
+ { t('accept-without-comments') }
+
+
+
+
+
+
+ }>
+ { t('discard') }
+
+
+ { createPortal(<>
+
+
+
+ >, document.getElementById("modals") as Element) }
+ >
+}
diff --git a/src/components/async.tsx b/src/components/async.tsx
new file mode 100644
index 0000000..6b90ce6
--- /dev/null
+++ b/src/components/async.tsx
@@ -0,0 +1,29 @@
+import { AsyncResult } from "@/hooks";
+import React from "react";
+import { CircularProgress } from "@material-ui/core";
+import { Alert } from "@material-ui/lab";
+import { Loading } from "@/components/loading";
+
+type AsyncProps = {
+ async: AsyncResult,
+ children: (value: TValue) => JSX.Element,
+ loading?: () => JSX.Element,
+ error?: (error: TError) => JSX.Element,
+}
+
+const defaultLoading = () => ;
+const defaultError = (error: any) => { error.message };
+
+export function Async(
+ { async, children: render, loading = defaultLoading, error = defaultError }: AsyncProps
+) {
+ if (async.isLoading || (!async.error && !async.value)) {
+ return loading();
+ }
+
+ if (typeof async.error !== "undefined") {
+ return error(async.error);
+ }
+
+ return render(async.value as TValue);
+}
diff --git a/src/components/fileinfo.tsx b/src/components/fileinfo.tsx
new file mode 100644
index 0000000..544baee
--- /dev/null
+++ b/src/components/fileinfo.tsx
@@ -0,0 +1,93 @@
+import { InternshipDocument } from "@/api/dto/internship-registration";
+import { useAsync } from "@/hooks";
+import { DocumentFileInfo } from "@/api/upload";
+import React, { useCallback } from "react";
+import api from "@/api";
+import { Async } from "@/components/async";
+import { Button, Grid, Paper, PaperProps, SvgIconProps, Theme, Typography } from "@material-ui/core";
+import { makeStyles, createStyles } from "@material-ui/core/styles";
+import filesize from "filesize";
+import { Actions } from "@/components/actions";
+import { useTranslation } from "react-i18next";
+import { FileDownloadOutline, FileOutline, FileImageOutline, FilePdfOutline, FileWordOutline } from "mdi-material-ui";
+import classNames from "classnames";
+
+const useStyles = makeStyles((theme: Theme) => createStyles({
+ root: {
+ padding: theme.spacing(2),
+ backgroundColor: "#e9f0f5",
+ },
+ header: {
+ color: theme.palette.primary.dark,
+ },
+ download: {
+ color: theme.palette.primary.dark,
+ },
+ actions: {
+ marginTop: theme.spacing(2),
+ },
+ icon: {
+ fontSize: "6rem",
+ margin: "0 auto",
+ },
+ grid: {
+ display: "flex",
+ alignItems: "center",
+ },
+ iconColumn: {
+ flex: "0 1 auto",
+ marginRight: "1rem",
+ color: theme.palette.primary.dark + "af",
+ },
+ asideColumn: {
+ flex: "1 1 auto"
+ }
+}))
+
+export type FileInfoProps = {
+ document: InternshipDocument
+} & PaperProps;
+
+export type FileIconProps = {
+ mime: string;
+} & SvgIconProps;
+
+export function FileIcon({ mime, ...props }: FileIconProps) {
+ switch (true) {
+ case ["application/pdf", "application/x-pdf"].includes(mime):
+ return
+ case mime === "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
+ return
+ case mime.startsWith("image/"):
+ return
+ default:
+ return
+ }
+}
+
+export const FileInfo = ({ document, ...props }: FileInfoProps) => {
+ const fileinfo = useAsync(useCallback(() => api.upload.fileinfo(document), [document.id]));
+ const classes = useStyles();
+
+ const { t } = useTranslation();
+
+ return
+
+ { fileinfo =>
+
+
+
+
+
}
+
+
+}
diff --git a/src/components/loading.tsx b/src/components/loading.tsx
new file mode 100644
index 0000000..e791e42
--- /dev/null
+++ b/src/components/loading.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { createStyles, makeStyles } from "@material-ui/core/styles";
+import { CircularProgress, Typography } from "@material-ui/core";
+
+const useStyles = makeStyles(theme => createStyles({
+ root: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ "& > :not(:last-child)": {
+ marginBottom: theme.spacing(2),
+ }
+ }
+}))
+
+export type LoadingProps = {
+ size?: string | number;
+ label?: string;
+};
+
+export function Loading({ size, label, ...props }: LoadingProps) {
+ const classes = useStyles();
+
+ return
+
+ { label && { label } }
+
+}
diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx
index b452875..d169680 100644
--- a/src/components/proposalPreview.tsx
+++ b/src/components/proposalPreview.tsx
@@ -1,12 +1,13 @@
import { Internship } from "@/data";
import React from "react";
-import { Typography } from "@material-ui/core";
+import { List, Typography, ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { useVerticalSpacing } from "@/styles";
-import moment from "moment";
+import moment from "moment-timezone";
import { Label, Section } from "@/components/section";
import { StudentPreview } from "@/pages/user/profile";
+import { Check, StickerCheck } from "mdi-material-ui";
export type ProposalPreviewProps = {
proposal: Internship;
@@ -42,6 +43,16 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
{ proposal.type.label.pl }
+
+
+
+ { proposal.program.map(subject =>
+
+ { subject.description }
+ ) }
+
+
+
diff --git a/src/components/step.tsx b/src/components/step.tsx
index fa08903..3f322a7 100644
--- a/src/components/step.tsx
+++ b/src/components/step.tsx
@@ -1,10 +1,11 @@
-import moment, { Moment } from "moment";
+import moment, { Moment } from "moment-timezone";
import { Box, Step as StepperStep, StepContent, StepLabel, StepProps as StepperStepProps, Typography } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import React, { ReactChild, useMemo } from "react";
import { StepIcon } from "@/components/stepIcon";
type StepProps = StepperStepProps & {
+ notBefore?: Moment;
until?: Moment;
completedOn?: Moment;
label: string;
@@ -17,7 +18,7 @@ type StepProps = StepperStepProps & {
const now = moment();
export const Step = (props: StepProps) => {
- const { until, label, completedOn, children, completed = false, declined = false, waiting = false, state = null, ...rest } = props;
+ const { until, notBefore, label, completedOn, children, completed = false, declined = false, waiting = false, state = null, ...rest } = props;
const { t } = useTranslation();
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
@@ -26,18 +27,22 @@ export const Step = (props: StepProps) => {
return
{ label }
- { until &&
- { state && <>
- { state }
+
+ { state && { state } }
+ { notBefore &&
+
+ { t('not-before', { date: notBefore }) }
+ }
+ { until && <>
•
+
+ { t('until', { date: until }) }
+ { isLate && - { t('late', { by: moment.duration(now.diff(until)) }) } }
+ { !isLate && !completed && - { t('left', { left: left }) } }
+
> }
-
- { t('until', { date: until }) }
- { isLate && - { t('late', { by: moment.duration(now.diff(until)) }) } }
- { !isLate && !completed && - { t('left', { left: left }) } }
-
- }
+
{ children && { children } }
diff --git a/src/data/course.ts b/src/data/course.ts
index 6bcb065..568fa37 100644
--- a/src/data/course.ts
+++ b/src/data/course.ts
@@ -5,5 +5,4 @@ import { Identifiable } from "./common";
export interface Course extends Identifiable {
name: string,
desiredSemesters: Semester[],
- possibleProgramEntries: InternshipProgramEntry[];
}
diff --git a/src/data/edition.ts b/src/data/edition.ts
index 6ca8725..4e11994 100644
--- a/src/data/edition.ts
+++ b/src/data/edition.ts
@@ -1,4 +1,4 @@
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
import { Course } from "@/data/course";
import { Identifiable } from "@/data/common";
diff --git a/src/data/internship.ts b/src/data/internship.ts
index d3e946e..b819581 100644
--- a/src/data/internship.ts
+++ b/src/data/internship.ts
@@ -1,4 +1,4 @@
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
import { Identifiable, Multilingual } from "./common";
import { Student } from "@/data/student";
import { Company, Office } from "@/data/company";
diff --git a/src/forms/company.tsx b/src/forms/company.tsx
index 246b1c2..d4896fd 100644
--- a/src/forms/company.tsx
+++ b/src/forms/company.tsx
@@ -194,7 +194,7 @@ export const CompanyForm: React.FunctionComponent = () => {
return (
<>
-
+
typeof option === "string" ? option : option.name }
renderOption={ company => }
diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx
index c36b3a3..f5bfe04 100644
--- a/src/forms/internship.tsx
+++ b/src/forms/internship.tsx
@@ -1,16 +1,28 @@
-import React, { HTMLProps, useMemo, useState } from "react";
-import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Grid, TextField, Typography } from "@material-ui/core";
+import React, { HTMLProps, useEffect, useMemo, useRef, useState } from "react";
+import {
+ Button,
+ Checkbox,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ FormControlLabel,
+ FormGroup,
+ Grid,
+ TextField,
+ Typography
+} from "@material-ui/core";
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, Office, Student } from "@/data";
+import { Company, Internship, InternshipProgramEntry, InternshipType, Office, Student } from "@/data";
import { Nullable } from "@/helpers";
-import moment, { Moment } from "moment";
+import { Moment } from "moment-timezone";
import { computeWorkingHours } from "@/utils/date";
-import { Autocomplete } from "@material-ui/lab";
+import { Alert, AlertTitle, Autocomplete } from "@material-ui/lab";
import { emptyInternship } from "@/provider/dummy/internship";
-import { useDispatch } from "@/state/actions";
+import { InternshipProposalActions, useDispatch } from "@/state/actions";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
@@ -25,6 +37,8 @@ import { TextField as TextFieldFormik } from "formik-material-ui"
import { useCurrentEdition, useCurrentStudent, useInternshipTypes, useUpdateEffect } from "@/hooks";
import { internshipRegistrationUpdateTransformer } from "@/api/dto/internship-registration";
import api from "@/api";
+import FormLabel from "@material-ui/core/FormLabel";
+import { ValidationError, ValidationMessage } from "@/api/internship";
export type InternshipFormValues = {
startDate: Moment | null;
@@ -43,6 +57,7 @@ export type InternshipFormValues = {
mentorEmail: string;
mentorPhone: string;
kindOther: string | null;
+ program: InternshipProgramEntry[];
// relations
kind: InternshipType | null;
@@ -72,6 +87,7 @@ const emptyInternshipValues: InternshipFormValues = {
startDate: null,
student: sampleStudent,
workingHours: 40,
+ program: [],
}
export const InternshipTypeItem = ({ internshipType: type, ...props }: { internshipType: InternshipType } & HTMLProps) => {
@@ -86,9 +102,24 @@ export const InternshipTypeItem = ({ internshipType: type, ...props }: { interns
const InternshipProgramForm = () => {
const { t } = useTranslation();
const { values, handleBlur, setFieldValue, errors } = useFormikContext();
+ const [ selectedProgramEntries, setSelectedProgramEntries ] = useState(values.program);
+
+ const possibleProgramEntries = useSelector(state => state.edition.program);
const types = useInternshipTypes();
+ const handleProgramEntryChange = (entry: InternshipProgramEntry) => (ev: any) => {
+ if (ev.target.checked) {
+ setSelectedProgramEntries([ ...selectedProgramEntries, entry ]);
+ } else {
+ setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur != entry));
+ }
+ }
+
+ useEffect(() => {
+ setFieldValue("program", selectedProgramEntries);
+ }, [ selectedProgramEntries ])
+
return (
@@ -108,6 +139,20 @@ const InternshipProgramForm = () => {
{/* */}
{/* }*/}
{/**/}
+
+
+ { t('forms.internship.fields.program', { count: 3 }) }
+ { possibleProgramEntries.map(
+ entry => }
+ checked={ selectedProgramEntries.find(cur => entry.id == cur.id) !== undefined }
+ onChange={ handleProgramEntryChange(entry) }
+ label={ entry.description }
+ key={ entry.id }
+ />
+ ) }
+
+
)
}
@@ -137,19 +182,22 @@ const InternshipDurationForm = () => {
return (
- setFieldValue("startDate", value) }
- format="DD MMMM yyyy"
+ setFieldValue("startDate", value) }
+ format="DD.MM.yyyy"
disableToolbar fullWidth
- variant="inline" label={ t("forms.internship.fields.start-date") }
- minDate={ moment() }
+ variant="inline"
+ label={ t("forms.internship.fields.start-date") }
/>
- setFieldValue("endDate", value) }
- format="DD MMMM yyyy"
+ setFieldValue("endDate", value) }
+ format="DD.MM.yyyy"
disableToolbar fullWidth
- variant="inline" label={ t("forms.internship.fields.end-date") }
- minDate={ startDate || moment() }
+ variant="inline"
+ label={ t("forms.internship.fields.end-date") }
+ minDate={ startDate }
/>
@@ -207,6 +255,7 @@ const converter: Transformer, InternshipFormValues, Interns
mentorLastName: internship.mentor?.surname || "",
mentorPhone: internship.mentor?.phone || "",
workingHours: 40,
+ program: internship.program || [],
}
},
reverseTransform(form: InternshipFormValues, context: InternshipConverterContext): Nullable {
@@ -236,12 +285,18 @@ const converter: Transformer, InternshipFormValues, Interns
},
hours: form.hours ? form.hours : 0,
type: form.kind as InternshipType,
+ program: form.program,
}
}
}
export const InternshipForm: React.FunctionComponent = () => {
- const student = useCurrentStudent();
+ const student = useCurrentStudent();
+ const history = useHistory();
+ const root = useRef(null);
+ const dispatch = useDispatch();
+
+ const [errors, setErrors] = useState([]);
const initialInternship = useSelector>(state => getInternshipProposal(state.proposal) || {
...emptyInternship,
@@ -252,12 +307,8 @@ export const InternshipForm: React.FunctionComponent = () => {
});
const edition = useCurrentEdition();
-
const { t } = useTranslation();
- const dispatch = useDispatch();
- const history = useHistory();
-
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const validationSchema = Yup.object>({
@@ -285,6 +336,7 @@ 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")),
+ program: Yup.array() as any,
// kindOther: Yup.string().when("kind", {
// is: (values: InternshipFormValues) => values?.kind === InternshipType.Other,
// then: Yup.string().required(t("validation.required"))
@@ -293,17 +345,25 @@ export const InternshipForm: React.FunctionComponent = () => {
const values = converter.transform(initialInternship);
- const handleSubmit = (values: InternshipFormValues) => {
+ const handleSubmit = async (values: InternshipFormValues) => {
setConfirmDialogOpen(false);
const internship = converter.reverseTransform(values, { internship: initialInternship as Internship });
const update = internshipRegistrationUpdateTransformer.transform(internship);
- console.log(update);
+ try {
+ await api.internship.update(update);
+ dispatch({ type: InternshipProposalActions.Send });
- api.internship.update(update);
-
- // history.push(route("home"))
+ history.push(route("home"))
+ } catch (error) {
+ if (error instanceof ValidationError) {
+ setErrors(error.messages);
+ root.current?.scrollIntoView({ behavior: "smooth" })
+ } else {
+ throw error;
+ }
+ }
}
const InnerForm = () => {
@@ -323,10 +383,16 @@ export const InternshipForm: React.FunctionComponent = () => {
setConfirmDialogOpen(false);
}
- return
diff --git a/src/provider/dummy/edition.ts b/src/provider/dummy/edition.ts
index 995da58..1c464ff 100644
--- a/src/provider/dummy/edition.ts
+++ b/src/provider/dummy/edition.ts
@@ -1,5 +1,5 @@
import { Edition } from "@/data/edition";
-import moment from "moment";
+import moment from "moment-timezone";
import { sampleCourse } from "@/provider/dummy/student";
export const sampleEdition: Edition = {
diff --git a/src/provider/dummy/internship.ts b/src/provider/dummy/internship.ts
index 9f10d5b..31c4011 100644
--- a/src/provider/dummy/internship.ts
+++ b/src/provider/dummy/internship.ts
@@ -13,7 +13,7 @@ export const emptyInternship: Nullable = {
endDate: null,
startDate: null,
type: null,
- program: null,
+ program: [],
isAccepted: false,
lengthInWeeks: 0,
mentor: emptyMentor,
diff --git a/src/provider/dummy/student.ts b/src/provider/dummy/student.ts
index 38272d7..9033f53 100644
--- a/src/provider/dummy/student.ts
+++ b/src/provider/dummy/student.ts
@@ -25,7 +25,6 @@ export const sampleCourse: Course = {
id: courseIdSequence(),
name: "Informatyka",
desiredSemesters: [6],
- possibleProgramEntries: sampleProgramEntries,
}
export const sampleStudent: Student = {
diff --git a/src/serialization/edition.ts b/src/serialization/edition.ts
index ae4555c..6c1ced6 100644
--- a/src/serialization/edition.ts
+++ b/src/serialization/edition.ts
@@ -1,7 +1,7 @@
import { Serializable, SerializationTransformer } from "@/serialization/types";
import { Edition } from "@/data/edition";
import { momentSerializationTransformer } from "@/serialization/moment";
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
export const editionSerializationTransformer: SerializationTransformer = {
transform(subject: Edition, context?: unknown): Serializable {
diff --git a/src/serialization/internship.ts b/src/serialization/internship.ts
index 9a9f628..326091f 100644
--- a/src/serialization/internship.ts
+++ b/src/serialization/internship.ts
@@ -1,7 +1,7 @@
import { Internship, InternshipType } from "@/data";
import { Serializable, SerializationTransformer } from "@/serialization/types";
import { momentSerializationTransformer } from "@/serialization/moment";
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
export const internshipSerializationTransformer: SerializationTransformer = {
transform: (internship: Internship): Serializable => ({
diff --git a/src/serialization/moment.ts b/src/serialization/moment.ts
index ae53d77..50cc164 100644
--- a/src/serialization/moment.ts
+++ b/src/serialization/moment.ts
@@ -1,7 +1,7 @@
import { SerializationTransformer } from "@/serialization/types";
-import moment, { Moment } from "moment";
+import moment, { Moment } from "moment-timezone";
export const momentSerializationTransformer: SerializationTransformer = {
- transform: (subject: Moment) => subject && subject.toISOString(),
+ transform: (subject: Moment) => subject && subject.clone().utc(false).add(subject.utcOffset(), 'minutes').toISOString(),
reverseTransform: (subject: string) => subject ? moment(subject) : null,
}
diff --git a/src/serialization/types.ts b/src/serialization/types.ts
index befd035..424786d 100644
--- a/src/serialization/types.ts
+++ b/src/serialization/types.ts
@@ -1,4 +1,4 @@
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
type Simplify = string |
T extends string ? string :
diff --git a/src/state/actions/edition.ts b/src/state/actions/edition.ts
index f25b853..cbb30aa 100644
--- a/src/state/actions/edition.ts
+++ b/src/state/actions/edition.ts
@@ -1,5 +1,6 @@
import { Action } from "@/state/actions/base";
import { Edition } from "@/data/edition";
+import { InternshipProgramEntry } from "@/data";
export enum EditionActions {
Set = 'SET_EDITION',
@@ -7,6 +8,7 @@ export enum EditionActions {
export interface SetAction extends Action {
edition: Edition,
+ program: InternshipProgramEntry[],
}
export type EditionAction = SetAction;
diff --git a/src/state/actions/plan.ts b/src/state/actions/plan.ts
index 90a0cdb..cafc834 100644
--- a/src/state/actions/plan.ts
+++ b/src/state/actions/plan.ts
@@ -7,6 +7,7 @@ import {
} from "@/state/actions/submission";
import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration";
+import { Action } from "@/state/actions/base";
export enum InternshipPlanActions {
Send = "SEND_PLAN",
@@ -14,8 +15,11 @@ export enum InternshipPlanActions {
Approve = "RECEIVE_PLAN_APPROVE",
Decline = "RECEIVE_PLAN_DECLINE",
Receive = "RECEIVE_PLAN_STATE",
+ Reset = "RESET_PLAN",
}
+export interface ResetPlanAction extends Action {}
+
export interface SendPlanAction extends SendSubmissionAction {
document: InternshipDocument;
}
@@ -40,4 +44,6 @@ export type InternshipPlanAction
| SavePlanAction
| ReceivePlanApproveAction
| ReceivePlanDeclineAction
- | ReceivePlanUpdateAction;
+ | ReceivePlanUpdateAction
+ | ResetPlanAction
+ ;
diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts
index 2db7692..1520e25 100644
--- a/src/state/actions/proposal.ts
+++ b/src/state/actions/proposal.ts
@@ -17,7 +17,6 @@ export enum InternshipProposalActions {
}
export interface SendProposalAction extends SendSubmissionAction {
- internship: Internship;
}
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction {
diff --git a/src/state/reducer/edition.ts b/src/state/reducer/edition.ts
index 43084d4..6b970b3 100644
--- a/src/state/reducer/edition.ts
+++ b/src/state/reducer/edition.ts
@@ -2,15 +2,26 @@ import { Edition } from "@/data/edition";
import { EditionAction, EditionActions } from "@/state/actions/edition";
import { editionSerializationTransformer, Serializable } from "@/serialization";
import { LoginAction, LogoutAction, UserActions } from "@/state/actions";
+import { InternshipProgramEntry } from "@/data";
-export type EditionState = Serializable | null;
+export type EditionState = Serializable<{
+ edition: Edition | null,
+ program: InternshipProgramEntry[],
+}>
-const initialEditionState: EditionState = null;
+const initialEditionState: EditionState = {
+ edition: null,
+ program: [],
+};
const editionReducer = (state: EditionState = initialEditionState, action: EditionAction | LogoutAction | LoginAction): EditionState => {
switch (action.type) {
case EditionActions.Set:
- return editionSerializationTransformer.transform(action.edition);
+ return {
+ ...state,
+ edition: editionSerializationTransformer.transform(action.edition),
+ program: action.program,
+ };
case UserActions.Login:
case UserActions.Logout:
return initialEditionState;
diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts
index 6374056..d57303b 100644
--- a/src/state/reducer/index.ts
+++ b/src/state/reducer/index.ts
@@ -22,4 +22,4 @@ export type AppState = ReturnType;
export default rootReducer;
-export const isReady = (state: AppState) => !!state.edition;
+export const isReady = (state: AppState) => !!(state.edition?.edition);
diff --git a/src/state/reducer/plan.ts b/src/state/reducer/plan.ts
index b89f661..3887ce0 100644
--- a/src/state/reducer/plan.ts
+++ b/src/state/reducer/plan.ts
@@ -1,4 +1,4 @@
-import { InternshipPlanAction, InternshipPlanActions, InternshipProposalActions } from "@/state/actions";
+import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions";
import { Serializable } from "@/serialization/types";
import {
createSubmissionReducer,
@@ -10,6 +10,7 @@ import {
import { Reducer } from "react";
import { SubmissionAction } from "@/state/actions/submission";
import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
+import { Api } from "mdi-material-ui";
export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & {
document: Serializable | null;
@@ -40,9 +41,14 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla
document: action.document,
}
case InternshipPlanActions.Receive:
+ if (state.overwritten) {
+ return state;
+ }
+
return {
...state,
accepted: action.state === ApiSubmissionState.Accepted,
+ declined: action.state === ApiSubmissionState.Rejected,
sent: [
ApiSubmissionState.Accepted,
ApiSubmissionState.Rejected,
@@ -51,6 +57,9 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla
document: action.document,
}
+ case InternshipPlanActions.Reset:
+ return defaultInternshipPlanState;
+
default:
return state;
}
diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts
index d20eeb4..6370976 100644
--- a/src/state/reducer/proposal.ts
+++ b/src/state/reducer/proposal.ts
@@ -42,12 +42,16 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter
case InternshipProposalActions.Send:
return {
...state,
- proposal: internshipSerializationTransformer.transform(action.internship),
}
case InternshipProposalActions.Receive:
+ if (state.overwritten) {
+ return state;
+ }
+
return {
...state,
accepted: action.state === ApiSubmissionState.Accepted,
+ declined: action.state === ApiSubmissionState.Rejected,
sent: [
ApiSubmissionState.Accepted,
ApiSubmissionState.Rejected,
diff --git a/src/state/reducer/submission.ts b/src/state/reducer/submission.ts
index 864cf2f..0cbd62a 100644
--- a/src/state/reducer/submission.ts
+++ b/src/state/reducer/submission.ts
@@ -1,7 +1,7 @@
import { DeanApproval } from "@/data/deanApproval";
import { Action } from "@/state/actions";
import { momentSerializationTransformer } from "@/serialization";
-import moment from "moment";
+import moment from "moment-timezone";
import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, SubmissionAction } from "@/state/actions/submission";
export type SubmissionStatus = "draft" | "awaiting" | "accepted" | "declined";
@@ -12,6 +12,7 @@ export type SubmissionState = {
sentOn: string | null;
declined: boolean;
comment: string | null;
+ overwritten: boolean;
}
export type MayRequireDeanApproval = {
@@ -24,6 +25,7 @@ export const defaultSubmissionState: SubmissionState = {
sentOn: null,
declined: false,
comment: null,
+ overwritten: false,
}
export const defaultDeanApprovalsState: MayRequireDeanApproval = {
@@ -56,6 +58,7 @@ export function createSubmissionReducer).comment,
+ overwritten: true,
}
case SubmissionAction.Decline:
return {
@@ -63,6 +66,7 @@ export function createSubmissionReducer).comment,
+ overwritten: true,
}
case SubmissionAction.Send:
return {
@@ -72,6 +76,7 @@ export function createSubmissionReducer *:not(:last-child) {
+ margin-bottom: 1rem;
+ }
+}
diff --git a/src/styles/spacing.ts b/src/styles/spacing.ts
index dc8863d..b6a3339 100644
--- a/src/styles/spacing.ts
+++ b/src/styles/spacing.ts
@@ -17,3 +17,16 @@ export const useHorizontalSpacing = makeStyles(theme => createStyles({
}
}
}))
+
+export const useSpacing = makeStyles(theme => createStyles({
+ horizontal: {
+ "& > *:not(:last-child)": {
+ marginRight: (spacing: number = defaultSpacing) => theme.spacing(spacing)
+ }
+ },
+ vertical: {
+ "& > *:not(:last-child)": {
+ marginBottom: (spacing: number = defaultSpacing) => theme.spacing(spacing)
+ }
+ }
+}))
diff --git a/src/ui/theme.ts b/src/ui/theme.ts
index cf65aad..9a1f07b 100644
--- a/src/ui/theme.ts
+++ b/src/ui/theme.ts
@@ -4,7 +4,6 @@ export const studentTheme = responsiveFontSizes(createMuiTheme({
props: {
MuiGrid: {
spacing: 3,
- xs: 12,
},
MuiContainer: {
maxWidth: "md"
diff --git a/src/utils/date.ts b/src/utils/date.ts
index b42eadf..054d559 100644
--- a/src/utils/date.ts
+++ b/src/utils/date.ts
@@ -1,4 +1,4 @@
-import { Moment } from "moment";
+import { Moment } from "moment-timezone";
import Holidays from "date-holidays";
const holidays = new Holidays()
diff --git a/translations/pl.yaml b/translations/pl.yaml
index 4ec3a5c..32e762e 100644
--- a/translations/pl.yaml
+++ b/translations/pl.yaml
@@ -2,10 +2,12 @@
copyright: Wydział ETI Politechniki Gdańskiej © {{ date, YYYY }}
login: zaloguj się
+login-in-progress: Logowanie w toku, proszę czekać...
logout: wyloguj się
logged-in-as: zalogowany jako <1>{{ name }}1>
until: do {{ date, DD MMMM YYYY }}
+not-before: od {{ date, DD MMMM YYYY }}
late: '{{ by, humanize }} spóźnienia'
left: jeszcze {{ left, humanize }}
@@ -79,6 +81,7 @@ forms:
country: Kraj
street: Ulica
building: Nr budynku
+ program: Program praktyki (wybierz {{ count }})
help:
weeks: Wartość wyliczana automatycznie
working-hours: Liczba godzin w tygodniu roboczym
@@ -112,6 +115,11 @@ submission:
draft: "wersja robocza"
internship:
+ validation:
+ has-errors: "W formularzu zostały znalezione błędy"
+ error:
+ declared_hours:
+ empty: "Brak zadeklarowanej długości praktyki."
intern:
semester: semestr {{ semester, roman }}
album: "numer albumu {{ album }}"
@@ -133,6 +141,7 @@ internship:
place: "Miejsce odbywania praktyki"
kind: "Rodzaj i program praktyki"
mentor: "Zakładowy opiekun praktyki"
+ program: "Realizowane punkty programu praktyki"
discard:
title: "Odrzuć zgłoszenie praktyki"
info: "Poniższa informacja zostanie przekazana praktykantowi w celu poprawy zgłoszenia."
@@ -140,6 +149,14 @@ internship:
title: "Zaakceptuj zgłoszenie praktyki"
info: "Poniższa informacja zostanie przekazana praktykantowi."
+plan:
+ discard:
+ title: "Odrzuć indywidualny program praktyki"
+ info: "Poniższa informacja zostanie przekazana praktykantowi w celu poprawy zgłoszenia."
+ accept:
+ title: "Zaakceptuj indywidualny program praktyki"
+ info: "Poniższa informacja zostanie przekazana praktykantowi."
+
steps:
personal-data:
header: "Uzupełnienie danych"
@@ -155,7 +172,8 @@ steps:
header: "Zgłoszenie praktyki"
info:
draft: >
- Przed podjęciem praktyki należy ją zgłosić. (TODO)
+ Przed podjęciem praktyki należy ją zgłosić - w tym celu należy elektronicznie wypełnić formularz zgłoszenia
+ praktyki.
awaiting: >
Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o
akceptacji bądź konieczności wprowadzenia zmian.
@@ -171,7 +189,8 @@ steps:
info:
draft: >
W porozumieniu z firmą w której odbywają się praktyki należy sporządzić Indywidualny Plan Praktyk zgodnie z
- załączonym szablonem a następnie wysłać go do weryfikacji. (TODO)
+ załączonym szablonem a następnie wysłać go do weryfikacji. Indywidualny Plan Praktyk musi zostać zatwierdzony
+ oraz podpisany przez Twojego zakłądowego opiekuna praktyki.
awaiting: >
Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i
zatwierdzony. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian.
@@ -200,3 +219,4 @@ validation:
minimum-hours: "Minimalna liczba godzin wynosi {{ hours }}"
contact-coordinator: "Skontaktuj się z koordynatorem"
+download: "pobierz"
diff --git a/yarn.lock b/yarn.lock
index e5383ca..281e4cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3858,6 +3858,11 @@ filesize@6.0.1:
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.0.1.tgz#f850b509909c7c86f7e450ea19006c31c2ed3d2f"
integrity sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg==
+filesize@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00"
+ integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==
+
fill-range@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"