Merge pull request 'feature/management' (#19) from feature/management into master
This commit is contained in:
commit
8e1fe48393
7
deploy-dev.sh
Normal file
7
deploy-dev.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
npx webpack --mode production --progress || exit $?
|
||||||
|
|
||||||
|
rsync -azv $BASEDIR/public/* system-praktyk@kadet.net:~/dev/front
|
||||||
|
rsync -azv $BASEDIR/build/* system-praktyk@kadet.net:~/dev/front
|
||||||
|
|
7
deploy-stg.sh
Normal file
7
deploy-stg.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
npx webpack --mode production --progress || exit $?
|
||||||
|
|
||||||
|
rsync -azv $BASEDIR/public/* system-praktyk@kadet.net:~/stg/front
|
||||||
|
rsync -azv $BASEDIR/build/* system-praktyk@kadet.net:~/stg/front
|
||||||
|
|
@ -5,11 +5,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.9.0",
|
"@babel/core": "7.9.0",
|
||||||
"@babel/preset-typescript": "^7.10.1",
|
"@babel/preset-typescript": "^7.10.1",
|
||||||
|
"@ckeditor/ckeditor5-build-classic": "^23.1.0",
|
||||||
|
"@ckeditor/ckeditor5-react": "^3.0.0",
|
||||||
"@date-io/moment": "^1.3.13",
|
"@date-io/moment": "^1.3.13",
|
||||||
"@material-ui/core": "^4.10.1",
|
"@material-ui/core": "^4.10.1",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.55",
|
"@material-ui/lab": "^4.0.0-alpha.55",
|
||||||
"@material-ui/pickers": "^3.2.10",
|
"@material-ui/pickers": "^3.2.10",
|
||||||
|
"@svgr/webpack": "^5.5.0",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/node": "^12.0.0",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react": "^16.9.0",
|
"@types/react": "^16.9.0",
|
||||||
@ -38,9 +41,9 @@
|
|||||||
"i18next": "^19.6.0",
|
"i18next": "^19.6.0",
|
||||||
"i18next-browser-languagedetector": "^5.0.0",
|
"i18next-browser-languagedetector": "^5.0.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"material-table": "^1.69.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-timezone": "^2.26.0",
|
|
||||||
"moment-timezone": "^0.5.31",
|
"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",
|
||||||
|
53
public/img/pg-logo.svg
Normal file
53
public/img/pg-logo.svg
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
id="prefix__PGLogotyp"
|
||||||
|
viewBox="0 0 143 64"
|
||||||
|
version="1.1">
|
||||||
|
<metadata
|
||||||
|
id="metadata935">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs905">
|
||||||
|
<style
|
||||||
|
id="style903">.prefix__cls-1{fill:#fff}</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
id="prefix__Path_108"
|
||||||
|
d="m 30.446,9.626 -3.339,2.357 1.375,1.866 2.652,-1.964 h 3.045 l 0.982,-7.17 -6.777,-1.277 -0.687,3.732 2.259,0.393 0.295,-1.473 2.357,0.491 -0.393,3.045 z"
|
||||||
|
class="prefix__cls-1"
|
||||||
|
data-name="Path 108" />
|
||||||
|
<path
|
||||||
|
id="prefix__Path_109"
|
||||||
|
d="m 111.866,11.884 2.652,1.964 1.375,-1.866 -3.241,-2.357 h -1.866 l -0.393,-3.045 2.455,-0.491 0.2,1.473 2.259,-0.393 -0.593,-3.731 -6.875,1.277 0.982,7.17 z"
|
||||||
|
class="prefix__cls-1"
|
||||||
|
data-name="Path 109" />
|
||||||
|
<path
|
||||||
|
id="prefix__Path_110"
|
||||||
|
d="m 130.036,44.884 h -2.259 v 2.259 h 7.464 l 2.159,3.045 -5.4,10.705 h -7.857 l 1.67,-3.339 h 3.241 l 3.536,-7.17 h -8.741 V 39.777 l -9.92,-7.464 2.161,-2.848 -1.866,-1.375 -3.339,4.42 v 2.554 l -11.2,2.455 -3.437,-0.982 V 34.571 H 93.991 V 49.4 L 71.4,58.045 48.813,49.4 V 32.214 h -2.259 v 1.964 l -3.634,1.08 h -10.9 v -2.847 l -3.339,-4.42 -1.866,1.375 2.161,2.848 -9.92,7.563 v 10.607 h -8.743 l 3.536,7.17 h 3.241 l 1.67,3.339 H 10.9 L 5.6,50.286 7.761,47.241 h 7.464 V 44.982 H 12.964 V 35.75 l 4.321,-3.241 V 27.795 L 21.8,24.357 20.523,22.589 27.3,17.482 35.259,21.8 v 8.45 L 44.1,31.527 45.473,27.5 h 3.536 V 8.643 h 45.179 v 21.214 h 3.536 l 1.473,4.321 8.741,-4.125 V 21.8 l 7.955,-4.321 6.777,5.107 -1.375,1.768 4.518,3.438 v 4.714 l 4.321,3.241 v 9.134 z m -86.136,-27.5 1.473,-4.42 h 1.179 v 8.054 H 44 l -6.58,4.911 v -5.3 z m -6.384,10.9 z M 46.65,25.338 H 43.9 l -1.277,3.732 -4.616,-0.589 6.875,-5.107 h 1.768 z m 58.732,3.339 -5.009,2.357 L 99.2,27.6 h -2.75 v -4.323 h 1.768 z m -7.759,-15.714 1.473,4.42 6.384,3.143 v 5.3 L 98.9,20.92 h -2.554 v -8.054 h 1.277 z m 39.977,12.573 -1.377,1.864 3.634,2.75 3.143,-4.125 v -1.864 l -6.384,-4.714 -5.009,6.482 v 3.536 l 4.223,3.143 v 4.518 l -7.857,-5.893 v -4.715 l -4.714,-3.536 1.375,-1.768 -6.679,-5.009 2.063,-1.08 5.893,4.42 -1.375,1.768 4.223,3.241 1.375,-1.866 -4.223,-3.143 1.375,-1.768 -5.107,-3.83 v -2.652 l 6.286,4.714 -1.375,1.768 3.438,2.554 1.375,-1.866 -3.437,-2.554 1.375,-1.768 -7.761,-5.795 V 5.795 l 9.036,6.777 -1.375,1.768 2.554,1.964 1.375,-1.866 -2.554,-1.964 1.375,-1.768 -10.214,-7.76 V 0 h -2.259 v 2.455 l -3.634,0.688 0.393,2.259 3.241,-0.589 v 7.857 l -6.187,3.339 -2.848,-2.161 -1.375,1.866 1.964,1.473 -3.732,2.063 -6.973,-3.437 L 99.1,10.705 H 96.25 V 6.384 h -49.6 v 4.321 H 43.8 L 42.032,15.812 35.063,19.25 31.33,17.188 33.294,15.715 31.92,13.848 29.072,16.009 22.884,12.67 V 4.813 L 26.125,5.402 26.518,3.143 22.884,2.456 V 0 h -2.259 v 2.946 l -10.214,7.854 1.375,1.768 -2.554,1.964 1.375,1.868 2.554,-1.964 -1.375,-1.766 9.036,-6.777 v 2.652 l -7.759,5.795 1.375,1.768 -3.438,2.553 1.375,1.866 3.438,-2.554 -1.375,-1.768 6.286,-4.714 v 2.652 l -5.107,3.83 1.375,1.768 -4.223,3.143 1.375,1.67 4.223,-3.241 -1.277,-1.67 5.893,-4.42 2.063,1.08 -6.679,5.009 1.375,1.768 -4.714,3.536 V 31.33 L 7.17,37.223 v -4.518 l 4.223,-3.143 V 25.83 L 6.384,19.348 0,24.063 v 1.866 L 3.143,30.054 6.777,27.304 5.5,25.536 3.634,26.911 2.357,25.143 5.893,22.491 9.036,26.616 v 1.768 l -4.223,3.143 v 6.482 l 1.866,2.455 4.027,-3.045 v 7.563 H 6.58 L 2.946,50.089 9.429,63.25 h 11.785 v -2.357 l -2.75,-5.6 h -3.241 l -1.277,-2.554 h 7.268 V 40.955 L 23.669,39.187 33,43.9 v 6.286 l -2.161,3.144 2.061,4.224 h 3.241 l 1.67,3.339 h -7.856 l -4.027,-8.152 1.964,-2.848 v -5.009 h -4.321 v 2.259 h 2.063 v 2.063 l -2.357,3.339 5.3,10.607 h 11.789 v -2.357 l -2.75,-5.6 h -3.241 l -0.884,-1.768 1.866,-2.652 V 42.33 l -9.527,-4.91 3.929,-2.946 v 2.848 H 43.411 L 46.652,36.34 V 50.679 L 71.5,60.205 96.348,50.678 V 38.795 l 3.438,0.982 13.554,-2.946 V 34.67 l 3.929,2.946 -9.527,4.911 v 8.446 l 1.866,2.652 -0.884,1.768 h -3.241 l -2.75,5.6 v 2.357 h 11.786 l 5.3,-10.607 -2.355,-3.343 v -2.061 h 2.062 V 45.08 h -4.321 v 5.009 l 1.964,2.848 -4.027,8.152 h -7.857 l 1.67,-3.339 H 110.2 L 112.263,53.527 110,50.384 V 44.1 l 9.33,-4.714 2.455,1.768 v 11.784 h 7.268 l -1.277,2.554 h -3.241 l -2.75,5.6 v 2.357 h 11.786 l 6.482,-13.161 -3.634,-5.107 h -4.125 v -7.565 l 4.027,3.045 1.866,-2.455 v -6.483 l -4.223,-3.143 v -1.768 l 3.143,-4.125 3.536,2.652 -1.277,1.67 z"
|
||||||
|
class="prefix__cls-1"
|
||||||
|
data-name="Path 110" />
|
||||||
|
<path
|
||||||
|
id="prefix__Path_111"
|
||||||
|
d="m 63.25,20.822 a 1.2,1.2 0 0 0 -0.884,-0.295 h -2.357 v 3.045 h 2.357 a 1.85,1.85 0 0 0 0.786,-0.2 0.658,0.658 0 0 0 0.393,-0.687 V 21.607 A 0.658,0.658 0 0 0 63.25,20.821"
|
||||||
|
class="prefix__cls-1"
|
||||||
|
data-name="Path 111" />
|
||||||
|
<path
|
||||||
|
id="prefix__Path_112"
|
||||||
|
d="m 86.331,21.215 v 2.063 h -8.349 v -2.063 l -0.393,-3.437 2.063,1.375 -0.491,1.964 h 2.455 l -0.786,-1.964 1.375,-2.259 1.375,2.259 -0.786,1.964 h 2.456 l -0.491,-1.964 2.063,-1.375 z m -0.589,10.116 -2.652,-0.295 0.393,2.652 h -2.554 l 0.393,-2.652 -2.652,0.295 v -2.554 l 2.652,0.393 -0.393,-2.652 h 2.554 l -0.393,2.554 2.652,-0.295 z m 0,10.313 -2.652,-0.393 0.393,2.652 h -2.554 l 0.393,-2.652 -2.652,0.295 v -2.555 l 2.652,0.393 -0.393,-2.554 h 2.554 l -0.393,2.554 2.652,-0.295 v 2.554 z M 72.286,44.102 H 70.813 V 18.759 h 1.473 z M 65.411,22.786 a 3.531,3.531 0 0 1 -0.2,1.08 2.648,2.648 0 0 1 -0.687,0.786 3.072,3.072 0 0 1 -0.982,0.491 4.221,4.221 0 0 1 -1.179,0.2 h -2.354 v 3.536 h -1.768 v -9.727 h 4.129 a 4.93,4.93 0 0 1 1.179,0.2 3.072,3.072 0 0 1 0.982,0.491 1.527,1.527 0 0 1 0.589,0.786 2.127,2.127 0 0 1 0.2,1.179 v 0.982 z m -0.295,18.955 a 3.981,3.981 0 0 1 -0.884,0.491 c -0.295,0.1 -0.589,0.295 -0.884,0.393 a 2.868,2.868 0 0 1 -0.884,0.2 3.028,3.028 0 0 1 -0.982,0.1 5.944,5.944 0 0 1 -1.473,-0.2 2.24,2.24 0 0 1 -1.179,-0.589 3.845,3.845 0 0 1 -0.786,-0.982 3.137,3.137 0 0 1 -0.295,-1.375 v -3.734 a 3.137,3.137 0 0 1 0.295,-1.375 3.845,3.845 0 0 1 0.786,-0.982 3.319,3.319 0 0 1 1.179,-0.589 7.581,7.581 0 0 1 1.473,-0.2 4.758,4.758 0 0 1 1.768,0.295 3.7,3.7 0 0 1 1.473,0.884 l -0.687,1.277 a 7.326,7.326 0 0 0 -1.179,-0.688 3.024,3.024 0 0 0 -1.277,-0.295 2.163,2.163 0 0 0 -0.786,0.1 4.787,4.787 0 0 0 -0.687,0.295 c -0.2,0.1 -0.295,0.295 -0.491,0.491 a 1.42,1.42 0 0 0 -0.2,0.688 v 3.732 a 1.42,1.42 0 0 0 0.2,0.688 1.184,1.184 0 0 0 0.491,0.491 4.788,4.788 0 0 0 0.688,0.295 3.585,3.585 0 0 0 1.67,0 3.489,3.489 0 0 0 0.884,-0.393 V 38.602 H 61.97 v -1.28 h 3.241 v 4.42 z M 51.17,10.902 v 36.83 l 20.33,7.857 20.33,-7.857 v -36.83 z"
|
||||||
|
class="prefix__cls-1"
|
||||||
|
data-name="Path 112" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.9 KiB |
@ -5,6 +5,7 @@
|
|||||||
<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"/>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
|
||||||
<title>Zgłoszenie praktyki studenckiej</title>
|
<title>Zgłoszenie praktyki studenckiej</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
</head>
|
</head>
|
||||||
|
@ -17,7 +17,6 @@ export const courseDtoTransformer: Transformer<CourseDTO, Course> = {
|
|||||||
id: subject.id,
|
id: subject.id,
|
||||||
name: subject.name,
|
name: subject.name,
|
||||||
desiredSemesters: [],
|
desiredSemesters: [],
|
||||||
possibleProgramEntries: [], // todo
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,9 @@ export const internshipTypeDtoTransformer: Transformer<InternshipTypeDTO, Intern
|
|||||||
description: subject.description ? {
|
description: subject.description ? {
|
||||||
pl: subject.description,
|
pl: subject.description,
|
||||||
en: subject.descriptionEng || ""
|
en: subject.descriptionEng || ""
|
||||||
} : undefined
|
} : undefined,
|
||||||
|
requiresDeanApproval: parseInt(subject.id || "0") == 4,
|
||||||
|
requiresInsurance: parseInt(subject.id || "0") >= 4,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reverseTransform(subject: InternshipType, context?: unknown): InternshipTypeDTO {
|
reverseTransform(subject: InternshipType, context?: unknown): InternshipTypeDTO {
|
||||||
|
@ -13,7 +13,7 @@ import * as internship from "./internship";
|
|||||||
import * as upload from "./upload";
|
import * as upload from "./upload";
|
||||||
|
|
||||||
export const axios = Axios.create({
|
export const axios = Axios.create({
|
||||||
baseURL: process.env.API_BASE_URL || "https://system-praktyk.stg.kadet.net/api/",
|
baseURL: process.env.API_BASE_URL || `https://${window.location.hostname}/api/`,
|
||||||
})
|
})
|
||||||
|
|
||||||
axios.interceptors.request.use(config => {
|
axios.interceptors.request.use(config => {
|
||||||
|
@ -21,20 +21,21 @@ export class ValidationError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiError {
|
||||||
|
key: string;
|
||||||
|
parameters: { [name: string]: string },
|
||||||
|
}
|
||||||
|
|
||||||
interface UpdateResponse {
|
interface UpdateResponse {
|
||||||
status: SubmissionState;
|
status: SubmissionState;
|
||||||
errors?: string[];
|
errors?: ApiError[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<SubmissionState> {
|
export async function update(internship: Nullable<InternshipRegistrationUpdate>): Promise<SubmissionState> {
|
||||||
const response = (await axios.put<UpdateResponse>(INTERNSHIP_REGISTRATION_ENDPOINT, internship)).data;
|
const response = (await axios.put<UpdateResponse>(INTERNSHIP_REGISTRATION_ENDPOINT, internship)).data;
|
||||||
|
|
||||||
if (response.status == SubmissionState.Draft) {
|
if (response.status == SubmissionState.Draft) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(response.errors || []);
|
||||||
response.errors?.map(
|
|
||||||
msg => ({ key: msg, parameters: {} })
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.status;
|
return response.status;
|
||||||
|
@ -3,7 +3,7 @@ import { PageDTO, pageDtoTransformer } from "./dto/page"
|
|||||||
import { axios } from "@/api/index";
|
import { axios } from "@/api/index";
|
||||||
import { prepare } from "@/routing";
|
import { prepare } from "@/routing";
|
||||||
|
|
||||||
const STATIC_PAGE_ENDPOINT = "/staticPage/:slug"
|
export const STATIC_PAGE_ENDPOINT = "/staticPage/:slug"
|
||||||
|
|
||||||
export async function get(slug: string): Promise<Page> {
|
export async function get(slug: string): Promise<Page> {
|
||||||
const response = await axios.get<PageDTO>(prepare(STATIC_PAGE_ENDPOINT, { slug }))
|
const response = await axios.get<PageDTO>(prepare(STATIC_PAGE_ENDPOINT, { slug }))
|
||||||
|
@ -2,7 +2,7 @@ import { InternshipType } from "@/data";
|
|||||||
import { axios } from "@/api/index";
|
import { axios } from "@/api/index";
|
||||||
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||||
|
|
||||||
const AVAILABLE_INTERNSHIP_TYPES = '/internshipTypes';
|
const AVAILABLE_INTERNSHIP_TYPES = '/internshipTypes/current';
|
||||||
|
|
||||||
export async function available(): Promise<InternshipType[]> {
|
export async function available(): Promise<InternshipType[]> {
|
||||||
const response = await axios.get<InternshipTypeDTO[]>(AVAILABLE_INTERNSHIP_TYPES);
|
const response = await axios.get<InternshipTypeDTO[]>(AVAILABLE_INTERNSHIP_TYPES);
|
||||||
|
@ -2,6 +2,7 @@ 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";
|
import { Identifiable } from "@/data";
|
||||||
|
import store from "@/state/store";
|
||||||
|
|
||||||
export enum UploadType {
|
export enum UploadType {
|
||||||
Ipp = "IppScan",
|
Ipp = "IppScan",
|
||||||
@ -17,6 +18,7 @@ export interface DocumentFileInfo extends Identifiable {
|
|||||||
|
|
||||||
const CREATE_DOCUMENT_ENDPOINT = '/document';
|
const CREATE_DOCUMENT_ENDPOINT = '/document';
|
||||||
const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan';
|
const DOCUMENT_UPLOAD_ENDPOINT = '/document/:id/scan';
|
||||||
|
const DOCUMENT_DOWNLOAD_ENDPOINT = 'document/:id/scan/download';
|
||||||
|
|
||||||
export async function create(type: UploadType) {
|
export async function create(type: UploadType) {
|
||||||
const response = await axios.post<InternshipDocument>(CREATE_DOCUMENT_ENDPOINT, { type });
|
const response = await axios.post<InternshipDocument>(CREATE_DOCUMENT_ENDPOINT, { type });
|
||||||
@ -35,3 +37,7 @@ export async function fileinfo(document: InternshipDocument): Promise<DocumentFi
|
|||||||
const response = await axios.get<DocumentFileInfo>(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }));
|
const response = await axios.get<DocumentFileInfo>(prepare(DOCUMENT_UPLOAD_ENDPOINT, { id: document.id as string }));
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function link(document: InternshipDocument): string {
|
||||||
|
return axios.defaults.baseURL + prepare(DOCUMENT_DOWNLOAD_ENDPOINT, { id: document.id as string }) + "?auth=" + store.getState().user.token;
|
||||||
|
}
|
||||||
|
16
src/app.tsx
16
src/app.tsx
@ -15,6 +15,7 @@ import { getLocale, Locale } from "@/state/reducer/settings";
|
|||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { Container } from "@material-ui/core";
|
import { Container } from "@material-ui/core";
|
||||||
|
import { useCurrentUser } from "@/hooks";
|
||||||
|
|
||||||
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
|
||||||
const student = useSelector<AppState, Student>(state => state.student as Student);
|
const student = useSelector<AppState, Student>(state => state.student as Student);
|
||||||
@ -62,6 +63,7 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>)
|
|||||||
function App() {
|
function App() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
|
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
|
||||||
|
const user = useCurrentUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(locale);
|
i18n.changeLanguage(locale);
|
||||||
@ -96,13 +98,19 @@ function App() {
|
|||||||
</header>
|
</header>
|
||||||
<main id="content">
|
<main id="content">
|
||||||
{ <Switch>
|
{ <Switch>
|
||||||
{ routes.map(({ name, content, middlewares = [], ...route }) => <Route { ...route } key={ name }>
|
{ routes.map(({ name, content, middlewares = [], ...route }) =>
|
||||||
{ processMiddlewares([ ...middlewares, content ]) }
|
<Route { ...route } key={ name } render={ () => {
|
||||||
</Route>) }
|
const Next = () => processMiddlewares([ ...middlewares, content ])
|
||||||
|
return <Next />
|
||||||
|
} } />
|
||||||
|
) }
|
||||||
</Switch> }
|
</Switch> }
|
||||||
</main>
|
</main>
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<Container>
|
<Container style={{ display: 'flex', alignItems: "center" }}>
|
||||||
|
<ul className="footer__menu">
|
||||||
|
{ user?.isManager && <li><Link to="/management">{ t("management") }</Link></li> }
|
||||||
|
</ul>
|
||||||
<div className="footer__copyright">{ t('copyright', { date: moment() }) }</div>
|
<div className="footer__copyright">{ t('copyright', { date: moment() }) }</div>
|
||||||
</Container>
|
</Container>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import React, { HTMLProps } from "react";
|
import React, { HTMLProps } from "react";
|
||||||
import { useHorizontalSpacing } from "@/styles";
|
import { useHorizontalSpacing } from "@/styles";
|
||||||
|
|
||||||
export const Actions = (props: HTMLProps<HTMLDivElement>) => {
|
export type ActionsProps = {
|
||||||
const classes = useHorizontalSpacing(2);
|
spacing?: number;
|
||||||
|
} & HTMLProps<HTMLDivElement>;
|
||||||
|
|
||||||
return <div className={ classes.root } { ...props } style={{ display: "flex", alignItems: "center" }}/>
|
export const Actions = ({ spacing = 2, ...props }: ActionsProps) => {
|
||||||
|
const classes = useHorizontalSpacing(spacing);
|
||||||
|
|
||||||
|
return <div className={ classes.root } { ...props } style={{ display: "flex", alignItems: "center", ...props.style }}/>
|
||||||
}
|
}
|
||||||
|
@ -9,21 +9,22 @@ type AsyncProps<TValue, TError = any> = {
|
|||||||
children: (value: TValue) => JSX.Element,
|
children: (value: TValue) => JSX.Element,
|
||||||
loading?: () => JSX.Element,
|
loading?: () => JSX.Element,
|
||||||
error?: (error: TError) => JSX.Element,
|
error?: (error: TError) => JSX.Element,
|
||||||
|
keepValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLoading = () => <Loading />;
|
const defaultLoading = () => <Loading />;
|
||||||
const defaultError = (error: any) => <Alert severity="error">{ error.message }</Alert>;
|
const defaultError = (error: any) => <Alert severity="error">{ error.message }</Alert>;
|
||||||
|
|
||||||
export function Async<TValue, TError = any>(
|
export function Async<TValue, TError = any>(
|
||||||
{ async, children: render, loading = defaultLoading, error = defaultError }: AsyncProps<TValue, TError>
|
{ async, children: render, loading = defaultLoading, error = defaultError, keepValue = false }: AsyncProps<TValue, TError>
|
||||||
) {
|
) {
|
||||||
if (async.isLoading || (!async.error && !async.value)) {
|
if (async.value && (!async.isLoading || keepValue)) {
|
||||||
return loading();
|
return render(async.value as TValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof async.error !== "undefined") {
|
if (typeof async.error !== "undefined") {
|
||||||
return error(async.error);
|
return error(async.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(async.value as TValue);
|
return loading();
|
||||||
}
|
}
|
||||||
|
46
src/components/confirm.tsx
Normal file
46
src/components/confirm.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { Button, ButtonProps, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export type ConfirmProps = {
|
||||||
|
children: (action: () => void) => React.ReactNode,
|
||||||
|
title?: string,
|
||||||
|
content?: React.ReactNode,
|
||||||
|
onConfirm?: () => void,
|
||||||
|
onCancel?: () => void,
|
||||||
|
confirm?: (props: Pick<ButtonProps, 'onClick'>) => React.ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Confirm({ children, title, content, confirm, onConfirm, onCancel }: ConfirmProps) {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setOpen(false);
|
||||||
|
onConfirm?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{ children(() => { setOpen(true) }) }
|
||||||
|
{ createPortal(
|
||||||
|
<Dialog open={ open } onClose={ handleCancel }>
|
||||||
|
{ title && <DialogTitle>{ title }</DialogTitle>}
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{ content || t('confirmation') }</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{ confirm ? confirm({ onClick: handleConfirm }) : <Button color="primary" variant="contained" autoFocus onClick={ handleConfirm }>{ t('confirm') }</Button> }
|
||||||
|
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>,
|
||||||
|
document.getElementById("modals") as Element,
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
74
src/components/contact.tsx
Normal file
74
src/components/contact.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { Field, Form, Formik } from "formik";
|
||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, Typography } from "@material-ui/core";
|
||||||
|
import { CKEditorField } from "@/field/ckeditor";
|
||||||
|
import { Actions } from "@/components/actions";
|
||||||
|
import { Cancel, Send } from "mdi-material-ui";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { capitalize } from "@/helpers";
|
||||||
|
|
||||||
|
export type ContactFormValues = {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialContactFormValues: ContactFormValues = {
|
||||||
|
content: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContactDialogProps = {
|
||||||
|
onSend: (values: ContactFormValues) => void;
|
||||||
|
} & DialogProps;
|
||||||
|
|
||||||
|
export function ContactForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
return <div className={ spacing.vertical } style={{ overflow: 'hidden' }}>
|
||||||
|
<Field label={ t("forms.contact.field.content") } name="content" component={ CKEditorField }/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContactDialog({ onSend, ...props }: ContactDialogProps) {
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <Dialog { ...props } maxWidth="lg">
|
||||||
|
<Formik initialValues={ initialContactFormValues } onSubmit={ onSend }>
|
||||||
|
<Form className={ spacing.vertical }>
|
||||||
|
<DialogTitle>{ capitalize(t("forms.contact.title")) }</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<ContactForm />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Actions>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Send /> } type="submit">{ t("send") }</Button>
|
||||||
|
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||||
|
</Actions>
|
||||||
|
</DialogActions>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</Dialog>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContactActionProps = {
|
||||||
|
children: (props: { action: () => void }) => React.ReactNode
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ContactAction({ children }: ContactActionProps) {
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleClose = () => { setOpen(false) };
|
||||||
|
const handleSubmit = (values: ContactFormValues) => {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{ children({ action: () => setOpen(true) }) }
|
||||||
|
{ createPortal(
|
||||||
|
<ContactDialog open={ open } onSend={ handleSubmit } onClose={ handleClose }/>,
|
||||||
|
document.getElementById("modals") as HTMLElement
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
@ -84,7 +84,7 @@ export const FileInfo = ({ document, ...props }: FileInfoProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Actions className={ classes.actions }>
|
<Actions className={ classes.actions }>
|
||||||
<Button className={ classes.download } startIcon={ <FileDownloadOutline /> }>{ t("download") }</Button>
|
<Button className={ classes.download } startIcon={ <FileDownloadOutline /> } href={ api.upload.link(document) }>{ t("download") }</Button>
|
||||||
</Actions>
|
</Actions>
|
||||||
</aside>
|
</aside>
|
||||||
</div> }
|
</div> }
|
||||||
|
@ -6,6 +6,8 @@ import { Company, Office } from "@/data/company";
|
|||||||
export interface InternshipType extends Identifiable {
|
export interface InternshipType extends Identifiable {
|
||||||
label: Multilingual<string>,
|
label: Multilingual<string>,
|
||||||
description?: Multilingual<string>,
|
description?: Multilingual<string>,
|
||||||
|
requiresDeanApproval: boolean,
|
||||||
|
requiresInsurance: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternshipProgramEntry extends Identifiable {
|
export interface InternshipProgramEntry extends Identifiable {
|
||||||
|
@ -5,3 +5,5 @@ export interface Page extends Identifiable {
|
|||||||
content: Multilingual<string>;
|
content: Multilingual<string>;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
23
src/field/ckeditor.tsx
Normal file
23
src/field/ckeditor.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { FieldProps } from "formik";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||||
|
// @ts-ignore
|
||||||
|
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||||
|
import { FormControl, FormControlLabel, FormControlProps, FormLabel, TextFieldProps } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type CKEditorFieldProps = FieldProps & FormControlProps & { label?: string };
|
||||||
|
|
||||||
|
export function CKEditorField({ field, form, error, label, ...props }: CKEditorFieldProps) {
|
||||||
|
const handleChange = (_: unknown, editor: any) => {
|
||||||
|
const data = editor.getData();
|
||||||
|
form.setFieldValue(field.name, data);
|
||||||
|
form.setFieldTouched(field.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FormControl { ...props }>
|
||||||
|
<FormLabel style={{ marginBottom: "0.5rem" }}>{ label }</FormLabel>
|
||||||
|
<CKEditor data={ field.value } editor={ ClassicEditor } onChange={ handleChange }/>
|
||||||
|
</FormControl>
|
||||||
|
}
|
@ -112,7 +112,7 @@ const InternshipProgramForm = () => {
|
|||||||
if (ev.target.checked) {
|
if (ev.target.checked) {
|
||||||
setSelectedProgramEntries([ ...selectedProgramEntries, entry ]);
|
setSelectedProgramEntries([ ...selectedProgramEntries, entry ]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur != entry));
|
setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur.id != entry.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +133,7 @@ const InternshipProgramForm = () => {
|
|||||||
onBlur={ handleBlur }
|
onBlur={ handleBlur }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{ values.kind?.requiresDeanApproval && <Grid item xs={ 12 }><Alert severity="warning">{ t("internship.kind-requires-dean-approval") }</Alert></Grid> }
|
||||||
{/*<Grid item md={ 8 }>*/}
|
{/*<Grid item md={ 8 }>*/}
|
||||||
{/* {*/}
|
{/* {*/}
|
||||||
{/* values.kind === InternshipType.Other &&*/}
|
{/* values.kind === InternshipType.Other &&*/}
|
||||||
@ -159,6 +160,7 @@ const InternshipProgramForm = () => {
|
|||||||
|
|
||||||
const InternshipDurationForm = () => {
|
const InternshipDurationForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const edition = useCurrentEdition();
|
||||||
const {
|
const {
|
||||||
values: { startDate, endDate, workingHours },
|
values: { startDate, endDate, workingHours },
|
||||||
errors,
|
errors,
|
||||||
@ -174,6 +176,8 @@ const InternshipDurationForm = () => {
|
|||||||
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
|
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
|
||||||
const weeks = useMemo(() => hours !== null ? Math.floor(hours / workingHours) : null, [ hours ]);
|
const weeks = useMemo(() => hours !== null ? Math.floor(hours / workingHours) : null, [ hours ]);
|
||||||
|
|
||||||
|
const requiresDeanApproval = useMemo(() => edition?.startDate?.isAfter(startDate) || edition?.endDate?.isBefore(endDate), [ startDate, endDate ])
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
setFieldTouched("hours", true);
|
setFieldTouched("hours", true);
|
||||||
setFieldValue("hours", hours, true);
|
setFieldValue("hours", hours, true);
|
||||||
@ -200,6 +204,9 @@ const InternshipDurationForm = () => {
|
|||||||
minDate={ startDate }
|
minDate={ startDate }
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{ requiresDeanApproval && <Grid item xs={ 12 }>
|
||||||
|
<Alert severity="warning">{ t("internship.duration-requires-dean-approval") }</Alert>
|
||||||
|
</Grid> }
|
||||||
<Grid item md={ 4 }>
|
<Grid item md={ 4 }>
|
||||||
<Field component={ TextFieldFormik }
|
<Field component={ TextFieldFormik }
|
||||||
name="workingHours"
|
name="workingHours"
|
||||||
@ -387,7 +394,7 @@ export const InternshipForm: React.FunctionComponent = () => {
|
|||||||
{ errors.length > 0 && <Alert severity="warning">
|
{ errors.length > 0 && <Alert severity="warning">
|
||||||
<AlertTitle>{ t('internship.validation.has-errors') }</AlertTitle>
|
<AlertTitle>{ t('internship.validation.has-errors') }</AlertTitle>
|
||||||
<ul style={{ paddingLeft: 0 }}>
|
<ul style={{ paddingLeft: 0 }}>
|
||||||
{ errors.map(message => <li key={ message.key }>{ t(`internship.validation.${message.key}`, message.parameters) }</li>) }
|
{ errors.map(message => <li key={ message.key }>{ t(`validation.api.${message.key}`, message.parameters) }</li>) }
|
||||||
</ul>
|
</ul>
|
||||||
</Alert> }
|
</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>
|
||||||
|
@ -32,9 +32,10 @@ export const PlanForm = () => {
|
|||||||
|
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
destination = await api.upload.create(UploadType.Ipp);
|
destination = await api.upload.create(UploadType.Ipp);
|
||||||
dispatch({ type: InternshipPlanActions.Send, document: destination });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch({ type: InternshipPlanActions.Send, document: destination });
|
||||||
|
|
||||||
await api.upload.upload(destination, file);
|
await api.upload.upload(destination, file);
|
||||||
|
|
||||||
history.push("/");
|
history.push("/");
|
||||||
|
@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useFormikContext } from "formik";
|
import { useFormikContext } from "formik";
|
||||||
import { InternshipFormValues } from "@/forms/internship";
|
import { InternshipFormValues } from "@/forms/internship";
|
||||||
import { useCurrentEdition } from "@/hooks";
|
import { useCurrentEdition } from "@/hooks";
|
||||||
|
import { ContactAction } from "@/components/contact";
|
||||||
|
|
||||||
export const StudentForm = () => {
|
export const StudentForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -36,8 +37,10 @@ export const StudentForm = () => {
|
|||||||
<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 xs={12}>
|
<Grid item xs={12}>
|
||||||
<Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }>
|
<Alert severity="warning" action={ <ContactAction>{
|
||||||
Powyższe dane nie są poprawne?
|
({ action }) => <Button color="inherit" size="small" onClick={ action }>{ t("contact") }</Button>
|
||||||
|
}</ContactAction> }>
|
||||||
|
{ t("incorrect-data-question") }
|
||||||
</Alert>
|
</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -2,6 +2,7 @@ export type Nullable<T> = { [P in keyof T]: T[P] | null }
|
|||||||
|
|
||||||
export type Subset<T> = { [K in keyof T]?: Subset<T[K]> }
|
export type Subset<T> = { [K in keyof T]?: Subset<T[K]> }
|
||||||
export type Dictionary<T> = { [key: string]: T };
|
export type Dictionary<T> = { [key: string]: T };
|
||||||
|
export type OneOrMany<T> = T | T[];
|
||||||
|
|
||||||
export type Index = string | symbol | number;
|
export type Index = string | symbol | number;
|
||||||
|
|
||||||
@ -26,3 +27,23 @@ export function throttle<TArgs extends any[]>(decorated: (...args: TArgs) => voi
|
|||||||
}, time);
|
}, time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function encapsulate<T>(value: OneOrMany<T>): T[] {
|
||||||
|
if (value instanceof Array) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ value ];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function one<T>(value: OneOrMany<T>): T {
|
||||||
|
if (value instanceof Array) {
|
||||||
|
return value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalize(value: string): string {
|
||||||
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { AppState } from "@/state/reducer";
|
|||||||
import { Edition, getEditionDeadlines } from "@/data/edition";
|
import { Edition, getEditionDeadlines } from "@/data/edition";
|
||||||
import { editionSerializationTransformer } from "@/serialization";
|
import { editionSerializationTransformer } from "@/serialization";
|
||||||
import { Student } from "@/data";
|
import { Student } from "@/data";
|
||||||
|
import { UserState } from "@/state/reducer/user";
|
||||||
|
|
||||||
export const useCurrentStudent = () => useSelector<AppState, Student | null>(
|
export const useCurrentStudent = () => useSelector<AppState, Student | null>(
|
||||||
state => state.student
|
state => state.student
|
||||||
@ -16,3 +17,7 @@ export const useDeadlines = () => {
|
|||||||
const edition = useCurrentEdition() as Edition;
|
const edition = useCurrentEdition() as Edition;
|
||||||
return getEditionDeadlines(edition);
|
return getEditionDeadlines(edition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useCurrentUser = () => useSelector<AppState, UserState>(
|
||||||
|
state => state.user
|
||||||
|
)
|
||||||
|
@ -19,7 +19,6 @@ export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setValue(undefined);
|
|
||||||
|
|
||||||
const myMagicNumber = semaphore.value + 1;
|
const myMagicNumber = semaphore.value + 1;
|
||||||
semaphore.value = myMagicNumber;
|
semaphore.value = myMagicNumber;
|
||||||
@ -54,9 +53,9 @@ export function useAsync<T, TError = any>(supplier: Promise<T> | (() => Promise<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAsyncState<T, TError = any>(initial: Promise<T> | undefined): AsyncState<T, TError> {
|
export function useAsyncState<T, TError = any>(initial?: Promise<T> | undefined): AsyncState<T, TError> {
|
||||||
const [promise, setPromise] = useState<Promise<T> | undefined>(initial);
|
const [promise, setPromise] = useState<Promise<T> | undefined>(initial);
|
||||||
const asyncState = useAsync(promise);
|
const asyncState = useAsync<T, TError>(promise);
|
||||||
|
|
||||||
return [ asyncState, setPromise ];
|
return [ asyncState, setPromise ];
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,11 @@ import { convertToRoman } from "@/utils/numbers";
|
|||||||
const resources = {
|
const resources = {
|
||||||
en: {
|
en: {
|
||||||
translation: require('../translations/en.yaml'),
|
translation: require('../translations/en.yaml'),
|
||||||
|
management: require('../translations/management.en.yaml'),
|
||||||
},
|
},
|
||||||
pl: {
|
pl: {
|
||||||
translation: require('../translations/pl.yaml'),
|
translation: require('../translations/pl.yaml'),
|
||||||
|
management: require('../translations/management.pl.yaml'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
src/management/api/edition.ts
Normal file
17
src/management/api/edition.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { axios } from "@/api";
|
||||||
|
import { EditionDTO, editionDtoTransformer } from "@/api/dto/edition";
|
||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
import { prepare } from "@/routing";
|
||||||
|
|
||||||
|
const MANAGEMENT_EDITION_INDEX_ENDPOINT = '/management/editions';
|
||||||
|
const MANAGEMENT_EDITION_ENDPOINT = '/management/editions/:edition';
|
||||||
|
|
||||||
|
export async function all(): Promise<Edition[]> {
|
||||||
|
const response = await axios.get<EditionDTO[]>(MANAGEMENT_EDITION_INDEX_ENDPOINT);
|
||||||
|
return response.data.map(dto => editionDtoTransformer.transform(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function details(edition: string): Promise<Edition> {
|
||||||
|
const response = await axios.get<EditionDTO>(prepare(MANAGEMENT_EDITION_ENDPOINT, { edition }));
|
||||||
|
return editionDtoTransformer.transform(response.data);
|
||||||
|
}
|
11
src/management/api/index.ts
Normal file
11
src/management/api/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as edition from "./edition"
|
||||||
|
import * as page from "./page"
|
||||||
|
import * as type from "./type"
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
edition,
|
||||||
|
page,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api;
|
30
src/management/api/page.ts
Normal file
30
src/management/api/page.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Page } from "@/data/page";
|
||||||
|
import pageDtoTransformer, { PageDTO } from "@/api/dto/page";
|
||||||
|
import { axios } from "@/api";
|
||||||
|
import { STATIC_PAGE_ENDPOINT } from "@/api/page";
|
||||||
|
import { prepare } from "@/routing";
|
||||||
|
import { encapsulate, OneOrMany } from "@/helpers";
|
||||||
|
|
||||||
|
const STATIC_PAGE_INDEX_ENDPOINT = "/staticPage";
|
||||||
|
|
||||||
|
export { get, STATIC_PAGE_ENDPOINT } from "@/api/page"
|
||||||
|
|
||||||
|
export async function all(): Promise<Page[]> {
|
||||||
|
const response = await axios.get<PageDTO[]>(STATIC_PAGE_INDEX_ENDPOINT);
|
||||||
|
return response.data.map(dto => pageDtoTransformer.transform(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(page: OneOrMany<Pick<Page, "slug">>): Promise<void> {
|
||||||
|
const pages = encapsulate(page);
|
||||||
|
|
||||||
|
await Promise.all(pages.map(page => axios.delete(prepare(STATIC_PAGE_ENDPOINT, { slug: page.slug }))));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(page: Page): Promise<Page> {
|
||||||
|
const response = await axios.put<PageDTO>(
|
||||||
|
STATIC_PAGE_INDEX_ENDPOINT,
|
||||||
|
pageDtoTransformer.reverseTransform(page),
|
||||||
|
);
|
||||||
|
|
||||||
|
return pageDtoTransformer.transform(response.data);
|
||||||
|
}
|
28
src/management/api/type.ts
Normal file
28
src/management/api/type.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { InternshipType } from "@/data";
|
||||||
|
import { axios } from "@/api";
|
||||||
|
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||||
|
import { encapsulate, OneOrMany } from "@/helpers";
|
||||||
|
import { prepare } from "@/routing";
|
||||||
|
|
||||||
|
const INTERNSHIP_TYPE_INDEX_ENDPOINT = '/internshipTypes'
|
||||||
|
const INTERNSHIP_TYPE_ENDPOINT = INTERNSHIP_TYPE_INDEX_ENDPOINT + "/:id";
|
||||||
|
|
||||||
|
export async function all(): Promise<InternshipType[]> {
|
||||||
|
const response = await axios.get<InternshipTypeDTO[]>(INTERNSHIP_TYPE_INDEX_ENDPOINT);
|
||||||
|
return response.data.map(dto => internshipTypeDtoTransformer.transform(dto))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(type: OneOrMany<InternshipType>): Promise<void> {
|
||||||
|
await Promise.all(encapsulate(type).map(
|
||||||
|
type => axios.delete(prepare(INTERNSHIP_TYPE_ENDPOINT, { id: type.id as string }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(type: InternshipType): Promise<InternshipType> {
|
||||||
|
await axios.put<InternshipType>(
|
||||||
|
INTERNSHIP_TYPE_INDEX_ENDPOINT,
|
||||||
|
internshipTypeDtoTransformer.reverseTransform(type)
|
||||||
|
);
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
15
src/management/common/BulkActions.tsx
Normal file
15
src/management/common/BulkActions.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Actions, ActionsProps } from "@/components";
|
||||||
|
import React from "react";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export type BulkActionsProps = ActionsProps;
|
||||||
|
|
||||||
|
export const BulkActions = ({ children, ...props }: BulkActionsProps) => {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
|
||||||
|
return <Actions { ...props }>
|
||||||
|
<Typography variant="subtitle2">{ t("actions.bulk") }: </Typography>
|
||||||
|
{ children }
|
||||||
|
</Actions>;
|
||||||
|
};
|
56
src/management/common/DeleteResourceAction.tsx
Normal file
56
src/management/common/DeleteResourceAction.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { OneOrMany } from "@/helpers";
|
||||||
|
import useTheme from "@material-ui/core/styles/useTheme";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { Confirm } from "@/components/confirm";
|
||||||
|
import { Button, IconButton, Tooltip } from "@material-ui/core";
|
||||||
|
import { Delete } from "mdi-material-ui";
|
||||||
|
import { createBoundComponent } from "@/management/common/helpers";
|
||||||
|
|
||||||
|
export type DeleteResourceActionProps<T> = {
|
||||||
|
onDelete: (resource: OneOrMany<T>) => void;
|
||||||
|
resource: OneOrMany<T>;
|
||||||
|
label: (resource: T) => string;
|
||||||
|
children?: (action: any) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DeleteResourceAction<T>({ onDelete, resource, children, label }: DeleteResourceActionProps<T>) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
|
||||||
|
const confirmation = <>
|
||||||
|
{ !Array.isArray(resource)
|
||||||
|
? <Trans i18nKey="confirm.delete">
|
||||||
|
Czy na pewno chcesz usunąć <strong>{ label(resource) }</strong>?
|
||||||
|
</Trans>
|
||||||
|
: <>
|
||||||
|
{ t("confirm.bulk-delete") }
|
||||||
|
<ul>
|
||||||
|
{ resource.map(current => <li key={ label(current) }>{ label(current) }</li>) }
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>;
|
||||||
|
|
||||||
|
return <Confirm
|
||||||
|
onConfirm={ () => onDelete(resource) }
|
||||||
|
content={ confirmation }
|
||||||
|
confirm={ props =>
|
||||||
|
<Button variant="contained" startIcon={ <Delete /> }
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.palette.error.dark,
|
||||||
|
color: theme.palette.error.contrastText,
|
||||||
|
}}
|
||||||
|
{ ...props }>
|
||||||
|
{ t("actions.delete") }
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ action => children ? children(action) : <Tooltip title={ t("actions.delete") as string }><IconButton onClick={ action }><Delete /></IconButton></Tooltip> }
|
||||||
|
</Confirm>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDeleteAction<T>(props: Pick<DeleteResourceActionProps<T>, 'label' | 'onDelete'>) {
|
||||||
|
return createBoundComponent(DeleteResourceAction, props);
|
||||||
|
}
|
||||||
|
|
28
src/management/common/LabelWithIcon.tsx
Normal file
28
src/management/common/LabelWithIcon.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export type LabelWithIconProps = {
|
||||||
|
icon: React.ReactNode,
|
||||||
|
children: React.ReactChildren,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LabelWithIcon({ icon, children }: LabelWithIconProps) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return <div className={ classes.root }>
|
||||||
|
<div className={ classes.icon }>{ icon }</div>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
}
|
11
src/management/common/MaterialTableTitle.tsx
Normal file
11
src/management/common/MaterialTableTitle.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AsyncResult } from "@/hooks";
|
||||||
|
import { CircularProgress } from "@material-ui/core";
|
||||||
|
|
||||||
|
export type MaterialTableTitleProps = { result: AsyncResult<any>, label: React.ReactNode } & React.HTMLProps<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const MaterialTableTitle = ({ label, result, style, ...props }: MaterialTableTitleProps) =>
|
||||||
|
<div style={ { display: "flex", alignItems: "center", ...style } } { ...props }>
|
||||||
|
{ label }
|
||||||
|
{ result.isLoading && <CircularProgress size="1.5rem" style={ { marginLeft: "1rem" } }/> }
|
||||||
|
</div>
|
15
src/management/common/MultilangualCell.tsx
Normal file
15
src/management/common/MultilangualCell.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Multilingual } from "@/data";
|
||||||
|
import React from "react";
|
||||||
|
import { Chip } from "@material-ui/core";
|
||||||
|
|
||||||
|
export type MultilingualCellProps = { value: Multilingual<React.ReactNode> }
|
||||||
|
|
||||||
|
export const MultilingualCell = ({ value }: MultilingualCellProps) => {
|
||||||
|
return <>
|
||||||
|
{ Object.keys(value).map(language => <div>
|
||||||
|
<Chip size="small" label={ language.toUpperCase() } style={ { marginRight: "0.5rem" } }/>
|
||||||
|
{ value[language as keyof Multilingual<any>] }
|
||||||
|
</div>) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
35
src/management/common/helpers.tsx
Normal file
35
src/management/common/helpers.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Column } from "material-table";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { Multilingual } from "@/data";
|
||||||
|
|
||||||
|
export function actionsColumn<T extends Object>(render: (value: T) => React.ReactNode): Column<T> {
|
||||||
|
return {
|
||||||
|
title: <Trans i18nKey="management:actions.label" />,
|
||||||
|
render: value => <Actions style={{ margin: "-1rem" }} spacing={ 0 }>{ render(value) }</Actions>,
|
||||||
|
sorting: false,
|
||||||
|
width: 0,
|
||||||
|
resizable: false,
|
||||||
|
removable: false,
|
||||||
|
searchable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBoundComponent<T, TBoundProps extends keyof T>(Component: React.ComponentType<T>, bound: Pick<T, TBoundProps>) {
|
||||||
|
return (props: Omit<T, TBoundProps>) => <Component { ...bound as any } { ...props } />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Comparator<T> = (a: T, b: T) => number;
|
||||||
|
export type MultilingualComparator<T> = Comparator<Multilingual<T>>;
|
||||||
|
|
||||||
|
export function createMultilingualComparator<T>(comparator: Comparator<T>): MultilingualComparator<T> {
|
||||||
|
return (a, b) => comparator(a.pl, b.pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const multilingualStringComparator = createMultilingualComparator<string>((a, b) => a && b ? a.localeCompare(b) : 0)
|
||||||
|
export const multilingualNumberComparator = createMultilingualComparator<number>((a, b) => a - b)
|
||||||
|
|
||||||
|
export function fieldComparator<T, K extends keyof T>(field: K, comparator: Comparator<T[K]>): Comparator<T> {
|
||||||
|
return (a, b) => comparator(a[field], b[field])
|
||||||
|
}
|
81
src/management/edition/list.tsx
Normal file
81
src/management/edition/list.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, { useCallback, useEffect } from "react";
|
||||||
|
import { Page } from "@/pages/base";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAsync, useAsyncState } from "@/hooks";
|
||||||
|
import api from "@/management/api";
|
||||||
|
import { Async } from "@/components/async";
|
||||||
|
import { Container, Typography } from "@material-ui/core";
|
||||||
|
import MaterialTable, { Action, Column } from "material-table";
|
||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
import { Pencil } from "mdi-material-ui";
|
||||||
|
import { Management } from "../main";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { EditStaticPageDialog } from "@/management/page/create";
|
||||||
|
|
||||||
|
export type EditionDetailsProps = {
|
||||||
|
edition: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditionDetails({ edition, ...props }: EditionDetailsProps) {
|
||||||
|
const result = useAsync(useCallback(() => api.edition.details(edition), [ edition ]));
|
||||||
|
|
||||||
|
return <Async async={ result }>{ edition => <pre>{ JSON.stringify(edition, null, 2) }</pre> }</Async>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditionsManagement() {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const editions = useAsync(useCallback(api.edition.all, []));
|
||||||
|
|
||||||
|
const columns: Column<Edition>[] = [
|
||||||
|
{
|
||||||
|
title: t("edition.field.id"),
|
||||||
|
field: "id",
|
||||||
|
cellStyle: { whiteSpace: "nowrap" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("edition.field.start"),
|
||||||
|
render: edition => edition.startDate.format("DD.MM.yyyy"),
|
||||||
|
customSort: (a, b) => b.startDate.unix() - a.startDate.unix(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("edition.field.end"),
|
||||||
|
render: edition => edition.endDate.format("DD.MM.yyyy"),
|
||||||
|
customSort: (a, b) => b.endDate.unix() - a.endDate.unix(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("edition.field.course"),
|
||||||
|
customSort: (a, b) => a.course.name.localeCompare(b.course.name),
|
||||||
|
render: edition => edition.course.name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const actions: Action<Edition>[] = [
|
||||||
|
{
|
||||||
|
icon: () => <Pencil />,
|
||||||
|
onClick: () => {},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return <Page>
|
||||||
|
<Page.Header maxWidth="lg">
|
||||||
|
<Management.Breadcrumbs>
|
||||||
|
<Typography color="textPrimary">{ t("edition.index.title") }</Typography>
|
||||||
|
</Management.Breadcrumbs>
|
||||||
|
<Page.Title>{ t("edition.index.title") }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<Async async={ editions }>
|
||||||
|
{ editions =>
|
||||||
|
<MaterialTable
|
||||||
|
columns={ columns }
|
||||||
|
data={ editions }
|
||||||
|
actions={ actions }
|
||||||
|
detailPanel={ edition => <EditionDetails edition={ edition.id as string } /> }
|
||||||
|
title={ t("edition.index.title") }
|
||||||
|
options={{ search: false, actionsColumnIndex: -1 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Async>
|
||||||
|
</Container>
|
||||||
|
</Page>;
|
||||||
|
}
|
54
src/management/main.tsx
Normal file
54
src/management/main.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { BreadcrumbsProps, Container, Link, List, ListItem, ListItemIcon, ListItemText, Paper } from "@material-ui/core";
|
||||||
|
import { Page } from "@/pages/base";
|
||||||
|
import React from "react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { route } from "@/routing";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline } from "mdi-material-ui";
|
||||||
|
|
||||||
|
export const Management = {
|
||||||
|
Breadcrumbs: ({ children, ...props }: BreadcrumbsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <Page.Breadcrumbs { ...props }>
|
||||||
|
<Link component={ RouterLink } to={ route("management:index") }>{ t("management:title") }</Link>
|
||||||
|
{ children }
|
||||||
|
</Page.Breadcrumbs>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManagementLinkProps = React.PropsWithChildren<{
|
||||||
|
icon: JSX.Element,
|
||||||
|
route: string,
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const ManagementLink = ({ icon, route, children }: ManagementLinkProps) =>
|
||||||
|
<ListItem button component={ RouterLink } to={ route }>
|
||||||
|
<ListItemIcon>{ icon }</ListItemIcon>
|
||||||
|
<ListItemText>{ children }</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
export const ManagementIndex = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <Page>
|
||||||
|
<Page.Header>
|
||||||
|
<Page.Title>{ t("management:title") }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
|
<Container>
|
||||||
|
<Paper elevation={ 2 }>
|
||||||
|
<List>
|
||||||
|
<ManagementLink icon={ <CalendarClock /> } route={ route("management:editions") }>
|
||||||
|
{ t("management:edition.index.title") }
|
||||||
|
</ManagementLink>
|
||||||
|
<ManagementLink icon={ <FileCertificateOutline /> } route={ route("management:types") }>
|
||||||
|
{ t("management:type.index.title") }
|
||||||
|
</ManagementLink>
|
||||||
|
<ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }>
|
||||||
|
{ t("management:page.index.title") }
|
||||||
|
</ManagementLink>
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
}
|
14
src/management/middleware.tsx
Normal file
14
src/management/middleware.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { isLoggedInMiddleware } from "@/middleware";
|
||||||
|
import { useCurrentUser } from "@/hooks";
|
||||||
|
import React from "react";
|
||||||
|
import { Middleware } from "@/routing";
|
||||||
|
|
||||||
|
export const isManagerMiddleware: Middleware<any, any> = Next => isLoggedInMiddleware(() => {
|
||||||
|
const user = useCurrentUser();
|
||||||
|
|
||||||
|
if (user.isManager) {
|
||||||
|
return <Next />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
})
|
46
src/management/page/edit.tsx
Normal file
46
src/management/page/edit.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
import { Form, Formik } from "formik";
|
||||||
|
import { initialStaticPageFormValues, StaticPageForm, StaticPageFormValues, staticPageFormValuesTransformer } from "@/management/page/form";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
import { Save } from "@material-ui/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Cancel } from "mdi-material-ui";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { default as StaticPage } from "@/data/page";
|
||||||
|
|
||||||
|
export type EditStaticPageDialogProps = {
|
||||||
|
onSave?: (page: StaticPage) => void;
|
||||||
|
page?: StaticPage;
|
||||||
|
} & DialogProps;
|
||||||
|
|
||||||
|
export function EditStaticPageDialog({ onSave, page, ...props }: EditStaticPageDialogProps) {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const spacing = useSpacing(3);
|
||||||
|
|
||||||
|
const handleSubmit = (values: StaticPageFormValues) => {
|
||||||
|
onSave?.(staticPageFormValuesTransformer.reverseTransform(values));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = page
|
||||||
|
? staticPageFormValuesTransformer.transform(page)
|
||||||
|
: initialStaticPageFormValues;
|
||||||
|
|
||||||
|
return <Dialog { ...props } maxWidth="lg">
|
||||||
|
<Formik initialValues={ initialValues } onSubmit={ handleSubmit }>
|
||||||
|
<Form className={ spacing.vertical }>
|
||||||
|
<DialogTitle>{ t(page ? "page.edit.title" : "page.create.title") }</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<StaticPageForm />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Actions>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Save /> } type="submit">{ t("save") }</Button>
|
||||||
|
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||||
|
</Actions>
|
||||||
|
</DialogActions>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</Dialog>
|
||||||
|
}
|
||||||
|
|
40
src/management/page/form.tsx
Normal file
40
src/management/page/form.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { default as StaticPage } from "@/data/page";
|
||||||
|
import { identityTransformer, Transformer } from "@/serialization";
|
||||||
|
import { Field, Form, FormikFormProps } from "formik";
|
||||||
|
import React from "react";
|
||||||
|
import { TextField as TextFieldFormik } from "formik-material-ui";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { CKEditorField } from "@/field/ckeditor";
|
||||||
|
|
||||||
|
export type StaticPageFormValues = StaticPage;
|
||||||
|
|
||||||
|
export const initialStaticPageFormValues: StaticPageFormValues = {
|
||||||
|
slug: "",
|
||||||
|
title: {
|
||||||
|
en: "",
|
||||||
|
pl: "",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
en: "",
|
||||||
|
pl: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const staticPageFormValuesTransformer: Transformer<StaticPage, StaticPageFormValues> = identityTransformer;
|
||||||
|
|
||||||
|
export function StaticPageForm() {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
return <div className={ spacing.vertical }>
|
||||||
|
<Field label={ t("page.field.slug") } name="slug" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Typography variant="subtitle2">{ t("page.field.title") }</Typography>
|
||||||
|
<Field label={ t("translation:language.pl") } name="title.pl" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Field label={ t("translation:language.en") } name="title.en" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Typography variant="subtitle2">{ t("page.field.content") }</Typography>
|
||||||
|
<Field label={ t("translation:language.pl") } name="content.pl" fullWidth component={ CKEditorField }/>
|
||||||
|
<Field label={ t("translation:language.en") } name="content.en" fullWidth component={ CKEditorField }/>
|
||||||
|
</div>
|
||||||
|
}
|
157
src/management/page/list.tsx
Normal file
157
src/management/page/list.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { Page } from "@/pages/base";
|
||||||
|
import { Management } from "@/management/main";
|
||||||
|
import { Box, Button, CircularProgress, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { useAsyncState } from "@/hooks";
|
||||||
|
import api from "@/management/api";
|
||||||
|
import { Async } from "@/components/async";
|
||||||
|
import MaterialTable, { Action, Column } from "material-table";
|
||||||
|
import { default as StaticPage } from "@/data/page";
|
||||||
|
import { Delete, FileFind, Pencil, Refresh } from "mdi-material-ui";
|
||||||
|
import { encapsulate, one, OneOrMany } from "@/helpers";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { Add, Edit } from "@material-ui/icons";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { EditStaticPageDialog } from "@/management/page/edit";
|
||||||
|
import { Confirm } from "@/components/confirm";
|
||||||
|
import useTheme from "@material-ui/core/styles/useTheme";
|
||||||
|
import { BulkActions } from "@/management/common/BulkActions";
|
||||||
|
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||||
|
import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers";
|
||||||
|
import { createDeleteAction } from "@/management/common/DeleteResourceAction";
|
||||||
|
import { MultilingualCell } from "@/management/common/MultilangualCell";
|
||||||
|
|
||||||
|
const label = (page: StaticPage) => page.title.pl;
|
||||||
|
|
||||||
|
export const StaticPageManagement = () => {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const [ result, setPagesPromise ] = useAsyncState<StaticPage[]>();
|
||||||
|
const [ selected, setSelected ] = useState<StaticPage[]>([]);
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
const updatePageList = () => {
|
||||||
|
setPagesPromise(api.page.all());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(updatePageList, []);
|
||||||
|
|
||||||
|
const EditStaticPageAction = ({ page }: { page: StaticPage }) => {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handlePageCreation = async (page: StaticPage) => {
|
||||||
|
await api.page.save(page);
|
||||||
|
setOpen(false);
|
||||||
|
updatePageList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Tooltip title={ t("actions.edit") as any }>
|
||||||
|
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{ open && createPortal(
|
||||||
|
<EditStaticPageDialog open={ open } onSave={ handlePageCreation } page={ page } onClose={ () => setOpen(false) }/>,
|
||||||
|
document.getElementById("modals") as Element
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateStaticPageAction = () => {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handlePageCreation = async (page: StaticPage) => {
|
||||||
|
await api.page.save(page);
|
||||||
|
setOpen(false);
|
||||||
|
updatePageList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||||
|
{ createPortal(
|
||||||
|
<EditStaticPageDialog open={ open } onSave={ handlePageCreation } onClose={ () => setOpen(false) }/>,
|
||||||
|
document.getElementById("modals") as Element
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageDeletion = async (page: OneOrMany<StaticPage>) => {
|
||||||
|
await api.page.remove(page);
|
||||||
|
updatePageList();
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteStaticPageAction = createDeleteAction<StaticPage>({ label, onDelete: handlePageDeletion })
|
||||||
|
|
||||||
|
const PreviewStaticPageAction = ({ page }: { page: StaticPage }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const handlePagePreview = async () => history.push(`/${page.slug}`);
|
||||||
|
|
||||||
|
return <Tooltip title={ t("actions.preview") as string }>
|
||||||
|
<IconButton onClick={ handlePagePreview }><FileFind /></IconButton>
|
||||||
|
</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Column<StaticPage>[] = [
|
||||||
|
{
|
||||||
|
render: page => <MultilingualCell value={ page.title }/>,
|
||||||
|
title: t("page.field.title"),
|
||||||
|
customSort: fieldComparator("title", multilingualStringComparator),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "slug",
|
||||||
|
title: t("page.field.slug"),
|
||||||
|
},
|
||||||
|
actionsColumn(page => <>
|
||||||
|
<EditStaticPageAction page={ page } />
|
||||||
|
<DeleteStaticPageAction resource={ page } />
|
||||||
|
<PreviewStaticPageAction page={ page } />
|
||||||
|
</>)
|
||||||
|
];
|
||||||
|
|
||||||
|
const PagePreview = ({ page }: { page: StaticPage }) =>
|
||||||
|
<Box className={ spacing.vertical } p={ 3 }>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2">Polski</Typography>
|
||||||
|
<Typography variant="h2">{ page.title.pl }</Typography>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: page.content.pl }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="subtitle2">English</Typography>
|
||||||
|
<Typography variant="h2">{ page.title.en }</Typography>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: page.content.en }} />
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
return <Page>
|
||||||
|
<Page.Header maxWidth="lg">
|
||||||
|
<Management.Breadcrumbs>
|
||||||
|
<Typography color="textPrimary">{ t("page.index.title") }</Typography>
|
||||||
|
</Management.Breadcrumbs>
|
||||||
|
<Page.Title>{ t("page.index.title") }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
|
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||||
|
<Actions>
|
||||||
|
<CreateStaticPageAction />
|
||||||
|
<Button onClick={ updatePageList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||||
|
</Actions>
|
||||||
|
{ selected.length > 0 && <BulkActions>
|
||||||
|
<DeleteStaticPageAction resource={ selected }>
|
||||||
|
{ action => <Button startIcon={ <Delete /> } onClick={ action }>{ t("actions.delete") }</Button> }
|
||||||
|
</DeleteStaticPageAction>
|
||||||
|
</BulkActions> }
|
||||||
|
<Async async={ result } keepValue>{
|
||||||
|
pages => <MaterialTable
|
||||||
|
title={ <MaterialTableTitle result={ result } label={ t("page.index.title") }/> }
|
||||||
|
columns={ columns }
|
||||||
|
data={ pages }
|
||||||
|
detailPanel={ page => <PagePreview page={ page } /> }
|
||||||
|
onSelectionChange={ pages => setSelected(pages) }
|
||||||
|
options={{ selection: true }}
|
||||||
|
/>
|
||||||
|
}</Async>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StaticPageManagement;
|
22
src/management/routing.tsx
Normal file
22
src/management/routing.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Route } from "@/routing";
|
||||||
|
import { isManagerMiddleware } from "@/management/middleware";
|
||||||
|
import { EditionsManagement } from "@/management/edition/list";
|
||||||
|
import React from "react";
|
||||||
|
import { ManagementIndex } from "@/management/main";
|
||||||
|
import StaticPageManagement from "@/management/page/list";
|
||||||
|
import { InternshipTypeManagement } from "@/management/type/list";
|
||||||
|
|
||||||
|
export const managementRoutes: Route[] = ([
|
||||||
|
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
||||||
|
|
||||||
|
{ name: "editions", path: "/editions", content: EditionsManagement },
|
||||||
|
{ name: "types", path: "/types", content: InternshipTypeManagement },
|
||||||
|
{ name: "static_pages", path: "/static-pages", content: StaticPageManagement }
|
||||||
|
] as Route[]).map(
|
||||||
|
({ name, path, middlewares = [], ...route }): Route => ({
|
||||||
|
name: `management:${ name }`,
|
||||||
|
path: `/management${ path }`,
|
||||||
|
middlewares: [ isManagerMiddleware, ...middlewares ],
|
||||||
|
...route
|
||||||
|
})
|
||||||
|
);
|
47
src/management/type/edit.tsx
Normal file
47
src/management/type/edit.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
import { Form, Formik } from "formik";
|
||||||
|
import { initialStaticPageFormValues, StaticPageForm, StaticPageFormValues, staticPageFormValuesTransformer } from "@/management/page/form";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
import { Save } from "@material-ui/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Cancel } from "mdi-material-ui";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { initialInternshipTypeFormValues, InternshipTypeForm, InternshipTypeFormValues, internshipTypeFormValuesTransformer } from "@/management/type/form";
|
||||||
|
import { InternshipType } from "@/data";
|
||||||
|
|
||||||
|
export type EditInternshipTypeDialogProps = {
|
||||||
|
onSave?: (page: InternshipType) => void;
|
||||||
|
value?: InternshipType;
|
||||||
|
} & DialogProps;
|
||||||
|
|
||||||
|
export function EditInternshipTypeDialog({ onSave, value, ...props }: EditInternshipTypeDialogProps) {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const spacing = useSpacing(3);
|
||||||
|
|
||||||
|
const handleSubmit = (values: InternshipTypeFormValues) => {
|
||||||
|
onSave?.(internshipTypeFormValuesTransformer.reverseTransform(values));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = value
|
||||||
|
? internshipTypeFormValuesTransformer.transform(value)
|
||||||
|
: initialInternshipTypeFormValues;
|
||||||
|
|
||||||
|
return <Dialog { ...props } maxWidth="lg">
|
||||||
|
<Formik initialValues={ initialValues } onSubmit={ handleSubmit }>
|
||||||
|
<Form className={ spacing.vertical }>
|
||||||
|
<DialogTitle>{ t(value ? "type.edit.title" : "type.create.title") }</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<InternshipTypeForm />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Actions>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Save /> } type="submit">{ t("save") }</Button>
|
||||||
|
<Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button>
|
||||||
|
</Actions>
|
||||||
|
</DialogActions>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</Dialog>
|
||||||
|
}
|
||||||
|
|
55
src/management/type/form.tsx
Normal file
55
src/management/type/form.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { InternshipType } from "@/data";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { Field } from "formik";
|
||||||
|
import { TextField as TextFieldFormik, Checkbox as CheckboxFormik } from "formik-material-ui";
|
||||||
|
import { FormControlLabel, FormGroup, Typography } from "@material-ui/core";
|
||||||
|
import { CKEditorField } from "@/field/ckeditor";
|
||||||
|
import { AccountCheck, ShieldCheck } from "mdi-material-ui";
|
||||||
|
import { identityTransformer, Transformer } from "@/serialization";
|
||||||
|
import { LabelWithIcon } from "@/management/common/LabelWithIcon";
|
||||||
|
|
||||||
|
export type InternshipTypeFormValues = Omit<InternshipType, 'id'>;
|
||||||
|
|
||||||
|
export const initialInternshipTypeFormValues: InternshipTypeFormValues = {
|
||||||
|
label: {
|
||||||
|
pl: "",
|
||||||
|
en: "",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
pl: "",
|
||||||
|
en: "",
|
||||||
|
},
|
||||||
|
requiresInsurance: false,
|
||||||
|
requiresDeanApproval: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const internshipTypeFormValuesTransformer: Transformer<InternshipType, InternshipTypeFormValues> = identityTransformer;
|
||||||
|
|
||||||
|
export function InternshipTypeForm() {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
return <div className={ spacing.vertical }>
|
||||||
|
<Typography variant="subtitle2">{ t("type.field.label") }</Typography>
|
||||||
|
<Field label={ t("translation:language.pl") } name="label.pl" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Field label={ t("translation:language.en") } name="label.en" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Typography variant="subtitle2">{ t("type.field.description") }</Typography>
|
||||||
|
<Field label={ t("translation:language.pl") } name="description.pl" fullWidth component={ TextFieldFormik }/>
|
||||||
|
<Field label={ t("translation:language.en") } name="description.en" fullWidth component={ TextFieldFormik }/>
|
||||||
|
|
||||||
|
<Typography variant="subtitle2">{ t("type.field.flags") }</Typography>
|
||||||
|
<FormGroup>
|
||||||
|
<FormControlLabel
|
||||||
|
control={ <Field name="requiresDeanApproval" component={ CheckboxFormik }/> }
|
||||||
|
label={ <LabelWithIcon icon={ <AccountCheck /> }>{ t("type.flag.dean-approval") }</LabelWithIcon> }
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={ <Field name="requiresInsurance" component={ CheckboxFormik }/> }
|
||||||
|
label={ <LabelWithIcon icon={ <ShieldCheck /> }>{ t("type.flag.insurance") }</LabelWithIcon> }
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
149
src/management/type/list.tsx
Normal file
149
src/management/type/list.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { Page } from "@/pages/base";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAsyncState } from "@/hooks";
|
||||||
|
import { InternshipType } from "@/data";
|
||||||
|
import api from "@/management/api";
|
||||||
|
import { Management } from "@/management/main";
|
||||||
|
import { Button, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||||
|
import { Async } from "@/components/async";
|
||||||
|
import MaterialTable, { Column } from "material-table";
|
||||||
|
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||||
|
import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers";
|
||||||
|
import { AccountCheck, Delete, Refresh, ShieldCheck } from "mdi-material-ui";
|
||||||
|
import { OneOrMany } from "@/helpers";
|
||||||
|
import { createDeleteAction } from "@/management/common/DeleteResourceAction";
|
||||||
|
import { BulkActions } from "@/management/common/BulkActions";
|
||||||
|
import { useSpacing } from "@/styles";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
import { MultilingualCell } from "@/management/common/MultilangualCell";
|
||||||
|
import { default as StaticPage } from "@/data/page";
|
||||||
|
import { Add, Edit } from "@material-ui/icons";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { EditStaticPageDialog } from "@/management/page/edit";
|
||||||
|
import { EditInternshipTypeDialog } from "@/management/type/edit";
|
||||||
|
|
||||||
|
const title = "type.index.title";
|
||||||
|
|
||||||
|
const label = (type: InternshipType) => type?.label?.pl;
|
||||||
|
|
||||||
|
export const InternshipTypeManagement = () => {
|
||||||
|
const { t } = useTranslation("management");
|
||||||
|
const [result, setTypesPromise] = useAsyncState<InternshipType[]>();
|
||||||
|
const [selected, setSelected] = useState<InternshipType[]>([]);
|
||||||
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
|
const updateTypeList = () => {
|
||||||
|
setTypesPromise(api.type.all());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTypeDelete = async (type: OneOrMany<InternshipType>) => {
|
||||||
|
await api.type.remove(type);
|
||||||
|
updateTypeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(updateTypeList, []);
|
||||||
|
|
||||||
|
const DeleteTypeAction = createDeleteAction({ label, onDelete: handleTypeDelete });
|
||||||
|
|
||||||
|
const CreateTypeAction = () => {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleTypeCreation = async (value: InternshipType) => {
|
||||||
|
await api.type.save(value);
|
||||||
|
setOpen(false);
|
||||||
|
updateTypeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||||
|
{ open && createPortal(
|
||||||
|
<EditInternshipTypeDialog open={ open } onSave={ handleTypeCreation } onClose={ () => setOpen(false) }/>,
|
||||||
|
document.getElementById("modals") as Element
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditTypeAction = ({ resource }: { resource: InternshipType }) => {
|
||||||
|
const [ open, setOpen ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleTypeCreation = async (value: InternshipType) => {
|
||||||
|
await api.type.save(value);
|
||||||
|
setOpen(false);
|
||||||
|
updateTypeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Tooltip title={ t("actions.edit") as any }>
|
||||||
|
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{ open && createPortal(
|
||||||
|
<EditInternshipTypeDialog open={ open } onSave={ handleTypeCreation } value={ resource } onClose={ () => setOpen(false) }/>,
|
||||||
|
document.getElementById("modals") as Element
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Column<InternshipType>[] = [
|
||||||
|
{
|
||||||
|
field: "id",
|
||||||
|
title: "ID",
|
||||||
|
width: 0,
|
||||||
|
defaultSort: "asc",
|
||||||
|
filtering: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("type.field.label"),
|
||||||
|
render: type => <MultilingualCell value={ type.label }/>,
|
||||||
|
customSort: fieldComparator("label", multilingualStringComparator),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("type.field.description"),
|
||||||
|
render: type => type.description && <MultilingualCell value={ type.description }/>,
|
||||||
|
sorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("type.field.flags"),
|
||||||
|
render: type => <div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
{ type.requiresDeanApproval && <Tooltip title={ t("type.flag.dean-approval") as string }><AccountCheck/></Tooltip> }
|
||||||
|
{ type.requiresInsurance && <Tooltip title={ t("type.flag.insurance") as string }><ShieldCheck/></Tooltip> }
|
||||||
|
</div>,
|
||||||
|
width: 0,
|
||||||
|
filtering: true,
|
||||||
|
sorting: false,
|
||||||
|
},
|
||||||
|
actionsColumn(type => <>
|
||||||
|
<DeleteTypeAction resource={ type }/>
|
||||||
|
<EditTypeAction resource={ type }/>
|
||||||
|
</>)
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Page>
|
||||||
|
<Page.Header maxWidth="lg">
|
||||||
|
<Management.Breadcrumbs>
|
||||||
|
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||||
|
</Management.Breadcrumbs>
|
||||||
|
<Page.Title>{ t(title) }</Page.Title>
|
||||||
|
</Page.Header>
|
||||||
|
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||||
|
<Actions>
|
||||||
|
<CreateTypeAction />
|
||||||
|
<Button onClick={ updateTypeList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||||
|
</Actions>
|
||||||
|
{ selected.length > 0 && <BulkActions>
|
||||||
|
<DeleteTypeAction resource={ selected }>
|
||||||
|
{ action => <Button startIcon={ <Delete /> } onClick={ action }>{ t("actions.delete") }</Button> }
|
||||||
|
</DeleteTypeAction>
|
||||||
|
</BulkActions> }
|
||||||
|
<Async async={ result } keepValue>{
|
||||||
|
pages => <MaterialTable
|
||||||
|
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||||
|
columns={ columns }
|
||||||
|
data={ pages }
|
||||||
|
onSelectionChange={ pages => setSelected(pages) }
|
||||||
|
options={ { selection: true, pageSize: 10 } }
|
||||||
|
/>
|
||||||
|
}</Async>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { Middleware, route } from "@/routing";
|
import { Middleware, route } from "@/routing";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { AppState, isReady } from "@/state/reducer";
|
import { AppState, isReady } from "@/state/reducer";
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect, useRouteMatch } from "react-router-dom";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { UserState } from "@/state/reducer/user";
|
import { UserState } from "@/state/reducer/user";
|
||||||
|
|
||||||
export const isReadyMiddleware: Middleware<any, any> = Next => isLoggedInMiddleware(() => {
|
export const isReadyMiddleware: Middleware<any, any> = Next => isLoggedInMiddleware(() => {
|
||||||
@ -22,5 +22,7 @@ export const isLoggedInMiddleware: Middleware<any, any> = Next => {
|
|||||||
return <Next />;
|
return <Next />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.sessionStorage.setItem('back-path', window.location.pathname);
|
||||||
|
|
||||||
return <Redirect to={ route("user_login") } />;
|
return <Redirect to={ route("user_login") } />;
|
||||||
}
|
}
|
||||||
|
@ -12,18 +12,19 @@ import { Alert } from "@material-ui/lab";
|
|||||||
import { Subset } from "@/helpers";
|
import { Subset } from "@/helpers";
|
||||||
import { useDispatch } from "@/state/actions";
|
import { useDispatch } from "@/state/actions";
|
||||||
import { loginToEdition } from "@/pages/edition/pick";
|
import { loginToEdition } from "@/pages/edition/pick";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||||
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
|
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
|
||||||
|
|
||||||
export const RegisterEditionPage = () => {
|
export const RegisterEditionPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [key, setKey] = useState<string>("");
|
|
||||||
const [{ value: edition, isLoading }, setEdition] = useAsyncState<Subset<Edition> | null>(undefined);
|
|
||||||
|
|
||||||
const classes = useVerticalSpacing(3);
|
const classes = useVerticalSpacing(3);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const match = useRouteMatch<any>();
|
||||||
|
|
||||||
|
const [key, setKey] = useState<string>(match.params['edition'] || "");
|
||||||
|
const [{ value: edition, isLoading }, setEdition] = useAsyncState<Subset<Edition> | null>(undefined);
|
||||||
|
|
||||||
useDebouncedEffect(() => {
|
useDebouncedEffect(() => {
|
||||||
setEdition(api.edition.get(key));
|
setEdition(api.edition.get(key));
|
||||||
|
@ -4,6 +4,7 @@ import { createStyles, makeStyles } from "@material-ui/core/styles";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { CommentQuestion } from "mdi-material-ui/index";
|
import { CommentQuestion } from "mdi-material-ui/index";
|
||||||
|
import { ContactAction } from "@/components/contact";
|
||||||
|
|
||||||
export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
|
export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -46,8 +47,12 @@ export const Status = ({ submission } : SubmissionStatusProps) => {
|
|||||||
return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>;
|
return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactAction = (props: ButtonProps) => {
|
export const ContactButton = (props: ButtonProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
|
return <ContactAction>{
|
||||||
|
({ action }) => <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props } onClick={ action}>
|
||||||
|
{ t('contact') }
|
||||||
|
</Button> }
|
||||||
|
</ContactAction>
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { InsuranceState } from "@/state/reducer/insurance";
|
|||||||
import { Actions, Step } from "@/components";
|
import { Actions, Step } from "@/components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ContactAction } from "@/pages/steps/common";
|
import { ContactButton } from "@/pages/steps/common";
|
||||||
import { useDeadlines } from "@/hooks";
|
import { useDeadlines } from "@/hooks";
|
||||||
import { StepProps } from "@material-ui/core";
|
import { StepProps } from "@material-ui/core";
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export const InsuranceStep = (props: StepProps) => {
|
|||||||
return <Step { ...props } label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }>
|
return <Step { ...props } label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }>
|
||||||
<p>{ t(`steps.insurance.instructions`) }</p>
|
<p>{ t(`steps.insurance.instructions`) }</p>
|
||||||
<Actions>
|
<Actions>
|
||||||
<ContactAction />
|
<ContactButton />
|
||||||
</Actions>
|
</Actions>
|
||||||
</Step>
|
</Step>
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ 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 { ContactButton, 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 { InternshipDocument } from "@/api/dto/internship-registration";
|
||||||
@ -56,9 +56,9 @@ const PlanActions = () => {
|
|||||||
</Actions>
|
</Actions>
|
||||||
case "declined":
|
case "declined":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<FormAction>{ t('fix-errors') }</FormAction>
|
<FormAction>{ t('send-again') }</FormAction>
|
||||||
<TemplateAction />
|
<TemplateAction />
|
||||||
<ContactAction/>
|
<ContactButton/>
|
||||||
</Actions>
|
</Actions>
|
||||||
case "draft":
|
case "draft":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
|
@ -10,7 +10,7 @@ import { Actions, Step } from "@/components";
|
|||||||
import { route } from "@/routing";
|
import { route } from "@/routing";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index";
|
import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index";
|
||||||
import { ContactAction, Status } from "@/pages/steps/common";
|
import { ContactButton, Status } from "@/pages/steps/common";
|
||||||
import { useDeadlines } from "@/hooks";
|
import { useDeadlines } from "@/hooks";
|
||||||
|
|
||||||
const ProposalActions = () => {
|
const ProposalActions = () => {
|
||||||
@ -43,7 +43,7 @@ const ProposalActions = () => {
|
|||||||
case "declined":
|
case "declined":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<FormAction>{ t('fix-errors') }</FormAction>
|
<FormAction>{ t('fix-errors') }</FormAction>
|
||||||
<ContactAction />
|
<ContactButton />
|
||||||
</Actions>
|
</Actions>
|
||||||
case "draft":
|
case "draft":
|
||||||
return <Actions>
|
return <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, CircularProgress, Container, Typography } from "@material-ui/core";
|
import { Button, CircularProgress, Container, SvgIcon, 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";
|
||||||
@ -12,13 +12,22 @@ import { UserActions } from "@/state/actions/user";
|
|||||||
import { getAuthorizeUrl } from "@/api/user";
|
import { getAuthorizeUrl } from "@/api/user";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Loading } from "@/components/loading";
|
import { Loading } from "@/components/loading";
|
||||||
|
import GUTLogo from "!@svgr/webpack!@/../public/img/pg-logo.svg";
|
||||||
|
|
||||||
const authorizeUser = (code?: string) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
|
||||||
|
type AuthorizeUserOptions = {
|
||||||
|
isStudent?: boolean;
|
||||||
|
isManager?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorizeUser = (code?: string, { isStudent = false, isManager = false }: AuthorizeUserOptions = { isStudent: true }) => async (dispatch: Dispatch<Action>, getState: () => AppState): Promise<void> => {
|
||||||
const token = await api.user.login(code);
|
const token = await api.user.login(code);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UserActions.Login,
|
type: UserActions.Login,
|
||||||
token,
|
token,
|
||||||
|
isStudent,
|
||||||
|
isManager,
|
||||||
})
|
})
|
||||||
|
|
||||||
const student = await api.student.current();
|
const student = await api.student.current();
|
||||||
@ -36,23 +45,32 @@ export const UserLoginPage = () => {
|
|||||||
const query = new URLSearchParams(useLocation().search);
|
const query = new URLSearchParams(useLocation().search);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSampleLogin = async () => {
|
const redirectAfterLogin = () => {
|
||||||
await dispatch(authorizeUser());
|
history.push(window.sessionStorage.getItem('back-path') || "/");
|
||||||
|
}
|
||||||
|
|
||||||
history.push(route("home"));
|
const handleSampleAdminLogin = async () => {
|
||||||
|
await dispatch(authorizeUser(undefined, { isManager: true }));
|
||||||
|
|
||||||
|
history.push(route("management:index"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSampleStudentLogin = async () => {
|
||||||
|
await dispatch(authorizeUser());
|
||||||
|
redirectAfterLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePgLogin = async () => {
|
const handlePgLogin = async () => {
|
||||||
history.push(route("user_login") + "/pg");
|
history.push(route("user_login") + "/pg");
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = useVerticalSpacing(3);
|
const classes = useVerticalSpacing(2);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function() {
|
(async function() {
|
||||||
if (location.pathname === `${match.path}/check/pg`) {
|
if (location.pathname === `${match.path}/check/pg`) {
|
||||||
await dispatch(authorizeUser(query.get("code") as string));
|
await dispatch(authorizeUser(query.get("code") as string));
|
||||||
history.push("/");
|
redirectAfterLogin();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [ match.path ]);
|
}, [ match.path ]);
|
||||||
@ -61,14 +79,21 @@ export const UserLoginPage = () => {
|
|||||||
|
|
||||||
return <Page>
|
return <Page>
|
||||||
<Page.Header maxWidth="md">
|
<Page.Header maxWidth="md">
|
||||||
<Page.Title>Zaloguj się</Page.Title>
|
<Page.Title>{ t("login") }</Page.Title>
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Container>
|
<Container>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={match.path} exact>
|
<Route path={match.path} exact>
|
||||||
<Container maxWidth="md" className={ classes.root }>
|
<Container maxWidth="md" className={ classes.root }>
|
||||||
<Button fullWidth onClick={ handlePgLogin } variant="contained" color="primary">Zaloguj się z pomocą konta PG</Button>
|
<Button fullWidth onClick={ handlePgLogin } variant="contained" color="primary" startIcon={
|
||||||
<Button fullWidth onClick={ handleSampleLogin } variant="contained" color="secondary">Zaloguj jako przykładowy student</Button>
|
// @ts-ignore
|
||||||
|
<GUTLogo style={{ height: "1.25rem" }} viewBox="0 0 144 64" />
|
||||||
|
}>
|
||||||
|
{ t("login-as.gut-account") }
|
||||||
|
</Button>
|
||||||
|
<Typography variant="subtitle2">{ t("login-as.sample") }</Typography>
|
||||||
|
<Button fullWidth onClick={ handleSampleStudentLogin } variant="contained" color="secondary">{ t("login-as.sample-student")}</Button>
|
||||||
|
<Button fullWidth onClick={ handleSampleAdminLogin } variant="contained" color="secondary">{ t("login-as.sample-manager")}</Button>
|
||||||
</Container>
|
</Container>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/pg`} render={ () => {
|
<Route path={`${match.path}/pg`} render={ () => {
|
||||||
|
@ -10,8 +10,9 @@ import PickEditionPage from "@/pages/edition/pick";
|
|||||||
import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
||||||
import UserFillPage from "@/pages/user/fill";
|
import UserFillPage from "@/pages/user/fill";
|
||||||
import UserProfilePage from "@/pages/user/profile";
|
import UserProfilePage from "@/pages/user/profile";
|
||||||
|
import { managementRoutes } from "@/management/routing";
|
||||||
|
|
||||||
type Route = {
|
export type Route = {
|
||||||
name?: string;
|
name?: string;
|
||||||
content: () => ReactComponentElement<any>,
|
content: () => ReactComponentElement<any>,
|
||||||
condition?: () => boolean,
|
condition?: () => boolean,
|
||||||
@ -36,6 +37,7 @@ export const routes: Route[] = [
|
|||||||
|
|
||||||
// edition
|
// edition
|
||||||
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
{ name: "edition_register", path: "/edition/register", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||||
|
{ name: "edition_register_exact", path: "/edition/register/:edition", exact: true, content: () => <RegisterEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||||
{ name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
{ name: "edition_pick", path: "/edition/pick", exact: true, content: () => <PickEditionPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||||
|
|
||||||
// internship
|
// internship
|
||||||
@ -48,8 +50,10 @@ export const routes: Route[] = [
|
|||||||
{ name: "user_fill", path: "/user/data", content: () => <UserFillPage/>, middlewares: [ isLoggedInMiddleware ] },
|
{ name: "user_fill", path: "/user/data", content: () => <UserFillPage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||||
{ name: "user_profile", path: "/user/profile", content: () => <UserProfilePage/>, middlewares: [ isLoggedInMiddleware ] },
|
{ name: "user_profile", path: "/user/profile", content: () => <UserProfilePage/>, middlewares: [ isLoggedInMiddleware ] },
|
||||||
|
|
||||||
|
...managementRoutes,
|
||||||
|
|
||||||
// fallback route for 404 pages
|
// fallback route for 404 pages
|
||||||
{ name: "fallback", path: "*", content: () => <FallbackPage/> }
|
{ name: "fallback", path: "*", content: () => <FallbackPage/> },
|
||||||
]
|
]
|
||||||
|
|
||||||
const routeNameMap = new Map(routes.filter(({ name }) => !!name).map(({ name, path }) => [name, path instanceof Array ? path[0] : path])) as Map<string, string>
|
const routeNameMap = new Map(routes.filter(({ name }) => !!name).map(({ name, path }) => [name, path instanceof Array ? path[0] : path])) as Map<string, string>
|
||||||
|
@ -19,3 +19,8 @@ export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transfo
|
|||||||
export type OneWayTransformer<TFrom, TResult, TContext = never> = {
|
export type OneWayTransformer<TFrom, TResult, TContext = never> = {
|
||||||
transform(subject: TFrom, context?: TContext): TResult;
|
transform(subject: TFrom, context?: TContext): TResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const identityTransformer: Transformer<any, any> = {
|
||||||
|
transform: subject => subject,
|
||||||
|
reverseTransform: subject => subject
|
||||||
|
}
|
||||||
|
@ -7,6 +7,8 @@ export enum UserActions {
|
|||||||
|
|
||||||
export interface LoginAction extends Action<UserActions.Login> {
|
export interface LoginAction extends Action<UserActions.Login> {
|
||||||
token: string;
|
token: string;
|
||||||
|
isStudent: boolean;
|
||||||
|
isManager: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LogoutAction = Action<UserActions.Logout>;
|
export type LogoutAction = Action<UserActions.Logout>;
|
||||||
|
@ -3,10 +3,14 @@ import { UserAction, UserActions } from "@/state/actions/user";
|
|||||||
export type UserState = {
|
export type UserState = {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
isManager: boolean;
|
||||||
|
isStudent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialUserState: UserState = {
|
const initialUserState: UserState = {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
|
isManager: false,
|
||||||
|
isStudent: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const userReducer = (state: UserState = initialUserState, action: UserAction): UserState => {
|
const userReducer = (state: UserState = initialUserState, action: UserAction): UserState => {
|
||||||
@ -16,12 +20,12 @@ const userReducer = (state: UserState = initialUserState, action: UserAction): U
|
|||||||
...state,
|
...state,
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
token: action.token,
|
token: action.token,
|
||||||
|
isManager: action.isManager,
|
||||||
|
isStudent: action.isManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
case UserActions.Logout:
|
case UserActions.Logout:
|
||||||
return {
|
return initialUserState;
|
||||||
loggedIn: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
@ -10,5 +10,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer__copyright {
|
.footer__copyright {
|
||||||
text-align: right;
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__menu {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: .5rem;
|
||||||
|
&::before {
|
||||||
|
content: "•";
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
0
translations/management.en.yaml
Normal file
0
translations/management.en.yaml
Normal file
51
translations/management.pl.yaml
Normal file
51
translations/management.pl.yaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
title: Zarządzanie
|
||||||
|
|
||||||
|
create: utwórz
|
||||||
|
refresh: $t(translation:refresh)
|
||||||
|
save: zapisz
|
||||||
|
cancel: anuluj
|
||||||
|
|
||||||
|
actions:
|
||||||
|
label: Akcje
|
||||||
|
bulk: Akcje masowe
|
||||||
|
preview: Podgląd
|
||||||
|
delete: Usuń
|
||||||
|
edit: Edytuj
|
||||||
|
|
||||||
|
edition:
|
||||||
|
index:
|
||||||
|
title: "Edycje praktyk"
|
||||||
|
field:
|
||||||
|
id: Identyfikator
|
||||||
|
start: Początek
|
||||||
|
end: Koniec
|
||||||
|
course: Kierunek
|
||||||
|
|
||||||
|
type:
|
||||||
|
index:
|
||||||
|
title: "Rodzeje praktyki"
|
||||||
|
edit:
|
||||||
|
title: "Edytuj rodzaj praktyki"
|
||||||
|
create:
|
||||||
|
title: "Utwórz rodzaj praktyki"
|
||||||
|
field:
|
||||||
|
label: "Rodzaj praktyki"
|
||||||
|
description: "Opis"
|
||||||
|
flags: "Wymogi"
|
||||||
|
flag:
|
||||||
|
dean-approval: "Wymaga zgody dziekana"
|
||||||
|
insurance: "Wymaga ubezpieczenia"
|
||||||
|
|
||||||
|
page:
|
||||||
|
index:
|
||||||
|
title: Strony statyczne
|
||||||
|
field:
|
||||||
|
title: Tytuł
|
||||||
|
content: Treść
|
||||||
|
slug: Adres
|
||||||
|
create:
|
||||||
|
title: Utwórz stronę statyczną
|
||||||
|
edit:
|
||||||
|
title: Zmień stronę statyczną
|
||||||
|
confirm:
|
||||||
|
bulk-delete: Czy na pewno chcesz usunąć wszystkie wybrane strony?
|
@ -5,6 +5,11 @@ login: zaloguj się
|
|||||||
login-in-progress: Logowanie w toku, proszę czekać...
|
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>
|
||||||
|
login-as:
|
||||||
|
sample: "Przykładowe konta"
|
||||||
|
gut-account: "Zaloguj z pomocą konta politechnicznego"
|
||||||
|
sample-student: "Zaloguj jako przykładowy student"
|
||||||
|
sample-manager: "Zaloguj jako przykładowy pełnomocnik/administrator"
|
||||||
|
|
||||||
until: do {{ date, DD MMMM YYYY }}
|
until: do {{ date, DD MMMM YYYY }}
|
||||||
not-before: od {{ date, DD MMMM YYYY }}
|
not-before: od {{ date, DD MMMM YYYY }}
|
||||||
@ -21,12 +26,15 @@ contact: skontaktuj się z pełnomocnikiem
|
|||||||
comments: Zgłoszone uwagi
|
comments: Zgłoszone uwagi
|
||||||
send-again: wyślij ponownie
|
send-again: wyślij ponownie
|
||||||
cancel: anuluj
|
cancel: anuluj
|
||||||
|
send: wyślij
|
||||||
|
|
||||||
accept: zaakceptuj
|
accept: zaakceptuj
|
||||||
accept-with-comments: zaakceptuj z uwagami
|
accept-with-comments: zaakceptuj z uwagami
|
||||||
accept-without-comments: zaakceptuj bez uwag
|
accept-without-comments: zaakceptuj bez uwag
|
||||||
discard: zgłoś uwagi
|
discard: zgłoś uwagi
|
||||||
|
|
||||||
|
incorrect-data-question: "Powyższe dane nie są poprawne?"
|
||||||
|
|
||||||
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
|
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
@ -58,6 +66,10 @@ forms:
|
|||||||
sections:
|
sections:
|
||||||
personal: "Dane osobowe"
|
personal: "Dane osobowe"
|
||||||
studies: "Dane kierunkowe"
|
studies: "Dane kierunkowe"
|
||||||
|
contact:
|
||||||
|
title: $t(contact)
|
||||||
|
field:
|
||||||
|
content: "Treść"
|
||||||
internship:
|
internship:
|
||||||
fields:
|
fields:
|
||||||
start-date: Data rozpoczęcia praktyki
|
start-date: Data rozpoczęcia praktyki
|
||||||
@ -123,6 +135,8 @@ internship:
|
|||||||
intern:
|
intern:
|
||||||
semester: semestr {{ semester, roman }}
|
semester: semestr {{ semester, roman }}
|
||||||
album: "numer albumu {{ album }}"
|
album: "numer albumu {{ album }}"
|
||||||
|
kind-requires-dean-approval: "Ten rodzaj praktyki/umowy wymaga akceptacji przez dziekana!"
|
||||||
|
duration-requires-dean-approval: "Taki okres trwania praktyki wymaga akceptacji przez dziekana!"
|
||||||
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
|
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
|
||||||
duration_2: "{{ duration, weeks }} tygodni"
|
duration_2: "{{ duration, weeks }} tygodni"
|
||||||
duration_0: "{{ duration, weeks }} tydzień"
|
duration_0: "{{ duration, weeks }} tydzień"
|
||||||
@ -190,7 +204,7 @@ steps:
|
|||||||
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. Indywidualny Plan Praktyk musi zostać zatwierdzony
|
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.
|
oraz podpisany przez Twojego zakładowego 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.
|
||||||
@ -211,7 +225,16 @@ steps:
|
|||||||
instructions: >
|
instructions: >
|
||||||
Należy zgłosić się do pełnomocnika ds. praktyk Twojego kierunku i podpisać umowę ubezpieczenia. (TODO)
|
Należy zgłosić się do pełnomocnika ds. praktyk Twojego kierunku i podpisać umowę ubezpieczenia. (TODO)
|
||||||
|
|
||||||
|
language:
|
||||||
|
pl: Polski
|
||||||
|
en: Angielski
|
||||||
|
|
||||||
validation:
|
validation:
|
||||||
|
api:
|
||||||
|
GreaterThanOrEqualValidator: Wartość pola "{{ PropertyName }}" musi być większa bądź równa {{ ComparisonValue }}.
|
||||||
|
NotEmptyValidator: Wartosć pola "{{ PropertyName }}" nie może być pusta.
|
||||||
|
NotNullValidator: Wartosć pola "{{ PropertyName }}" nie może być pusta.
|
||||||
|
PredicateValidator: Wartosć pola "{{ PropertyName }}" nie spełnia warunków walidacji.
|
||||||
required: "To pole jest wymagane"
|
required: "To pole jest wymagane"
|
||||||
email: "Wprowadź poprawny adres e-mail"
|
email: "Wprowadź poprawny adres e-mail"
|
||||||
phone: "Wprowadź poprawny numer telefonu"
|
phone: "Wprowadź poprawny numer telefonu"
|
||||||
@ -220,3 +243,5 @@ validation:
|
|||||||
|
|
||||||
contact-coordinator: "Skontaktuj się z koordynatorem"
|
contact-coordinator: "Skontaktuj się z koordynatorem"
|
||||||
download: "pobierz"
|
download: "pobierz"
|
||||||
|
management: "zarządzanie"
|
||||||
|
refresh: "odśwież"
|
||||||
|
@ -59,7 +59,7 @@ const config = {
|
|||||||
port: parseInt(process.env.APP_PORT || "3000"),
|
port: parseInt(process.env.APP_PORT || "3000"),
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "https://system-praktyk.stg.kadet.net/api/",
|
target: "https://system-praktyk.dev.kadet.net/api/",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
"^/api": ''
|
"^/api": ''
|
||||||
|
Loading…
Reference in New Issue
Block a user