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("accept-with-comments") } + + + + + { createPortal(<> + + { t(label + ".discard.title") } + + { t(label + ".discard.info") } + setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> + + + + + + + + + + { t(label + ".accept.title") } + + { t(label + ".accept.info") } + setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> + + + + + + + + , 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
+ return + { errors.length > 0 && + { t('internship.validation.has-errors') } +
    + { errors.map(message =>
  • { t(`internship.validation.${message.key}`, message.parameters) }
  • ) } +
+
} { t('internship.sections.intern-info') } - { t('internship.sections.kind' )} + { t('internship.sections.kind') } { t('internship.sections.duration') } diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index b7f2021..57e560d 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -41,19 +41,19 @@ export const PlanForm = () => { } return - + { t('forms.plan.instructions') } - + - + setFile(files[0]) }/> { t('forms.plan.dropzone-help') } - + }> Powyższe dane nie są poprawne? diff --git a/src/forms/user.tsx b/src/forms/user.tsx index 9b450bc..cdf1e7f 100644 --- a/src/forms/user.tsx +++ b/src/forms/user.tsx @@ -10,6 +10,8 @@ import { Actions } from "@/components"; import { Nullable } from "@/helpers"; import * as Yup from "yup"; import { StudentActions, useDispatch } from "@/state/actions"; +import { route } from "@/routing"; +import { useHistory } from "react-router-dom"; interface StudentFormValues { firstName: string; @@ -48,6 +50,7 @@ const studentToFormValuesTransformer: Transformer, StudentForm export const StudentForm = ({ student }: StudentFormProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); + const history = useHistory(); const validationSchema = useMemo(() => Yup.object({ semester: Yup.number().required().min(1).max(10), @@ -71,6 +74,8 @@ export const StudentForm = ({ student }: StudentFormProps) => { type: StudentActions.Set, student: updated, }) + + history.push(route("home")); } diff --git a/src/hooks/state.ts b/src/hooks/state.ts index d024dcd..9fc52a6 100644 --- a/src/hooks/state.ts +++ b/src/hooks/state.ts @@ -9,7 +9,7 @@ export const useCurrentStudent = () => useSelector( ) export const useCurrentEdition = () => useSelector( - state => state.edition && editionSerializationTransformer.reverseTransform(state.edition) + state => state.edition?.edition && editionSerializationTransformer.reverseTransform(state.edition.edition) ) export const useDeadlines = () => { diff --git a/src/i18n.ts b/src/i18n.ts index 84cc091..a5e47ec 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -4,7 +4,7 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector"; import "moment/locale/pl" import "moment/locale/en-gb" -import moment, { isDuration, isMoment, unitOfTime } from "moment"; +import moment, { isDuration, isMoment, unitOfTime } from "moment-timezone"; import { convertToRoman } from "@/utils/numbers"; const resources = { diff --git a/src/index.tsx b/src/index.tsx index 2c53768..6df4691 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,7 +7,7 @@ import store, { persistor } from "@/state/store"; import { PersistGate } from "redux-persist/integration/react"; import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles"; import { MuiPickersUtilsProvider } from "@material-ui/pickers"; -import moment, { Moment } from "moment"; +import moment, { Moment } from "moment-timezone"; import { studentTheme } from "@/ui/theme"; import { BrowserRouter } from "react-router-dom"; import MomentUtils from "@date-io/moment"; diff --git a/src/pages/edition/pick.tsx b/src/pages/edition/pick.tsx index 635d225..2047368 100644 --- a/src/pages/edition/pick.tsx +++ b/src/pages/edition/pick.tsx @@ -25,11 +25,12 @@ export const loginToEdition = (id: string) => async (dispatch: AppDispatch) => { token, }) - const edition = await api.edition.current(); + const { edition, program } = await api.edition.current(); dispatch({ type: EditionActions.Set, - edition + edition, + program, }) } diff --git a/src/pages/fallback.tsx b/src/pages/fallback.tsx index ff1584f..c383002 100644 --- a/src/pages/fallback.tsx +++ b/src/pages/fallback.tsx @@ -5,6 +5,8 @@ import React, { useMemo } from "react"; import { route } from "@/routing"; import { useAsync } from "@/hooks"; import api from "@/api"; +import { Loading } from "@/components/loading"; +import { useSpacing } from "@/styles"; export const FallbackPage = () => { const location = useLocation(); @@ -13,23 +15,23 @@ export const FallbackPage = () => { const { isLoading, value, error } = useAsync(promise); - console.log({ isLoading, value, error, location }); - if (isLoading) { - return + return
} if (error) { return - 404 - Strona nie została znaleziona + + 404 + Strona nie została znaleziona - - + + + + + - - } diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index 1b3a814..a108399 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -28,6 +28,7 @@ import { Actions } from "@/components"; import { InternshipProposalActions, useDispatch } from "@/state/actions"; import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui/index"; import { useVerticalSpacing } from "@/styles"; +import { AcceptanceActions } from "@/components/acceptance-action"; export const InternshipProposalFormPage = () => { const { t } = useTranslation(); @@ -54,49 +55,13 @@ export const InternshipProposalPreviewPage = () => { const dispatch = useDispatch(); const history = useHistory(); - const [isDiscardModalOpen, setDiscardModelOpen] = useState(false); - const [isAcceptModalOpen, setAcceptModelOpen] = useState(false); - - const [comment, setComment] = useState(""); - const [menuAnchor, setMenuAnchor] = useState(null); - - const handleAccept = () => { - dispatch({ type: InternshipProposalActions.Approve, comment }); + const handleAccept = (comment?: string) => { + dispatch({ type: InternshipProposalActions.Approve, comment: comment || null }); history.push(route("home")); } - const handleDiscard = () => { - dispatch({ type: InternshipProposalActions.Decline, comment }); - history.push(route("home")); - } - - 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 = () => { - dispatch({ type: InternshipProposalActions.Approve, comment: null }); + const handleDiscard = (comment: string) => { + dispatch({ type: InternshipProposalActions.Decline, comment: comment }); history.push(route("home")); } @@ -115,59 +80,13 @@ export const InternshipProposalPreviewPage = () => { { proposal && } - - - - - - - { t("accept-without-comments") } - { t("accept-with-comments") } - - - + - - { t("internship.discard.title") } - - { t("internship.discard.info") } - setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> - - - - - - - - - { t("internship.accept.title") } - - { t("internship.accept.info") } - setComment(ev.target.value) } fullWidth label={ t("comments") }/> - - - - - - - } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 6141ecd..761a47c 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -13,7 +13,7 @@ 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 { useCurrentEdition, useDeadlines } from "@/hooks"; import api from "@/api"; import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions"; import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration"; @@ -36,6 +36,10 @@ export const updateInternshipInfo = async (dispatch: AppDispatch) => { document: plan, state: plan.state, }) + } else { + dispatch({ + type: InternshipPlanActions.Reset, + }) } } @@ -47,6 +51,7 @@ export const MainPage = () => { const deadlines = useDeadlines(); const insurance = useSelector(root => root.insurance); const dispatch = useDispatch(); + const edition = useCurrentEdition(); useEffect(() => { dispatch(updateInternshipInfo); @@ -64,13 +69,15 @@ export const MainPage = () => { if (insurance.required) yield ; - yield + yield yield } - return + return + + { t("pages.my-internship.header") } + - { t("pages.my-internship.header") } { Array.from(getSteps()) } diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index e6f6c4c..2e15ec3 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -5,24 +5,29 @@ import { useTranslation } from "react-i18next"; import { Box, Button, ButtonProps, StepProps } from "@material-ui/core"; import { FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index"; import { route } from "@/routing"; -import { Link as RouterLink } from "react-router-dom"; +import { Link as RouterLink, useHistory } from "react-router-dom"; import { Actions, Step } from "@/components"; import React, { HTMLProps } from "react"; import { Alert, AlertTitle } from "@material-ui/lab"; import { ContactAction, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; import { useDeadlines } from "@/hooks"; +import { InternshipDocument } from "@/api/dto/internship-registration"; +import { FileInfo } from "@/components/fileinfo"; +import { useSpacing } from "@/styles"; +import { AcceptanceActions } from "@/components/acceptance-action"; +import { InternshipPlanActions, useDispatch } from "@/state/actions"; const PlanActions = () => { const status = useSelector(state => getSubmissionStatus(state.plan)); + const { t } = useTranslation(); + const dispatch = useDispatch(); + const history = useHistory(); - const ReviewAction = (props: ButtonProps) => - - - const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) => - const TemplateAction = (props: ButtonProps) => @@ -30,20 +35,28 @@ const PlanActions = () => { { t('steps.plan.template') } + const handleAccept = (comment?: string) => { + dispatch({ type: InternshipPlanActions.Approve, comment: comment || null }); + history.push(route("home")); + } + + const handleDiscard = (comment: string) => { + dispatch({ type: InternshipPlanActions.Decline, comment: comment }); + history.push(route("home")); + } + switch (status) { case "awaiting": return - + case "accepted": return - { t('send-again') } case "declined": return { t('fix-errors') } - @@ -72,6 +85,8 @@ export const PlanStep = (props: StepProps) => { const { t } = useTranslation(); const submission = useSelector(state => state.plan); + const document = useSelector(state => state.plan.document as InternshipDocument); + const spacing = useSpacing(2); const status = getSubmissionStatus(submission); const deadlines = useDeadlines(); @@ -83,10 +98,13 @@ export const PlanStep = (props: StepProps) => { active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" } until={ deadlines.proposal } state={ }> -

{ t(`steps.plan.info.${ status }`) }

+
+

{ t(`steps.plan.info.${ status }`) }

- { comment && } + { comment && } + { document && } - + +
; } diff --git a/src/pages/steps/student.tsx b/src/pages/steps/student.tsx index 24eab0b..1e31f73 100644 --- a/src/pages/steps/student.tsx +++ b/src/pages/steps/student.tsx @@ -30,7 +30,7 @@ export const StudentStep = (props: StepProps) => { : <>

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

- diff --git a/src/pages/user/login.tsx b/src/pages/user/login.tsx index c7dd466..ffdd9c9 100644 --- a/src/pages/user/login.tsx +++ b/src/pages/user/login.tsx @@ -1,6 +1,6 @@ import React, { Dispatch, useEffect } from "react"; import { Page } from "@/pages/base"; -import { Button, Container } from "@material-ui/core"; +import { Button, CircularProgress, Container, Typography } from "@material-ui/core"; import { Action, StudentActions, useDispatch } from "@/state/actions"; import { Route, Switch, useHistory, useLocation, useRouteMatch } from "react-router-dom"; import { route } from "@/routing"; @@ -10,6 +10,8 @@ import { AppState } from "@/state/reducer"; import api from "@/api"; import { UserActions } from "@/state/actions/user"; import { getAuthorizeUrl } from "@/api/user"; +import { useTranslation } from "react-i18next"; +import { Loading } from "@/components/loading"; const authorizeUser = (code?: string) => async (dispatch: Dispatch, getState: () => AppState): Promise => { const token = await api.user.login(code); @@ -32,6 +34,7 @@ export const UserLoginPage = () => { const match = useRouteMatch(); const location = useLocation(); const query = new URLSearchParams(useLocation().search); + const { t } = useTranslation(); const handleSampleLogin = async () => { await dispatch(authorizeUser()); @@ -54,6 +57,8 @@ export const UserLoginPage = () => { })(); }, [ match.path ]); + const inProgress = + return Zaloguj się @@ -66,11 +71,13 @@ export const UserLoginPage = () => { - (window.location.href = getAuthorizeUrl()) - } /> + { + window.location.href = getAuthorizeUrl() + + return inProgress + } } /> - Kod: { query.get("code") } + { inProgress } 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 }} 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"