Merge branch 'feature/student_data_form'
This commit is contained in:
commit
5ccf8094c8
@ -31,6 +31,7 @@
|
|||||||
"css-loader": "3.4.2",
|
"css-loader": "3.4.2",
|
||||||
"date-holidays": "^1.5.3",
|
"date-holidays": "^1.5.3",
|
||||||
"file-loader": "4.3.0",
|
"file-loader": "4.3.0",
|
||||||
|
"filesize": "^6.1.0",
|
||||||
"formik": "^2.1.5",
|
"formik": "^2.1.5",
|
||||||
"formik-material-ui": "^3.0.0-alpha.0",
|
"formik-material-ui": "^3.0.0-alpha.0",
|
||||||
"html-webpack-plugin": "4.0.0-beta.11",
|
"html-webpack-plugin": "4.0.0-beta.11",
|
||||||
@ -39,7 +40,8 @@
|
|||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"material-ui-dropzone": "^3.3.0",
|
"material-ui-dropzone": "^3.3.0",
|
||||||
"mdi-material-ui": "^6.17.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",
|
"node-sass": "^4.14.1",
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||||
"postcss-flexbugs-fixes": "4.1.0",
|
"postcss-flexbugs-fixes": "4.1.0",
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000"/>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap&subset=latin,latin-ext" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap&subset=latin,latin-ext"/>
|
||||||
<title>Zgłoszenie praktyki studenckiej</title>
|
<title>Zgłoszenie praktyki studenckiej</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
<div id="modals"></div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { Identifiable } from "@/data";
|
import { Identifiable, InternshipProgramEntry } from "@/data";
|
||||||
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
|
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
|
||||||
import { OneWayTransformer, Transformer } from "@/serialization";
|
import { OneWayTransformer, Transformer } from "@/serialization";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import moment from "moment";
|
import moment from "moment-timezone";
|
||||||
import { Subset } from "@/helpers";
|
import { Subset } from "@/helpers";
|
||||||
|
|
||||||
|
export interface ProgramEntryDTO extends Identifiable {
|
||||||
|
description: string;
|
||||||
|
descriptionEng: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditionDTO extends Identifiable {
|
export interface EditionDTO extends Identifiable {
|
||||||
editionStart: string,
|
editionStart: string,
|
||||||
editionFinish: string,
|
editionFinish: string,
|
||||||
reportingStart: string,
|
reportingStart: string,
|
||||||
course: CourseDTO,
|
course: CourseDTO,
|
||||||
|
availableSubjects: ProgramEntryDTO[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditionTeaserDTO extends Identifiable {
|
export interface EditionTeaserDTO extends Identifiable {
|
||||||
@ -39,6 +45,7 @@ export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
|
|||||||
editionStart: subject.startDate.toISOString(),
|
editionStart: subject.startDate.toISOString(),
|
||||||
course: courseDtoTransformer.reverseTransform(subject.course),
|
course: courseDtoTransformer.reverseTransform(subject.course),
|
||||||
reportingStart: subject.reportingStart.toISOString(),
|
reportingStart: subject.reportingStart.toISOString(),
|
||||||
|
availableSubjects: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
transform(subject: EditionDTO, context: undefined): Edition {
|
transform(subject: EditionDTO, context: undefined): Edition {
|
||||||
@ -55,3 +62,19 @@ export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const programEntryDtoTransformer: Transformer<ProgramEntryDTO, InternshipProgramEntry> = {
|
||||||
|
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: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -3,9 +3,10 @@ import { momentSerializationTransformer, OneWayTransformer } from "@/serializati
|
|||||||
import { Nullable } from "@/helpers";
|
import { Nullable } from "@/helpers";
|
||||||
import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor";
|
import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor";
|
||||||
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
import { sampleStudent } from "@/provider/dummy";
|
import { sampleStudent } from "@/provider/dummy";
|
||||||
import { UploadType } from "@/api/upload";
|
import { UploadType } from "@/api/upload";
|
||||||
|
import { ProgramEntryDTO, programEntryDtoTransformer } from "@/api/dto/edition";
|
||||||
|
|
||||||
export enum SubmissionState {
|
export enum SubmissionState {
|
||||||
Draft = "Draft",
|
Draft = "Draft",
|
||||||
@ -36,6 +37,7 @@ export interface InternshipRegistrationUpdate {
|
|||||||
type: number,
|
type: number,
|
||||||
mentor: MentorDTO,
|
mentor: MentorDTO,
|
||||||
hours: number,
|
hours: number,
|
||||||
|
subjects: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternshipRegistrationDTO extends Identifiable {
|
export interface InternshipRegistrationDTO extends Identifiable {
|
||||||
@ -47,6 +49,7 @@ export interface InternshipRegistrationDTO extends Identifiable {
|
|||||||
company: Company,
|
company: Company,
|
||||||
branchAddress: Office,
|
branchAddress: Office,
|
||||||
declaredHours: number,
|
declaredHours: number,
|
||||||
|
subjects: { subject: ProgramEntryDTO }[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternshipDocument extends Identifiable {
|
export interface InternshipDocument extends Identifiable {
|
||||||
@ -65,8 +68,8 @@ export interface InternshipInfoDTO {
|
|||||||
export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable<Internship>, Nullable<InternshipRegistrationUpdate>> = {
|
export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable<Internship>, Nullable<InternshipRegistrationUpdate>> = {
|
||||||
transform(subject: Nullable<Internship>, context?: unknown): Nullable<InternshipRegistrationUpdate> {
|
transform(subject: Nullable<Internship>, context?: unknown): Nullable<InternshipRegistrationUpdate> {
|
||||||
return {
|
return {
|
||||||
start: subject?.startDate?.toISOString() || null,
|
start: momentSerializationTransformer.transform(subject?.startDate) || null,
|
||||||
end: subject?.endDate?.toISOString() || null,
|
end: momentSerializationTransformer.transform(subject?.endDate) || null,
|
||||||
type: parseInt(subject?.type?.id || "0"),
|
type: parseInt(subject?.type?.id || "0"),
|
||||||
mentor: mentorDtoTransformer.reverseTransform(subject.mentor as Mentor),
|
mentor: mentorDtoTransformer.reverseTransform(subject.mentor as Mentor),
|
||||||
company: subject?.company?.id ? {
|
company: subject?.company?.id ? {
|
||||||
@ -80,6 +83,7 @@ export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable
|
|||||||
branchOffice: subject?.office?.address as NewBranchOffice
|
branchOffice: subject?.office?.address as NewBranchOffice
|
||||||
},
|
},
|
||||||
hours: subject?.hours,
|
hours: subject?.hours,
|
||||||
|
subjects: subject?.program?.map(program => program.id as string) || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +101,7 @@ export const internshipRegistrationDtoTransformer: OneWayTransformer<InternshipR
|
|||||||
hours: dto.declaredHours,
|
hours: dto.declaredHours,
|
||||||
isAccepted: dto.state === SubmissionState.Accepted,
|
isAccepted: dto.state === SubmissionState.Accepted,
|
||||||
lengthInWeeks: 0,
|
lengthInWeeks: 0,
|
||||||
program: [],
|
program: dto.subjects.map(subject => programEntryDtoTransformer.transform(subject.subject)),
|
||||||
intern: sampleStudent, // fixme
|
intern: sampleStudent, // fixme
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { axios } from "@/api/index";
|
import { axios } from "@/api/index";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import { prepare } from "@/routing";
|
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 { Subset } from "@/helpers";
|
||||||
|
import { InternshipProgramEntry } from "@/data";
|
||||||
|
|
||||||
const EDITIONS_ENDPOINT = "/editions";
|
const EDITIONS_ENDPOINT = "/editions";
|
||||||
const EDITION_INFO_ENDPOINT = "/editions/:key";
|
const EDITION_INFO_ENDPOINT = "/editions/:key";
|
||||||
@ -41,11 +42,17 @@ export async function get(key: string): Promise<Subset<Edition> | null> {
|
|||||||
return editionTeaserDtoTransformer.transform(dto);
|
return editionTeaserDtoTransformer.transform(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function current(): Promise<Edition> {
|
export async function current(): Promise<{
|
||||||
|
edition: Edition,
|
||||||
|
program: InternshipProgramEntry[],
|
||||||
|
}> {
|
||||||
const response = await axios.get<EditionDTO>(EDITION_CURRENT_ENDPOINT);
|
const response = await axios.get<EditionDTO>(EDITION_CURRENT_ENDPOINT);
|
||||||
const dto = response.data;
|
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<string> {
|
export async function login(key: string): Promise<string> {
|
||||||
|
@ -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 { axios } from "@/api/index";
|
||||||
import { Nullable } from "@/helpers";
|
import { Nullable } from "@/helpers";
|
||||||
|
|
||||||
const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration';
|
const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration';
|
||||||
const INTERNSHIP_ENDPOINT = '/internship';
|
const INTERNSHIP_ENDPOINT = '/internship';
|
||||||
|
|
||||||
export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<boolean> {
|
export type ValidationMessage = {
|
||||||
await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship);
|
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<InternshipRegistrationUpdate>): Promise<SubmissionState> {
|
||||||
|
const response = (await axios.put<UpdateResponse>(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<InternshipInfoDTO> {
|
export async function get(): Promise<InternshipInfoDTO> {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { axios } from "@/api/index";
|
import { axios } from "@/api/index";
|
||||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||||
import { prepare } from "@/routing";
|
import { prepare } from "@/routing";
|
||||||
|
import { Identifiable } from "@/data";
|
||||||
|
|
||||||
export enum UploadType {
|
export enum UploadType {
|
||||||
Ipp = "IppScan",
|
Ipp = "IppScan",
|
||||||
@ -8,6 +9,12 @@ export enum UploadType {
|
|||||||
Insurance = "NnwInsurance",
|
Insurance = "NnwInsurance",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DocumentFileInfo extends Identifiable {
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
mime: string;
|
||||||
|
}
|
||||||
|
|
||||||
const CREATE_DOCUMENT_ENDPOINT = '/document';
|
const CREATE_DOCUMENT_ENDPOINT = '/document';
|
||||||
const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan';
|
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);
|
const response = await axios.put(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }), data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fileinfo(document: InternshipDocument): Promise<DocumentFileInfo> {
|
||||||
|
const response = await axios.get<DocumentFileInfo>(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }));
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
49
src/app.tsx
49
src/app.tsx
@ -9,12 +9,11 @@ import '@/styles/overrides.scss'
|
|||||||
import '@/styles/header.scss'
|
import '@/styles/header.scss'
|
||||||
import '@/styles/footer.scss'
|
import '@/styles/footer.scss'
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Edition } from "@/data/edition";
|
|
||||||
import { SettingActions } from "@/state/actions/settings";
|
import { SettingActions } from "@/state/actions/settings";
|
||||||
import { useDispatch, UserActions } from "@/state/actions";
|
import { useDispatch, UserActions } from "@/state/actions";
|
||||||
import { getLocale, Locale } from "@/state/reducer/settings";
|
import { getLocale, Locale } from "@/state/reducer/settings";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import moment from "moment";
|
import moment from "moment-timezone";
|
||||||
import { Container } from "@material-ui/core";
|
import { Container } from "@material-ui/core";
|
||||||
|
|
||||||
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
||||||
@ -61,8 +60,6 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const edition = useSelector<AppState, Edition | null>(state => state.edition);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
|
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
|
||||||
|
|
||||||
@ -73,27 +70,29 @@ function App() {
|
|||||||
}, [ locale ])
|
}, [ locale ])
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<header className="header">
|
<header>
|
||||||
<div id="logo" className="header__logo">
|
<Container className="header">
|
||||||
<Link to={ route('home') }>
|
<div id="logo" className="header__logo">
|
||||||
<img src="img/pg-logotyp.svg"/>
|
<Link to={ route('home') }>
|
||||||
</Link>
|
<img src="img/pg-logotyp.svg"/>
|
||||||
</div>
|
</Link>
|
||||||
<div className="header__nav">
|
</div>
|
||||||
<nav className="header__top">
|
<div className="header__nav">
|
||||||
<ul className="header__menu">
|
<nav className="header__top">
|
||||||
</ul>
|
<ul className="header__menu">
|
||||||
<UserMenu className="header__user"/>
|
</ul>
|
||||||
<div className="header__divider"/>
|
<UserMenu className="header__user"/>
|
||||||
<LanguageSwitcher className="header__language-switcher"/>
|
<div className="header__divider"/>
|
||||||
</nav>
|
<LanguageSwitcher className="header__language-switcher"/>
|
||||||
<nav className="header__bottom">
|
</nav>
|
||||||
<ul className="header__menu header__menu--main">
|
<nav className="header__bottom">
|
||||||
<li><Link to={ route("home") }>{ t("pages.my-internship.header") }</Link></li>
|
<ul className="header__menu header__menu--main">
|
||||||
<li><Link to="/regulamin">Regulamin</Link></li>
|
<li><Link to={ route("home") }>{ t("pages.my-internship.header") }</Link></li>
|
||||||
</ul>
|
<li><Link to="/regulations">Regulamin</Link></li>
|
||||||
</nav>
|
</ul>
|
||||||
</div>
|
</nav>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
</header>
|
</header>
|
||||||
<main id="content">
|
<main id="content">
|
||||||
{ <Switch>
|
{ <Switch>
|
||||||
|
115
src/components/acceptance-action.tsx
Normal file
115
src/components/acceptance-action.tsx
Normal file
@ -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<boolean>(false);
|
||||||
|
const [isAcceptModalOpen, setAcceptModelOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [comment, setComment] = useState<string>("");
|
||||||
|
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(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<HTMLElement>) => {
|
||||||
|
setMenuAnchor(ev.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAcceptMenuClose = () => {
|
||||||
|
setMenuAnchor(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAcceptWithComment = () => {
|
||||||
|
setAcceptModelOpen(true);
|
||||||
|
setMenuAnchor(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAcceptWithoutComment = () => {
|
||||||
|
onAccept();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<ButtonGroup color="primary" variant="contained">
|
||||||
|
<Button onClick={ handleAcceptWithoutComment } startIcon={ <StickerCheckOutline /> }>
|
||||||
|
{ t('accept-without-comments') }
|
||||||
|
</Button>
|
||||||
|
<Button size="small" onClick={ handleAcceptMenuOpen }><MenuDown /></Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<Menu open={ !!menuAnchor } anchorEl={ menuAnchor } onClose={ handleAcceptMenuClose }>
|
||||||
|
<MenuItem onClick={ handleAcceptWithoutComment }>{ t("accept-without-comments") }</MenuItem>
|
||||||
|
<MenuItem onClick={ handleAcceptWithComment }>{ t("accept-with-comments") }</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Button onClick={ handleDiscardAction } color="secondary" startIcon={ <StickerRemoveOutline /> }>
|
||||||
|
{ t('discard') }
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{ createPortal(<>
|
||||||
|
<Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md">
|
||||||
|
<DialogTitle>{ t(label + ".discard.title") }</DialogTitle>
|
||||||
|
<DialogContent className={ classes.root }>
|
||||||
|
<Typography variant="body1">{ t(label + ".discard.info") }</Typography>
|
||||||
|
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={ handleDiscardModalClose }>
|
||||||
|
{ t('cancel') }
|
||||||
|
</Button>
|
||||||
|
<Button onClick={ handleDiscard } color="primary" variant="contained">
|
||||||
|
{ t('confirm') }
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md">
|
||||||
|
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
|
||||||
|
<DialogContent className={ classes.root }>
|
||||||
|
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
|
||||||
|
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={ handleAcceptModalClose }>
|
||||||
|
{ t('cancel') }
|
||||||
|
</Button>
|
||||||
|
<Button onClick={ handleAccept } color="primary" variant="contained">
|
||||||
|
{ t('confirm') }
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>, document.getElementById("modals") as Element) }
|
||||||
|
</>
|
||||||
|
}
|
29
src/components/async.tsx
Normal file
29
src/components/async.tsx
Normal file
@ -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<TValue, TError = any> = {
|
||||||
|
async: AsyncResult<TValue>,
|
||||||
|
children: (value: TValue) => JSX.Element,
|
||||||
|
loading?: () => JSX.Element,
|
||||||
|
error?: (error: TError) => JSX.Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLoading = () => <Loading />;
|
||||||
|
const defaultError = (error: any) => <Alert severity="error">{ error.message }</Alert>;
|
||||||
|
|
||||||
|
export function Async<TValue, TError = any>(
|
||||||
|
{ async, children: render, loading = defaultLoading, error = defaultError }: AsyncProps<TValue, TError>
|
||||||
|
) {
|
||||||
|
if (async.isLoading || (!async.error && !async.value)) {
|
||||||
|
return loading();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof async.error !== "undefined") {
|
||||||
|
return error(async.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(async.value as TValue);
|
||||||
|
}
|
93
src/components/fileinfo.tsx
Normal file
93
src/components/fileinfo.tsx
Normal file
@ -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 <FilePdfOutline {...props} />
|
||||||
|
case mime === "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||||
|
return <FileWordOutline {...props} />
|
||||||
|
case mime.startsWith("image/"):
|
||||||
|
return <FileImageOutline {...props} />
|
||||||
|
default:
|
||||||
|
return <FileOutline {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileInfo = ({ document, ...props }: FileInfoProps) => {
|
||||||
|
const fileinfo = useAsync<DocumentFileInfo>(useCallback(() => api.upload.fileinfo(document), [document.id]));
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <Paper variant="outlined" { ...props } className={ classNames(classes.root, props.className) }>
|
||||||
|
<Async async={ fileinfo }>
|
||||||
|
{ fileinfo => <div className={ classes.grid }>
|
||||||
|
<div className={ classes.iconColumn }>
|
||||||
|
<FileIcon mime={ fileinfo.mime } className={ classes.icon } />
|
||||||
|
</div>
|
||||||
|
<aside className={ classes.asideColumn }>
|
||||||
|
<Typography variant="h5" className={ classes.header }>{ fileinfo.filename }</Typography>
|
||||||
|
<Typography variant="subtitle2">
|
||||||
|
{ filesize(fileinfo.size) } • { fileinfo.mime }
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Actions className={ classes.actions }>
|
||||||
|
<Button className={ classes.download } startIcon={ <FileDownloadOutline /> }>{ t("download") }</Button>
|
||||||
|
</Actions>
|
||||||
|
</aside>
|
||||||
|
</div> }
|
||||||
|
</Async>
|
||||||
|
</Paper>
|
||||||
|
}
|
28
src/components/loading.tsx
Normal file
28
src/components/loading.tsx
Normal file
@ -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 <div className={ classes.root } { ...props }>
|
||||||
|
<CircularProgress size={ size }/>
|
||||||
|
{ label && <Typography variant="subtitle1" color="primary">{ label }</Typography> }
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import { Internship } from "@/data";
|
import { Internship } from "@/data";
|
||||||
import React from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useVerticalSpacing } from "@/styles";
|
import { useVerticalSpacing } from "@/styles";
|
||||||
import moment from "moment";
|
import moment from "moment-timezone";
|
||||||
import { Label, Section } from "@/components/section";
|
import { Label, Section } from "@/components/section";
|
||||||
import { StudentPreview } from "@/pages/user/profile";
|
import { StudentPreview } from "@/pages/user/profile";
|
||||||
|
import { Check, StickerCheck } from "mdi-material-ui";
|
||||||
|
|
||||||
export type ProposalPreviewProps = {
|
export type ProposalPreviewProps = {
|
||||||
proposal: Internship;
|
proposal: Internship;
|
||||||
@ -42,6 +43,16 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
|
|||||||
<Typography className="proposal__primary">{ proposal.type.label.pl }</Typography>
|
<Typography className="proposal__primary">{ proposal.type.label.pl }</Typography>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Label>{ t('internship.sections.program') }</Label>
|
||||||
|
<List>
|
||||||
|
{ proposal.program.map(subject => <ListItem key={ subject.id }>
|
||||||
|
<ListItemIcon><StickerCheck /></ListItemIcon>
|
||||||
|
<ListItemText>{ subject.description }</ListItemText>
|
||||||
|
</ListItem>) }
|
||||||
|
</List>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<Label>{ t('internship.sections.duration') }</Label>
|
<Label>{ t('internship.sections.duration') }</Label>
|
||||||
<Typography className="proposal__primary">
|
<Typography className="proposal__primary">
|
||||||
|
@ -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 { Box, Step as StepperStep, StepContent, StepLabel, StepProps as StepperStepProps, Typography } from "@material-ui/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import React, { ReactChild, useMemo } from "react";
|
import React, { ReactChild, useMemo } from "react";
|
||||||
import { StepIcon } from "@/components/stepIcon";
|
import { StepIcon } from "@/components/stepIcon";
|
||||||
|
|
||||||
type StepProps = StepperStepProps & {
|
type StepProps = StepperStepProps & {
|
||||||
|
notBefore?: Moment;
|
||||||
until?: Moment;
|
until?: Moment;
|
||||||
completedOn?: Moment;
|
completedOn?: Moment;
|
||||||
label: string;
|
label: string;
|
||||||
@ -17,7 +18,7 @@ type StepProps = StepperStepProps & {
|
|||||||
const now = moment();
|
const now = moment();
|
||||||
|
|
||||||
export const Step = (props: StepProps) => {
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
|
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
|
||||||
@ -26,18 +27,22 @@ export const Step = (props: StepProps) => {
|
|||||||
return <StepperStep { ...rest } completed={ completed }>
|
return <StepperStep { ...rest } completed={ completed }>
|
||||||
<StepLabel error={ declined } StepIconComponent={ StepIcon } StepIconProps={{ ...props, waiting } as any}>
|
<StepLabel error={ declined } StepIconComponent={ StepIcon } StepIconProps={{ ...props, waiting } as any}>
|
||||||
{ label }
|
{ label }
|
||||||
{ until && <Box>
|
<Box>
|
||||||
{ state && <>
|
{ state && <Typography variant="subtitle2" display="inline">{ state }</Typography> }
|
||||||
<Typography variant="subtitle2" display="inline">{ state }</Typography>
|
{ notBefore &&
|
||||||
|
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
||||||
|
{ t('not-before', { date: notBefore }) }
|
||||||
|
</Typography> }
|
||||||
|
{ until && <>
|
||||||
<Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography>
|
<Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography>
|
||||||
|
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
||||||
|
{ t('until', { date: until }) }
|
||||||
|
{ isLate && <Typography color="error" display="inline"
|
||||||
|
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
|
||||||
|
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
|
||||||
|
</Typography>
|
||||||
</> }
|
</> }
|
||||||
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
</Box>
|
||||||
{ t('until', { date: until }) }
|
|
||||||
{ isLate && <Typography color="error" display="inline"
|
|
||||||
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
|
|
||||||
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
|
|
||||||
</Typography>
|
|
||||||
</Box> }
|
|
||||||
</StepLabel>
|
</StepLabel>
|
||||||
{ children && <StepContent>{ children }</StepContent> }
|
{ children && <StepContent>{ children }</StepContent> }
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
|
@ -5,5 +5,4 @@ import { Identifiable } from "./common";
|
|||||||
export interface Course extends Identifiable {
|
export interface Course extends Identifiable {
|
||||||
name: string,
|
name: string,
|
||||||
desiredSemesters: Semester[],
|
desiredSemesters: Semester[],
|
||||||
possibleProgramEntries: InternshipProgramEntry[];
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
import { Course } from "@/data/course";
|
import { Course } from "@/data/course";
|
||||||
import { Identifiable } from "@/data/common";
|
import { Identifiable } from "@/data/common";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
import { Identifiable, Multilingual } from "./common";
|
import { Identifiable, Multilingual } from "./common";
|
||||||
import { Student } from "@/data/student";
|
import { Student } from "@/data/student";
|
||||||
import { Company, Office } from "@/data/company";
|
import { Company, Office } from "@/data/company";
|
||||||
|
@ -194,7 +194,7 @@ export const CompanyForm: React.FunctionComponent = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<Autocomplete options={ companies }
|
<Autocomplete options={ companies }
|
||||||
getOptionLabel={ option => typeof option === "string" ? option : option.name }
|
getOptionLabel={ option => typeof option === "string" ? option : option.name }
|
||||||
renderOption={ company => <CompanyItem company={ company }/> }
|
renderOption={ company => <CompanyItem company={ company }/> }
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
import React, { HTMLProps, useMemo, useState } from "react";
|
import React, { HTMLProps, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Grid, TextField, Typography } from "@material-ui/core";
|
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 { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
||||||
import { CompanyForm } from "@/forms/company";
|
import { CompanyForm } from "@/forms/company";
|
||||||
import { StudentForm } from "@/forms/student";
|
import { StudentForm } from "@/forms/student";
|
||||||
import { sampleStudent } from "@/provider/dummy/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 { Nullable } from "@/helpers";
|
||||||
import moment, { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
import { computeWorkingHours } from "@/utils/date";
|
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 { emptyInternship } from "@/provider/dummy/internship";
|
||||||
import { useDispatch } from "@/state/actions";
|
import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { AppState } from "@/state/reducer";
|
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 { useCurrentEdition, useCurrentStudent, useInternshipTypes, useUpdateEffect } from "@/hooks";
|
||||||
import { internshipRegistrationUpdateTransformer } from "@/api/dto/internship-registration";
|
import { internshipRegistrationUpdateTransformer } from "@/api/dto/internship-registration";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
|
import FormLabel from "@material-ui/core/FormLabel";
|
||||||
|
import { ValidationError, ValidationMessage } from "@/api/internship";
|
||||||
|
|
||||||
export type InternshipFormValues = {
|
export type InternshipFormValues = {
|
||||||
startDate: Moment | null;
|
startDate: Moment | null;
|
||||||
@ -43,6 +57,7 @@ export type InternshipFormValues = {
|
|||||||
mentorEmail: string;
|
mentorEmail: string;
|
||||||
mentorPhone: string;
|
mentorPhone: string;
|
||||||
kindOther: string | null;
|
kindOther: string | null;
|
||||||
|
program: InternshipProgramEntry[];
|
||||||
|
|
||||||
// relations
|
// relations
|
||||||
kind: InternshipType | null;
|
kind: InternshipType | null;
|
||||||
@ -72,6 +87,7 @@ const emptyInternshipValues: InternshipFormValues = {
|
|||||||
startDate: null,
|
startDate: null,
|
||||||
student: sampleStudent,
|
student: sampleStudent,
|
||||||
workingHours: 40,
|
workingHours: 40,
|
||||||
|
program: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InternshipTypeItem = ({ internshipType: type, ...props }: { internshipType: InternshipType } & HTMLProps<any>) => {
|
export const InternshipTypeItem = ({ internshipType: type, ...props }: { internshipType: InternshipType } & HTMLProps<any>) => {
|
||||||
@ -86,9 +102,24 @@ export const InternshipTypeItem = ({ internshipType: type, ...props }: { interns
|
|||||||
const InternshipProgramForm = () => {
|
const InternshipProgramForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { values, handleBlur, setFieldValue, errors } = useFormikContext<InternshipFormValues>();
|
const { values, handleBlur, setFieldValue, errors } = useFormikContext<InternshipFormValues>();
|
||||||
|
const [ selectedProgramEntries, setSelectedProgramEntries ] = useState<InternshipProgramEntry[]>(values.program);
|
||||||
|
|
||||||
|
const possibleProgramEntries = useSelector<AppState, InternshipProgramEntry[]>(state => state.edition.program);
|
||||||
|
|
||||||
const types = useInternshipTypes();
|
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 (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
@ -108,6 +139,20 @@ const InternshipProgramForm = () => {
|
|||||||
{/* <Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } />*/}
|
{/* <Field label={ t("forms.internship.fields.kind-other") } name="kindOther" fullWidth component={ TextFieldFormik } />*/}
|
||||||
{/* }*/}
|
{/* }*/}
|
||||||
{/*</Grid>*/}
|
{/*</Grid>*/}
|
||||||
|
<Grid item xs={ 12 }>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{ t('forms.internship.fields.program', { count: 3 }) }</FormLabel>
|
||||||
|
{ possibleProgramEntries.map(
|
||||||
|
entry => <FormControlLabel
|
||||||
|
control={ <Checkbox /> }
|
||||||
|
checked={ selectedProgramEntries.find(cur => entry.id == cur.id) !== undefined }
|
||||||
|
onChange={ handleProgramEntryChange(entry) }
|
||||||
|
label={ entry.description }
|
||||||
|
key={ entry.id }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</FormGroup>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -137,19 +182,22 @@ const InternshipDurationForm = () => {
|
|||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item md={ 6 }>
|
<Grid item md={ 6 }>
|
||||||
<DatePicker value={ startDate } onChange={ value => setFieldValue("startDate", value) }
|
<DatePicker value={ startDate }
|
||||||
format="DD MMMM yyyy"
|
onChange={ value => setFieldValue("startDate", value) }
|
||||||
|
format="DD.MM.yyyy"
|
||||||
disableToolbar fullWidth
|
disableToolbar fullWidth
|
||||||
variant="inline" label={ t("forms.internship.fields.start-date") }
|
variant="inline"
|
||||||
minDate={ moment() }
|
label={ t("forms.internship.fields.start-date") }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 6 }>
|
<Grid item md={ 6 }>
|
||||||
<DatePicker value={ endDate } onChange={ value => setFieldValue("endDate", value) }
|
<DatePicker value={ endDate }
|
||||||
format="DD MMMM yyyy"
|
onChange={ value => setFieldValue("endDate", value) }
|
||||||
|
format="DD.MM.yyyy"
|
||||||
disableToolbar fullWidth
|
disableToolbar fullWidth
|
||||||
variant="inline" label={ t("forms.internship.fields.end-date") }
|
variant="inline"
|
||||||
minDate={ startDate || moment() }
|
label={ t("forms.internship.fields.end-date") }
|
||||||
|
minDate={ startDate }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
@ -207,6 +255,7 @@ const converter: Transformer<Nullable<Internship>, InternshipFormValues, Interns
|
|||||||
mentorLastName: internship.mentor?.surname || "",
|
mentorLastName: internship.mentor?.surname || "",
|
||||||
mentorPhone: internship.mentor?.phone || "",
|
mentorPhone: internship.mentor?.phone || "",
|
||||||
workingHours: 40,
|
workingHours: 40,
|
||||||
|
program: internship.program || [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reverseTransform(form: InternshipFormValues, context: InternshipConverterContext): Nullable<Internship> {
|
reverseTransform(form: InternshipFormValues, context: InternshipConverterContext): Nullable<Internship> {
|
||||||
@ -236,12 +285,18 @@ const converter: Transformer<Nullable<Internship>, InternshipFormValues, Interns
|
|||||||
},
|
},
|
||||||
hours: form.hours ? form.hours : 0,
|
hours: form.hours ? form.hours : 0,
|
||||||
type: form.kind as InternshipType,
|
type: form.kind as InternshipType,
|
||||||
|
program: form.program,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InternshipForm: React.FunctionComponent = () => {
|
export const InternshipForm: React.FunctionComponent = () => {
|
||||||
const student = useCurrentStudent();
|
const student = useCurrentStudent();
|
||||||
|
const history = useHistory();
|
||||||
|
const root = useRef<HTMLElement>(null);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<ValidationMessage[]>([]);
|
||||||
|
|
||||||
const initialInternship = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || {
|
const initialInternship = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || {
|
||||||
...emptyInternship,
|
...emptyInternship,
|
||||||
@ -252,12 +307,8 @@ export const InternshipForm: React.FunctionComponent = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const edition = useCurrentEdition();
|
const edition = useCurrentEdition();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const validationSchema = Yup.object<Partial<InternshipFormValues>>({
|
const validationSchema = Yup.object<Partial<InternshipFormValues>>({
|
||||||
@ -285,6 +336,7 @@ export const InternshipForm: React.FunctionComponent = () => {
|
|||||||
city: Yup.string().required(t("validation.required")),
|
city: Yup.string().required(t("validation.required")),
|
||||||
postalCode: Yup.string().required(t("validation.required")),
|
postalCode: Yup.string().required(t("validation.required")),
|
||||||
building: Yup.string().required(t("validation.required")),
|
building: Yup.string().required(t("validation.required")),
|
||||||
|
program: Yup.array() as any,
|
||||||
// kindOther: Yup.string().when("kind", {
|
// kindOther: Yup.string().when("kind", {
|
||||||
// is: (values: InternshipFormValues) => values?.kind === InternshipType.Other,
|
// is: (values: InternshipFormValues) => values?.kind === InternshipType.Other,
|
||||||
// then: Yup.string().required(t("validation.required"))
|
// then: Yup.string().required(t("validation.required"))
|
||||||
@ -293,17 +345,25 @@ export const InternshipForm: React.FunctionComponent = () => {
|
|||||||
|
|
||||||
const values = converter.transform(initialInternship);
|
const values = converter.transform(initialInternship);
|
||||||
|
|
||||||
const handleSubmit = (values: InternshipFormValues) => {
|
const handleSubmit = async (values: InternshipFormValues) => {
|
||||||
setConfirmDialogOpen(false);
|
setConfirmDialogOpen(false);
|
||||||
|
|
||||||
const internship = converter.reverseTransform(values, { internship: initialInternship as Internship });
|
const internship = converter.reverseTransform(values, { internship: initialInternship as Internship });
|
||||||
const update = internshipRegistrationUpdateTransformer.transform(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"))
|
||||||
|
} catch (error) {
|
||||||
// history.push(route("home"))
|
if (error instanceof ValidationError) {
|
||||||
|
setErrors(error.messages);
|
||||||
|
root.current?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerForm = () => {
|
const InnerForm = () => {
|
||||||
@ -323,10 +383,16 @@ export const InternshipForm: React.FunctionComponent = () => {
|
|||||||
setConfirmDialogOpen(false);
|
setConfirmDialogOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Form>
|
return <Form ref={ root as any }>
|
||||||
|
{ errors.length > 0 && <Alert severity="warning">
|
||||||
|
<AlertTitle>{ t('internship.validation.has-errors') }</AlertTitle>
|
||||||
|
<ul style={{ paddingLeft: 0 }}>
|
||||||
|
{ errors.map(message => <li key={ message.key }>{ t(`internship.validation.${message.key}`, message.parameters) }</li>) }
|
||||||
|
</ul>
|
||||||
|
</Alert> }
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
|
||||||
<StudentForm />
|
<StudentForm />
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.kind' )}</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.kind') }</Typography>
|
||||||
<InternshipProgramForm />
|
<InternshipProgramForm />
|
||||||
<Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography>
|
<Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography>
|
||||||
<InternshipDurationForm />
|
<InternshipDurationForm />
|
||||||
|
@ -41,19 +41,19 @@ export const PlanForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <Grid container>
|
return <Grid container>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<Typography variant="body1" component="p">{ t('forms.plan.instructions') }</Typography>
|
<Typography variant="body1" component="p">{ t('forms.plan.instructions') }</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }>
|
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }>
|
||||||
{ t('steps.plan.template') }
|
{ t('steps.plan.template') }
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") } onChange={ files => setFile(files[0]) }/>
|
<DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") } onChange={ files => setFile(files[0]) }/>
|
||||||
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
|
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<Actions>
|
<Actions>
|
||||||
<Button variant="contained" color="primary" onClick={ handleSubmit }>
|
<Button variant="contained" color="primary" onClick={ handleSubmit }>
|
||||||
{ t('confirm') }
|
{ t('confirm') }
|
||||||
|
@ -35,7 +35,7 @@ export const StudentForm = () => {
|
|||||||
<Grid item md={3}>
|
<Grid item md={3}>
|
||||||
<TextField label={ t("forms.internship.fields.semester") } value={ student.semester || "" } disabled fullWidth/>
|
<TextField label={ t("forms.internship.fields.semester") } value={ student.semester || "" } disabled fullWidth/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item xs={12}>
|
||||||
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
|
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
|
||||||
Powyższe dane nie są poprawne?
|
Powyższe dane nie są poprawne?
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -10,6 +10,8 @@ import { Actions } from "@/components";
|
|||||||
import { Nullable } from "@/helpers";
|
import { Nullable } from "@/helpers";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { StudentActions, useDispatch } from "@/state/actions";
|
import { StudentActions, useDispatch } from "@/state/actions";
|
||||||
|
import { route } from "@/routing";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
interface StudentFormValues {
|
interface StudentFormValues {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@ -48,6 +50,7 @@ const studentToFormValuesTransformer: Transformer<Nullable<Student>, StudentForm
|
|||||||
export const StudentForm = ({ student }: StudentFormProps) => {
|
export const StudentForm = ({ student }: StudentFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const validationSchema = useMemo(() => Yup.object<StudentFormValues>({
|
const validationSchema = useMemo(() => Yup.object<StudentFormValues>({
|
||||||
semester: Yup.number().required().min(1).max(10),
|
semester: Yup.number().required().min(1).max(10),
|
||||||
@ -71,6 +74,8 @@ export const StudentForm = ({ student }: StudentFormProps) => {
|
|||||||
type: StudentActions.Set,
|
type: StudentActions.Set,
|
||||||
student: updated,
|
student: updated,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
history.push(route("home"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export const useCurrentStudent = () => useSelector<AppState, Student | null>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const useCurrentEdition = () => useSelector<AppState, Edition | null>(
|
export const useCurrentEdition = () => useSelector<AppState, Edition | null>(
|
||||||
state => state.edition && editionSerializationTransformer.reverseTransform(state.edition)
|
state => state.edition?.edition && editionSerializationTransformer.reverseTransform(state.edition.edition)
|
||||||
)
|
)
|
||||||
|
|
||||||
export const useDeadlines = () => {
|
export const useDeadlines = () => {
|
||||||
|
@ -4,7 +4,7 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
|
|||||||
|
|
||||||
import "moment/locale/pl"
|
import "moment/locale/pl"
|
||||||
import "moment/locale/en-gb"
|
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";
|
import { convertToRoman } from "@/utils/numbers";
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
|
@ -7,7 +7,7 @@ import store, { persistor } from "@/state/store";
|
|||||||
import { PersistGate } from "redux-persist/integration/react";
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles";
|
import { MuiThemeProvider as ThemeProvider, StylesProvider } from "@material-ui/core/styles";
|
||||||
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
|
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment-timezone";
|
||||||
import { studentTheme } from "@/ui/theme";
|
import { studentTheme } from "@/ui/theme";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import MomentUtils from "@date-io/moment";
|
import MomentUtils from "@date-io/moment";
|
||||||
|
@ -25,11 +25,12 @@ export const loginToEdition = (id: string) => async (dispatch: AppDispatch) => {
|
|||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
|
|
||||||
const edition = await api.edition.current();
|
const { edition, program } = await api.edition.current();
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: EditionActions.Set,
|
type: EditionActions.Set,
|
||||||
edition
|
edition,
|
||||||
|
program,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import React, { useMemo } from "react";
|
|||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { useAsync } from "@/hooks";
|
import { useAsync } from "@/hooks";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
|
import { Loading } from "@/components/loading";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
|
||||||
export const FallbackPage = () => {
|
export const FallbackPage = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -13,23 +15,23 @@ export const FallbackPage = () => {
|
|||||||
|
|
||||||
const { isLoading, value, error } = useAsync(promise);
|
const { isLoading, value, error } = useAsync(promise);
|
||||||
|
|
||||||
console.log({ isLoading, value, error, location });
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <CircularProgress />
|
return <div style={{ marginTop: "2rem" }}><Loading size="8rem"/></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Page title="Strona nie została znaleziona">
|
return <Page title="Strona nie została znaleziona">
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant="h1">404</Typography>
|
<Box my={4}>
|
||||||
<Typography variant="h2">Strona nie została znaleziona</Typography>
|
<Typography variant="h1">404</Typography>
|
||||||
|
<Typography variant="h2">Strona nie została znaleziona</Typography>
|
||||||
|
|
||||||
<Box my={ 4 }>
|
<Box my={ 4 }>
|
||||||
<Divider variant="fullWidth"/>
|
<Divider variant="fullWidth"/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button to={ route("home") } color="primary" component={ RouterLink }>strona główna</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button to={ route("home") } color="primary" component={ RouterLink }>strona główna</Button>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Page>
|
</Page>
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import { Actions } from "@/components";
|
|||||||
import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
||||||
import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui/index";
|
import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui/index";
|
||||||
import { useVerticalSpacing } from "@/styles";
|
import { useVerticalSpacing } from "@/styles";
|
||||||
|
import { AcceptanceActions } from "@/components/acceptance-action";
|
||||||
|
|
||||||
export const InternshipProposalFormPage = () => {
|
export const InternshipProposalFormPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -54,49 +55,13 @@ export const InternshipProposalPreviewPage = () => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const [isDiscardModalOpen, setDiscardModelOpen] = useState<boolean>(false);
|
const handleAccept = (comment?: string) => {
|
||||||
const [isAcceptModalOpen, setAcceptModelOpen] = useState<boolean>(false);
|
dispatch({ type: InternshipProposalActions.Approve, comment: comment || null });
|
||||||
|
|
||||||
const [comment, setComment] = useState<string>("");
|
|
||||||
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
|
|
||||||
|
|
||||||
const handleAccept = () => {
|
|
||||||
dispatch({ type: InternshipProposalActions.Approve, comment });
|
|
||||||
history.push(route("home"));
|
history.push(route("home"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDiscard = () => {
|
const handleDiscard = (comment: string) => {
|
||||||
dispatch({ type: InternshipProposalActions.Decline, comment });
|
dispatch({ type: InternshipProposalActions.Decline, comment: comment });
|
||||||
history.push(route("home"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAcceptModalClose = () => {
|
|
||||||
setAcceptModelOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiscardModalClose = () => {
|
|
||||||
setDiscardModelOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiscardAction = () => {
|
|
||||||
setDiscardModelOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAcceptMenuOpen = (ev: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setMenuAnchor(ev.currentTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAcceptMenuClose = () => {
|
|
||||||
setMenuAnchor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAcceptWithComment = () => {
|
|
||||||
setAcceptModelOpen(true);
|
|
||||||
setMenuAnchor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAcceptWithoutComment = () => {
|
|
||||||
dispatch({ type: InternshipProposalActions.Approve, comment: null });
|
|
||||||
history.push(route("home"));
|
history.push(route("home"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,59 +80,13 @@ export const InternshipProposalPreviewPage = () => {
|
|||||||
{ proposal && <ProposalPreview proposal={ proposal } /> }
|
{ proposal && <ProposalPreview proposal={ proposal } /> }
|
||||||
|
|
||||||
<Actions>
|
<Actions>
|
||||||
<ButtonGroup color="primary" variant="contained">
|
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="internship" />
|
||||||
<Button onClick={ handleAcceptWithoutComment } startIcon={ <StickerCheckOutline /> }>
|
|
||||||
{ t('accept-without-comments') }
|
|
||||||
</Button>
|
|
||||||
<Button size="small" onClick={ handleAcceptMenuOpen }><MenuDown /></Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<Menu open={ !!menuAnchor } anchorEl={ menuAnchor } onClose={ handleAcceptMenuClose }>
|
|
||||||
<MenuItem onClick={ handleAcceptWithoutComment }>{ t("accept-without-comments") }</MenuItem>
|
|
||||||
<MenuItem onClick={ handleAcceptWithComment }>{ t("accept-with-comments") }</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Button onClick={ handleDiscardAction } color="secondary" startIcon={ <StickerRemoveOutline /> }>
|
|
||||||
{ t('discard') }
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button component={ RouterLink } to={ route("home") }>
|
<Button component={ RouterLink } to={ route("home") }>
|
||||||
{ t('go-back') }
|
{ t('go-back') }
|
||||||
</Button>
|
</Button>
|
||||||
</Actions>
|
</Actions>
|
||||||
</Container>
|
</Container>
|
||||||
<Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md">
|
|
||||||
<DialogTitle>{ t("internship.discard.title") }</DialogTitle>
|
|
||||||
<DialogContent className={ classes.root }>
|
|
||||||
<Typography variant="body1">{ t("internship.discard.info") }</Typography>
|
|
||||||
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={ handleDiscardModalClose }>
|
|
||||||
{ t('cancel') }
|
|
||||||
</Button>
|
|
||||||
<Button onClick={ handleDiscard } color="primary" variant="contained">
|
|
||||||
{ t('confirm') }
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<Dialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md">
|
|
||||||
<DialogTitle>{ t("internship.accept.title") }</DialogTitle>
|
|
||||||
<DialogContent className={ classes.root }>
|
|
||||||
<Typography variant="body1">{ t("internship.accept.info") }</Typography>
|
|
||||||
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") }/>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={ handleAcceptModalClose }>
|
|
||||||
{ t('cancel') }
|
|
||||||
</Button>
|
|
||||||
<Button onClick={ handleAccept } color="primary" variant="contained">
|
|
||||||
{ t('confirm') }
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</Page>
|
</Page>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { PlanStep } from "@/pages/steps/plan";
|
|||||||
import { InsuranceState } from "@/state/reducer/insurance";
|
import { InsuranceState } from "@/state/reducer/insurance";
|
||||||
import { InsuranceStep } from "@/pages/steps/insurance";
|
import { InsuranceStep } from "@/pages/steps/insurance";
|
||||||
import { StudentStep } from "@/pages/steps/student";
|
import { StudentStep } from "@/pages/steps/student";
|
||||||
import { useDeadlines } from "@/hooks";
|
import { useCurrentEdition, useDeadlines } from "@/hooks";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions";
|
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions";
|
||||||
import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration";
|
import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration";
|
||||||
@ -36,6 +36,10 @@ export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
|||||||
document: plan,
|
document: plan,
|
||||||
state: plan.state,
|
state: plan.state,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: InternshipPlanActions.Reset,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +51,7 @@ export const MainPage = () => {
|
|||||||
const deadlines = useDeadlines();
|
const deadlines = useDeadlines();
|
||||||
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
|
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const edition = useCurrentEdition();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(updateInternshipInfo);
|
dispatch(updateInternshipInfo);
|
||||||
@ -64,13 +69,15 @@ export const MainPage = () => {
|
|||||||
if (insurance.required)
|
if (insurance.required)
|
||||||
yield <InsuranceStep key="insurance"/>;
|
yield <InsuranceStep key="insurance"/>;
|
||||||
|
|
||||||
yield <Step label={ t('steps.report.header') } until={ deadlines.report } key="report"/>
|
yield <Step label={ t('steps.report.header') } until={ deadlines.report } notBefore={ edition?.reportingStart } key="report"/>
|
||||||
yield <Step label={ t('steps.grade.header') } key="grade"/>
|
yield <Step label={ t('steps.grade.header') } key="grade"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Page my={ 6 }>
|
return <Page>
|
||||||
|
<Page.Header>
|
||||||
|
<Page.Title>{ t("pages.my-internship.header") }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant="h2">{ t("pages.my-internship.header") }</Typography>
|
|
||||||
<Stepper orientation="vertical" nonLinear>
|
<Stepper orientation="vertical" nonLinear>
|
||||||
{ Array.from(getSteps()) }
|
{ Array.from(getSteps()) }
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
@ -5,24 +5,29 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
|
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
|
||||||
import { FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index";
|
import { FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index";
|
||||||
import { route } from "@/routing";
|
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 { Actions, Step } from "@/components";
|
||||||
import React, { HTMLProps } from "react";
|
import React, { HTMLProps } from "react";
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||||
import { ContactAction, Status } from "@/pages/steps/common";
|
import { ContactAction, Status } from "@/pages/steps/common";
|
||||||
import { Description as DescriptionIcon } from "@material-ui/icons";
|
import { Description as DescriptionIcon } from "@material-ui/icons";
|
||||||
import { useDeadlines } from "@/hooks";
|
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 PlanActions = () => {
|
||||||
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.plan));
|
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.plan));
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const ReviewAction = (props: ButtonProps) =>
|
const FormAction = ({ children = t('steps.plan.submit'), ...props }: ButtonProps) =>
|
||||||
<Button startIcon={ <FileDownloadOutline /> } color="primary" { ...props }>{ t('steps.plan.download') }</Button>
|
<Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink } startIcon={ <FileUploadOutline /> } { ...props as any }>
|
||||||
|
{ children }
|
||||||
const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) =>
|
|
||||||
<Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink } startIcon={ <FileUploadOutline /> }>
|
|
||||||
{ t('steps.plan.submit') }
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
const TemplateAction = (props: ButtonProps) =>
|
const TemplateAction = (props: ButtonProps) =>
|
||||||
@ -30,20 +35,28 @@ const PlanActions = () => {
|
|||||||
{ t('steps.plan.template') }
|
{ t('steps.plan.template') }
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
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) {
|
switch (status) {
|
||||||
case "awaiting":
|
case "awaiting":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<ReviewAction />
|
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="plan" />
|
||||||
</Actions>
|
</Actions>
|
||||||
case "accepted":
|
case "accepted":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<ReviewAction/>
|
|
||||||
<FormAction variant="outlined" color="secondary">{ t('send-again') }</FormAction>
|
<FormAction variant="outlined" color="secondary">{ t('send-again') }</FormAction>
|
||||||
</Actions>
|
</Actions>
|
||||||
case "declined":
|
case "declined":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<FormAction>{ t('fix-errors') }</FormAction>
|
<FormAction>{ t('fix-errors') }</FormAction>
|
||||||
<ReviewAction />
|
|
||||||
<TemplateAction />
|
<TemplateAction />
|
||||||
<ContactAction/>
|
<ContactAction/>
|
||||||
</Actions>
|
</Actions>
|
||||||
@ -72,6 +85,8 @@ export const PlanStep = (props: StepProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const submission = useSelector<AppState, SubmissionState>(state => state.plan);
|
const submission = useSelector<AppState, SubmissionState>(state => state.plan);
|
||||||
|
const document = useSelector<AppState, InternshipDocument>(state => state.plan.document as InternshipDocument);
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
const status = getSubmissionStatus(submission);
|
const status = getSubmissionStatus(submission);
|
||||||
const deadlines = useDeadlines();
|
const deadlines = useDeadlines();
|
||||||
@ -83,10 +98,13 @@ export const PlanStep = (props: StepProps) => {
|
|||||||
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
|
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
|
||||||
until={ deadlines.proposal }
|
until={ deadlines.proposal }
|
||||||
state={ <Status submission={ submission } /> }>
|
state={ <Status submission={ submission } /> }>
|
||||||
<p>{ t(`steps.plan.info.${ status }`) }</p>
|
<div className={ spacing.vertical }>
|
||||||
|
<p>{ t(`steps.plan.info.${ status }`) }</p>
|
||||||
|
|
||||||
{ comment && <Box pb={ 2 }><PlanComment/></Box> }
|
{ comment && <Box pb={ 2 }><PlanComment/></Box> }
|
||||||
|
{ document && <FileInfo document={ document } /> }
|
||||||
|
|
||||||
<PlanActions/>
|
<PlanActions/>
|
||||||
|
</div>
|
||||||
</Step>;
|
</Step>;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export const StudentStep = (props: StepProps) => {
|
|||||||
</> : <>
|
</> : <>
|
||||||
<p>{ t('steps.personal-data.all-filled') }</p>
|
<p>{ t('steps.personal-data.all-filled') }</p>
|
||||||
<Actions>
|
<Actions>
|
||||||
<Button to={ route("user_profile") } variant="outlined" color="primary" component={ RouterLink } startIcon={ <AccountDetails /> }>
|
<Button to={ route("user_profile") } component={ RouterLink } startIcon={ <AccountDetails /> }>
|
||||||
{ t('steps.personal-data.actions.info') }
|
{ t('steps.personal-data.actions.info') }
|
||||||
</Button>
|
</Button>
|
||||||
</Actions>
|
</Actions>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Dispatch, useEffect } from "react";
|
import React, { Dispatch, useEffect } from "react";
|
||||||
import { Page } from "@/pages/base";
|
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 { Action, StudentActions, useDispatch } from "@/state/actions";
|
||||||
import { Route, Switch, useHistory, useLocation, useRouteMatch } from "react-router-dom";
|
import { Route, Switch, useHistory, useLocation, useRouteMatch } from "react-router-dom";
|
||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
@ -10,6 +10,8 @@ import { AppState } from "@/state/reducer";
|
|||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { UserActions } from "@/state/actions/user";
|
import { UserActions } from "@/state/actions/user";
|
||||||
import { getAuthorizeUrl } from "@/api/user";
|
import { getAuthorizeUrl } from "@/api/user";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Loading } from "@/components/loading";
|
||||||
|
|
||||||
const authorizeUser = (code?: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
const authorizeUser = (code?: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
||||||
const token = await api.user.login(code);
|
const token = await api.user.login(code);
|
||||||
@ -32,6 +34,7 @@ export const UserLoginPage = () => {
|
|||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const query = new URLSearchParams(useLocation().search);
|
const query = new URLSearchParams(useLocation().search);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSampleLogin = async () => {
|
const handleSampleLogin = async () => {
|
||||||
await dispatch(authorizeUser());
|
await dispatch(authorizeUser());
|
||||||
@ -54,6 +57,8 @@ export const UserLoginPage = () => {
|
|||||||
})();
|
})();
|
||||||
}, [ match.path ]);
|
}, [ match.path ]);
|
||||||
|
|
||||||
|
const inProgress = <Loading size="4rem" label={ t("login-in-progress") }/>
|
||||||
|
|
||||||
return <Page>
|
return <Page>
|
||||||
<Page.Header maxWidth="md">
|
<Page.Header maxWidth="md">
|
||||||
<Page.Title>Zaloguj się</Page.Title>
|
<Page.Title>Zaloguj się</Page.Title>
|
||||||
@ -66,11 +71,13 @@ export const UserLoginPage = () => {
|
|||||||
<Button fullWidth onClick={ handleSampleLogin } variant="contained" color="secondary">Zaloguj jako przykładowy student</Button>
|
<Button fullWidth onClick={ handleSampleLogin } variant="contained" color="secondary">Zaloguj jako przykładowy student</Button>
|
||||||
</Container>
|
</Container>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/pg`} render={
|
<Route path={`${match.path}/pg`} render={ () => {
|
||||||
() => (window.location.href = getAuthorizeUrl())
|
window.location.href = getAuthorizeUrl()
|
||||||
} />
|
|
||||||
|
return inProgress
|
||||||
|
} } />
|
||||||
<Route path={`${match.path}/check/pg`}>
|
<Route path={`${match.path}/check/pg`}>
|
||||||
Kod: { query.get("code") }
|
{ inProgress }
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import moment from "moment";
|
import moment from "moment-timezone";
|
||||||
import { sampleCourse } from "@/provider/dummy/student";
|
import { sampleCourse } from "@/provider/dummy/student";
|
||||||
|
|
||||||
export const sampleEdition: Edition = {
|
export const sampleEdition: Edition = {
|
||||||
|
@ -13,7 +13,7 @@ export const emptyInternship: Nullable<Internship> = {
|
|||||||
endDate: null,
|
endDate: null,
|
||||||
startDate: null,
|
startDate: null,
|
||||||
type: null,
|
type: null,
|
||||||
program: null,
|
program: [],
|
||||||
isAccepted: false,
|
isAccepted: false,
|
||||||
lengthInWeeks: 0,
|
lengthInWeeks: 0,
|
||||||
mentor: emptyMentor,
|
mentor: emptyMentor,
|
||||||
|
@ -25,7 +25,6 @@ export const sampleCourse: Course = {
|
|||||||
id: courseIdSequence(),
|
id: courseIdSequence(),
|
||||||
name: "Informatyka",
|
name: "Informatyka",
|
||||||
desiredSemesters: [6],
|
desiredSemesters: [6],
|
||||||
possibleProgramEntries: sampleProgramEntries,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sampleStudent: Student = {
|
export const sampleStudent: Student = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Serializable, SerializationTransformer } from "@/serialization/types";
|
import { Serializable, SerializationTransformer } from "@/serialization/types";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import { momentSerializationTransformer } from "@/serialization/moment";
|
import { momentSerializationTransformer } from "@/serialization/moment";
|
||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
|
|
||||||
export const editionSerializationTransformer: SerializationTransformer<Edition> = {
|
export const editionSerializationTransformer: SerializationTransformer<Edition> = {
|
||||||
transform(subject: Edition, context?: unknown): Serializable<Edition> {
|
transform(subject: Edition, context?: unknown): Serializable<Edition> {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Internship, InternshipType } from "@/data";
|
import { Internship, InternshipType } from "@/data";
|
||||||
import { Serializable, SerializationTransformer } from "@/serialization/types";
|
import { Serializable, SerializationTransformer } from "@/serialization/types";
|
||||||
import { momentSerializationTransformer } from "@/serialization/moment";
|
import { momentSerializationTransformer } from "@/serialization/moment";
|
||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
|
|
||||||
export const internshipSerializationTransformer: SerializationTransformer<Internship> = {
|
export const internshipSerializationTransformer: SerializationTransformer<Internship> = {
|
||||||
transform: (internship: Internship): Serializable<Internship> => ({
|
transform: (internship: Internship): Serializable<Internship> => ({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SerializationTransformer } from "@/serialization/types";
|
import { SerializationTransformer } from "@/serialization/types";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment-timezone";
|
||||||
|
|
||||||
export const momentSerializationTransformer: SerializationTransformer<Moment | null, string> = {
|
export const momentSerializationTransformer: SerializationTransformer<Moment | null, string> = {
|
||||||
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,
|
reverseTransform: (subject: string) => subject ? moment(subject) : null,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
|
|
||||||
type Simplify<T> = string |
|
type Simplify<T> = string |
|
||||||
T extends string ? string :
|
T extends string ? string :
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Action } from "@/state/actions/base";
|
import { Action } from "@/state/actions/base";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
|
import { InternshipProgramEntry } from "@/data";
|
||||||
|
|
||||||
export enum EditionActions {
|
export enum EditionActions {
|
||||||
Set = 'SET_EDITION',
|
Set = 'SET_EDITION',
|
||||||
@ -7,6 +8,7 @@ export enum EditionActions {
|
|||||||
|
|
||||||
export interface SetAction extends Action<EditionActions.Set> {
|
export interface SetAction extends Action<EditionActions.Set> {
|
||||||
edition: Edition,
|
edition: Edition,
|
||||||
|
program: InternshipProgramEntry[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditionAction = SetAction;
|
export type EditionAction = SetAction;
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
} from "@/state/actions/submission";
|
} from "@/state/actions/submission";
|
||||||
|
|
||||||
import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration";
|
import { InternshipDocument, SubmissionState } from "@/api/dto/internship-registration";
|
||||||
|
import { Action } from "@/state/actions/base";
|
||||||
|
|
||||||
export enum InternshipPlanActions {
|
export enum InternshipPlanActions {
|
||||||
Send = "SEND_PLAN",
|
Send = "SEND_PLAN",
|
||||||
@ -14,8 +15,11 @@ export enum InternshipPlanActions {
|
|||||||
Approve = "RECEIVE_PLAN_APPROVE",
|
Approve = "RECEIVE_PLAN_APPROVE",
|
||||||
Decline = "RECEIVE_PLAN_DECLINE",
|
Decline = "RECEIVE_PLAN_DECLINE",
|
||||||
Receive = "RECEIVE_PLAN_STATE",
|
Receive = "RECEIVE_PLAN_STATE",
|
||||||
|
Reset = "RESET_PLAN",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResetPlanAction extends Action<InternshipPlanActions.Reset> {}
|
||||||
|
|
||||||
export interface SendPlanAction extends SendSubmissionAction<InternshipPlanActions.Send> {
|
export interface SendPlanAction extends SendSubmissionAction<InternshipPlanActions.Send> {
|
||||||
document: InternshipDocument;
|
document: InternshipDocument;
|
||||||
}
|
}
|
||||||
@ -40,4 +44,6 @@ export type InternshipPlanAction
|
|||||||
| SavePlanAction
|
| SavePlanAction
|
||||||
| ReceivePlanApproveAction
|
| ReceivePlanApproveAction
|
||||||
| ReceivePlanDeclineAction
|
| ReceivePlanDeclineAction
|
||||||
| ReceivePlanUpdateAction;
|
| ReceivePlanUpdateAction
|
||||||
|
| ResetPlanAction
|
||||||
|
;
|
||||||
|
@ -17,7 +17,6 @@ export enum InternshipProposalActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SendProposalAction extends SendSubmissionAction<InternshipProposalActions.Send> {
|
export interface SendProposalAction extends SendSubmissionAction<InternshipProposalActions.Send> {
|
||||||
internship: Internship;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
|
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
|
||||||
|
@ -2,15 +2,26 @@ import { Edition } from "@/data/edition";
|
|||||||
import { EditionAction, EditionActions } from "@/state/actions/edition";
|
import { EditionAction, EditionActions } from "@/state/actions/edition";
|
||||||
import { editionSerializationTransformer, Serializable } from "@/serialization";
|
import { editionSerializationTransformer, Serializable } from "@/serialization";
|
||||||
import { LoginAction, LogoutAction, UserActions } from "@/state/actions";
|
import { LoginAction, LogoutAction, UserActions } from "@/state/actions";
|
||||||
|
import { InternshipProgramEntry } from "@/data";
|
||||||
|
|
||||||
export type EditionState = Serializable<Edition> | 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 => {
|
const editionReducer = (state: EditionState = initialEditionState, action: EditionAction | LogoutAction | LoginAction): EditionState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case EditionActions.Set:
|
case EditionActions.Set:
|
||||||
return editionSerializationTransformer.transform(action.edition);
|
return {
|
||||||
|
...state,
|
||||||
|
edition: editionSerializationTransformer.transform(action.edition),
|
||||||
|
program: action.program,
|
||||||
|
};
|
||||||
case UserActions.Login:
|
case UserActions.Login:
|
||||||
case UserActions.Logout:
|
case UserActions.Logout:
|
||||||
return initialEditionState;
|
return initialEditionState;
|
||||||
|
@ -22,4 +22,4 @@ export type AppState = ReturnType<typeof rootReducer>;
|
|||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
||||||
export const isReady = (state: AppState) => !!state.edition;
|
export const isReady = (state: AppState) => !!(state.edition?.edition);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { InternshipPlanAction, InternshipPlanActions, InternshipProposalActions } from "@/state/actions";
|
import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions";
|
||||||
import { Serializable } from "@/serialization/types";
|
import { Serializable } from "@/serialization/types";
|
||||||
import {
|
import {
|
||||||
createSubmissionReducer,
|
createSubmissionReducer,
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
import { Reducer } from "react";
|
import { Reducer } from "react";
|
||||||
import { SubmissionAction } from "@/state/actions/submission";
|
import { SubmissionAction } from "@/state/actions/submission";
|
||||||
import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
|
import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
|
||||||
|
import { Api } from "mdi-material-ui";
|
||||||
|
|
||||||
export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & {
|
export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & {
|
||||||
document: Serializable<InternshipDocument> | null;
|
document: Serializable<InternshipDocument> | null;
|
||||||
@ -40,9 +41,14 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla
|
|||||||
document: action.document,
|
document: action.document,
|
||||||
}
|
}
|
||||||
case InternshipPlanActions.Receive:
|
case InternshipPlanActions.Receive:
|
||||||
|
if (state.overwritten) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
accepted: action.state === ApiSubmissionState.Accepted,
|
accepted: action.state === ApiSubmissionState.Accepted,
|
||||||
|
declined: action.state === ApiSubmissionState.Rejected,
|
||||||
sent: [
|
sent: [
|
||||||
ApiSubmissionState.Accepted,
|
ApiSubmissionState.Accepted,
|
||||||
ApiSubmissionState.Rejected,
|
ApiSubmissionState.Rejected,
|
||||||
@ -51,6 +57,9 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla
|
|||||||
document: action.document,
|
document: action.document,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case InternshipPlanActions.Reset:
|
||||||
|
return defaultInternshipPlanState;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -42,12 +42,16 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter
|
|||||||
case InternshipProposalActions.Send:
|
case InternshipProposalActions.Send:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
proposal: internshipSerializationTransformer.transform(action.internship),
|
|
||||||
}
|
}
|
||||||
case InternshipProposalActions.Receive:
|
case InternshipProposalActions.Receive:
|
||||||
|
if (state.overwritten) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
accepted: action.state === ApiSubmissionState.Accepted,
|
accepted: action.state === ApiSubmissionState.Accepted,
|
||||||
|
declined: action.state === ApiSubmissionState.Rejected,
|
||||||
sent: [
|
sent: [
|
||||||
ApiSubmissionState.Accepted,
|
ApiSubmissionState.Accepted,
|
||||||
ApiSubmissionState.Rejected,
|
ApiSubmissionState.Rejected,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DeanApproval } from "@/data/deanApproval";
|
import { DeanApproval } from "@/data/deanApproval";
|
||||||
import { Action } from "@/state/actions";
|
import { Action } from "@/state/actions";
|
||||||
import { momentSerializationTransformer } from "@/serialization";
|
import { momentSerializationTransformer } from "@/serialization";
|
||||||
import moment from "moment";
|
import moment from "moment-timezone";
|
||||||
import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, SubmissionAction } from "@/state/actions/submission";
|
import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, SubmissionAction } from "@/state/actions/submission";
|
||||||
|
|
||||||
export type SubmissionStatus = "draft" | "awaiting" | "accepted" | "declined";
|
export type SubmissionStatus = "draft" | "awaiting" | "accepted" | "declined";
|
||||||
@ -12,6 +12,7 @@ export type SubmissionState = {
|
|||||||
sentOn: string | null;
|
sentOn: string | null;
|
||||||
declined: boolean;
|
declined: boolean;
|
||||||
comment: string | null;
|
comment: string | null;
|
||||||
|
overwritten: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MayRequireDeanApproval = {
|
export type MayRequireDeanApproval = {
|
||||||
@ -24,6 +25,7 @@ export const defaultSubmissionState: SubmissionState = {
|
|||||||
sentOn: null,
|
sentOn: null,
|
||||||
declined: false,
|
declined: false,
|
||||||
comment: null,
|
comment: null,
|
||||||
|
overwritten: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultDeanApprovalsState: MayRequireDeanApproval = {
|
export const defaultDeanApprovalsState: MayRequireDeanApproval = {
|
||||||
@ -56,6 +58,7 @@ export function createSubmissionReducer<TState, TActionType, TAction extends Act
|
|||||||
accepted: true,
|
accepted: true,
|
||||||
declined: false,
|
declined: false,
|
||||||
comment: (action as ReceiveSubmissionApproveAction<any>).comment,
|
comment: (action as ReceiveSubmissionApproveAction<any>).comment,
|
||||||
|
overwritten: true,
|
||||||
}
|
}
|
||||||
case SubmissionAction.Decline:
|
case SubmissionAction.Decline:
|
||||||
return {
|
return {
|
||||||
@ -63,6 +66,7 @@ export function createSubmissionReducer<TState, TActionType, TAction extends Act
|
|||||||
accepted: false,
|
accepted: false,
|
||||||
declined: true,
|
declined: true,
|
||||||
comment: (action as ReceiveSubmissionDeclineAction<any>).comment,
|
comment: (action as ReceiveSubmissionDeclineAction<any>).comment,
|
||||||
|
overwritten: true,
|
||||||
}
|
}
|
||||||
case SubmissionAction.Send:
|
case SubmissionAction.Send:
|
||||||
return {
|
return {
|
||||||
@ -72,6 +76,7 @@ export function createSubmissionReducer<TState, TActionType, TAction extends Act
|
|||||||
accepted: false,
|
accepted: false,
|
||||||
declined: false,
|
declined: false,
|
||||||
comment: null,
|
comment: null,
|
||||||
|
overwritten: false,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
@import "variables";
|
@import "variables";
|
||||||
|
|
||||||
.header {
|
header {
|
||||||
height: 110px;
|
|
||||||
background: $main;
|
background: $main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 110px;
|
||||||
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
@ -20,3 +20,14 @@
|
|||||||
.proposal__header:not(:first-child) {
|
.proposal__header:not(:first-child) {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > *:not(:last-child) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
@ -4,7 +4,6 @@ export const studentTheme = responsiveFontSizes(createMuiTheme({
|
|||||||
props: {
|
props: {
|
||||||
MuiGrid: {
|
MuiGrid: {
|
||||||
spacing: 3,
|
spacing: 3,
|
||||||
xs: 12,
|
|
||||||
},
|
},
|
||||||
MuiContainer: {
|
MuiContainer: {
|
||||||
maxWidth: "md"
|
maxWidth: "md"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Moment } from "moment";
|
import { Moment } from "moment-timezone";
|
||||||
import Holidays from "date-holidays";
|
import Holidays from "date-holidays";
|
||||||
|
|
||||||
const holidays = new Holidays()
|
const holidays = new Holidays()
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
copyright: Wydział ETI Politechniki Gdańskiej © {{ date, YYYY }}
|
copyright: Wydział ETI Politechniki Gdańskiej © {{ date, YYYY }}
|
||||||
|
|
||||||
login: zaloguj się
|
login: zaloguj się
|
||||||
|
login-in-progress: Logowanie w toku, proszę czekać...
|
||||||
logout: wyloguj się
|
logout: wyloguj się
|
||||||
logged-in-as: zalogowany jako <1>{{ name }}</1>
|
logged-in-as: zalogowany jako <1>{{ name }}</1>
|
||||||
|
|
||||||
until: do {{ date, DD MMMM YYYY }}
|
until: do {{ date, DD MMMM YYYY }}
|
||||||
|
not-before: od {{ date, DD MMMM YYYY }}
|
||||||
late: '{{ by, humanize }} spóźnienia'
|
late: '{{ by, humanize }} spóźnienia'
|
||||||
left: jeszcze {{ left, humanize }}
|
left: jeszcze {{ left, humanize }}
|
||||||
|
|
||||||
@ -79,6 +81,7 @@ forms:
|
|||||||
country: Kraj
|
country: Kraj
|
||||||
street: Ulica
|
street: Ulica
|
||||||
building: Nr budynku
|
building: Nr budynku
|
||||||
|
program: Program praktyki (wybierz {{ count }})
|
||||||
help:
|
help:
|
||||||
weeks: Wartość wyliczana automatycznie
|
weeks: Wartość wyliczana automatycznie
|
||||||
working-hours: Liczba godzin w tygodniu roboczym
|
working-hours: Liczba godzin w tygodniu roboczym
|
||||||
@ -112,6 +115,11 @@ submission:
|
|||||||
draft: "wersja robocza"
|
draft: "wersja robocza"
|
||||||
|
|
||||||
internship:
|
internship:
|
||||||
|
validation:
|
||||||
|
has-errors: "W formularzu zostały znalezione błędy"
|
||||||
|
error:
|
||||||
|
declared_hours:
|
||||||
|
empty: "Brak zadeklarowanej długości praktyki."
|
||||||
intern:
|
intern:
|
||||||
semester: semestr {{ semester, roman }}
|
semester: semestr {{ semester, roman }}
|
||||||
album: "numer albumu {{ album }}"
|
album: "numer albumu {{ album }}"
|
||||||
@ -133,6 +141,7 @@ internship:
|
|||||||
place: "Miejsce odbywania praktyki"
|
place: "Miejsce odbywania praktyki"
|
||||||
kind: "Rodzaj i program praktyki"
|
kind: "Rodzaj i program praktyki"
|
||||||
mentor: "Zakładowy opiekun praktyki"
|
mentor: "Zakładowy opiekun praktyki"
|
||||||
|
program: "Realizowane punkty programu praktyki"
|
||||||
discard:
|
discard:
|
||||||
title: "Odrzuć zgłoszenie praktyki"
|
title: "Odrzuć zgłoszenie praktyki"
|
||||||
info: "Poniższa informacja zostanie przekazana praktykantowi w celu poprawy zgłoszenia."
|
info: "Poniższa informacja zostanie przekazana praktykantowi w celu poprawy zgłoszenia."
|
||||||
@ -140,6 +149,14 @@ internship:
|
|||||||
title: "Zaakceptuj zgłoszenie praktyki"
|
title: "Zaakceptuj zgłoszenie praktyki"
|
||||||
info: "Poniższa informacja zostanie przekazana praktykantowi."
|
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:
|
steps:
|
||||||
personal-data:
|
personal-data:
|
||||||
header: "Uzupełnienie danych"
|
header: "Uzupełnienie danych"
|
||||||
@ -155,7 +172,8 @@ steps:
|
|||||||
header: "Zgłoszenie praktyki"
|
header: "Zgłoszenie praktyki"
|
||||||
info:
|
info:
|
||||||
draft: >
|
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: >
|
awaiting: >
|
||||||
Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o
|
Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o
|
||||||
akceptacji bądź konieczności wprowadzenia zmian.
|
akceptacji bądź konieczności wprowadzenia zmian.
|
||||||
@ -171,7 +189,8 @@ steps:
|
|||||||
info:
|
info:
|
||||||
draft: >
|
draft: >
|
||||||
W porozumieniu z firmą w której odbywają się praktyki należy sporządzić Indywidualny Plan Praktyk zgodnie z
|
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: >
|
awaiting: >
|
||||||
Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i
|
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.
|
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 }}"
|
minimum-hours: "Minimalna liczba godzin wynosi {{ hours }}"
|
||||||
|
|
||||||
contact-coordinator: "Skontaktuj się z koordynatorem"
|
contact-coordinator: "Skontaktuj się z koordynatorem"
|
||||||
|
download: "pobierz"
|
||||||
|
@ -3858,6 +3858,11 @@ filesize@6.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.0.1.tgz#f850b509909c7c86f7e450ea19006c31c2ed3d2f"
|
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.0.1.tgz#f850b509909c7c86f7e450ea19006c31c2ed3d2f"
|
||||||
integrity sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg==
|
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:
|
fill-range@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
||||||
|
Loading…
Reference in New Issue
Block a user