Compare commits
No commits in common. "master" and "feature/management" have entirely different histories.
master
...
feature/ma
@ -1,3 +0,0 @@
|
||||
/node_modules/
|
||||
/build/
|
||||
/.build/
|
14
Dockerfile
14
Dockerfile
@ -1,14 +0,0 @@
|
||||
FROM node:15.4.0 as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:mainline-alpine
|
||||
|
||||
COPY --from=build /app/public /var/www
|
||||
COPY --from=build /app/build /var/www
|
||||
COPY ./config/nginx.conf.template /etc/nginx/templates/default.conf.template
|
76
README.md
76
README.md
@ -1,44 +1,44 @@
|
||||
# System Praktyk - Frontend
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
Projekt oparty o [Create React App](https://create-react-app.dev/) z wyciągniętymi najważniejszymi rzeczami w celu
|
||||
minimalizacji zależności.
|
||||
## Available Scripts
|
||||
|
||||
## Skrypty
|
||||
```bash
|
||||
$ yarn server # uruchomienie serwera
|
||||
$ yarn watch # uruchomienie budowania assetów
|
||||
$ yarn build # uruchomienie produkcyjnego builda
|
||||
```
|
||||
In the project directory, you can run:
|
||||
|
||||
## Struktura projektu
|
||||
```
|
||||
/.build - skrypty budujace aplikacje na serwerze
|
||||
/config - konfiguracja
|
||||
/public - pliki publiczne
|
||||
/translations - tłumaczenia
|
||||
/src/api - moduły związane z interfejsowaniem z api
|
||||
/src/data - modele danych
|
||||
/src/forms - formularze i pochodne
|
||||
/src/components - wspólne komponenty
|
||||
/src/hooks - customowe hooki dla reacta
|
||||
/src/pages - podstrony
|
||||
/src/provider - przykładowe dane
|
||||
/src/serialization - serializacja modeli
|
||||
/src/state - zarządzanie stanem
|
||||
/src/management - moduły administracji kursami, struktura analogiczna
|
||||
/src/styles,ui - style
|
||||
/src/utils - pomocne funkcje
|
||||
```
|
||||
### `yarn start`
|
||||
|
||||
## Docker
|
||||
Obraz bazuje na nginxie z linii mainline. Budowanie obrazu:
|
||||
```
|
||||
docker build -f Dockerfile -t system-praktyk-front:latest .
|
||||
```
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
Dostępne są wszystkie zmienne środowiskowe typowe dla nginxa oraz dodatkowo `APP_API_BASE` definiującą bazowy adres pod którym dostępne jest api.
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
Przykład uruchomienia:
|
||||
```
|
||||
docker run -e APP_API_BASE="https://system-praktyk.stg.kadet.net" -p 80:80 system-praktyk-front
|
||||
```
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
@ -1,18 +0,0 @@
|
||||
server {
|
||||
server_name $NGINX_HOST;
|
||||
root /var/www;
|
||||
|
||||
location ~ ^/api/doc$ {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass $APP_API_BASE/api/;
|
||||
proxy_intercept_errors on;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
import { Identifiable, Identifier, InternshipProgramEntry } from "@/data";
|
||||
import { Identifiable, InternshipProgramEntry } from "@/data";
|
||||
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
|
||||
import { OneWayTransformer, Transformer } from "@/serialization";
|
||||
import { Edition } from "@/data/edition";
|
||||
import moment from "moment-timezone";
|
||||
import { Subset } from "@/helpers";
|
||||
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||
import { ReportFieldDefinition, ReportFieldType } from "@/data/report";
|
||||
|
||||
export interface ProgramEntryDTO extends Identifiable {
|
||||
description: string;
|
||||
@ -18,8 +16,6 @@ export interface EditionDTO extends Identifiable {
|
||||
reportingStart: string,
|
||||
course: CourseDTO,
|
||||
availableSubjects: ProgramEntryDTO[],
|
||||
availableInternshipTypes: InternshipTypeDTO[],
|
||||
reportSchema: FieldDefinitionDTO[],
|
||||
}
|
||||
|
||||
export interface EditionTeaserDTO extends Identifiable {
|
||||
@ -28,83 +24,6 @@ export interface EditionTeaserDTO extends Identifiable {
|
||||
courseName: string,
|
||||
}
|
||||
|
||||
export enum FieldDefinitionDTOType {
|
||||
LongText = "LongText",
|
||||
ShortText = "ShortText",
|
||||
Select = "Select",
|
||||
Radial = "Radial",
|
||||
Checkbox = "Checkbox",
|
||||
}
|
||||
|
||||
export const fieldDefinitionDtoTypeTransformer: Transformer<FieldDefinitionDTOType, ReportFieldType> = {
|
||||
transform(dto: FieldDefinitionDTOType, context?: unknown) {
|
||||
switch (dto) {
|
||||
case FieldDefinitionDTOType.LongText:
|
||||
return "long-text"
|
||||
case FieldDefinitionDTOType.ShortText:
|
||||
return "short-text";
|
||||
case FieldDefinitionDTOType.Select:
|
||||
return "select";
|
||||
case FieldDefinitionDTOType.Radial:
|
||||
return "radio";
|
||||
case FieldDefinitionDTOType.Checkbox:
|
||||
return "checkbox";
|
||||
}
|
||||
},
|
||||
reverseTransform(type: ReportFieldType, context?: unknown) {
|
||||
switch (type) {
|
||||
case "short-text":
|
||||
return FieldDefinitionDTOType.ShortText;
|
||||
case "long-text":
|
||||
return FieldDefinitionDTOType.LongText;
|
||||
case "checkbox":
|
||||
return FieldDefinitionDTOType.Checkbox;
|
||||
case "radio":
|
||||
return FieldDefinitionDTOType.Radial;
|
||||
case "select":
|
||||
return FieldDefinitionDTOType.Select;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface FieldDefinitionDTO extends Identifiable {
|
||||
label: string;
|
||||
labelEng: string;
|
||||
description: string;
|
||||
descriptionEng: string;
|
||||
fieldType: FieldDefinitionDTOType;
|
||||
choices: string[];
|
||||
}
|
||||
|
||||
export const fieldDefinitionDtoTransformer: Transformer<FieldDefinitionDTO, ReportFieldDefinition> = {
|
||||
transform(dto: FieldDefinitionDTO, context?: unknown): ReportFieldDefinition {
|
||||
return {
|
||||
id: dto.id,
|
||||
choices: (dto.choices || []).map(choice => JSON.parse(choice)),
|
||||
description: {
|
||||
pl: dto.description,
|
||||
en: dto.descriptionEng,
|
||||
},
|
||||
label: {
|
||||
pl: dto.label,
|
||||
en: dto.labelEng,
|
||||
},
|
||||
type: fieldDefinitionDtoTypeTransformer.transform(dto.fieldType),
|
||||
}
|
||||
},
|
||||
reverseTransform(subject: ReportFieldDefinition, context?: unknown): FieldDefinitionDTO {
|
||||
return {
|
||||
id: subject.id,
|
||||
choices: "choices" in subject && subject.choices.map(choice => JSON.stringify(choice)) || [],
|
||||
description: subject.description.pl,
|
||||
descriptionEng: subject.description.en,
|
||||
fieldType: fieldDefinitionDtoTypeTransformer.reverseTransform(subject.type),
|
||||
label: subject.label.pl,
|
||||
labelEng: subject.label.en,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const editionTeaserDtoTransformer: OneWayTransformer<EditionTeaserDTO, Subset<Edition>> = {
|
||||
transform(subject: EditionTeaserDTO, context?: undefined): Subset<Edition> {
|
||||
return subject && {
|
||||
@ -126,9 +45,7 @@ export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
|
||||
editionStart: subject.startDate.toISOString(),
|
||||
course: courseDtoTransformer.reverseTransform(subject.course),
|
||||
reportingStart: subject.reportingStart.toISOString(),
|
||||
availableSubjects: subject.program.map(entry => programEntryDtoTransformer.reverseTransform(entry)),
|
||||
availableInternshipTypes: subject.types.map(entry => internshipTypeDtoTransformer.reverseTransform(entry)),
|
||||
reportSchema: subject.schema.map(entry => fieldDefinitionDtoTransformer.reverseTransform(entry)),
|
||||
availableSubjects: [],
|
||||
};
|
||||
},
|
||||
transform(subject: EditionDTO, context: undefined): Edition {
|
||||
@ -142,9 +59,6 @@ export const editionDtoTransformer: Transformer<EditionDTO, Edition> = {
|
||||
proposalDeadline: moment(subject.reportingStart),
|
||||
reportingStart: moment(subject.reportingStart),
|
||||
reportingEnd: moment(subject.reportingStart).add(1, 'month'),
|
||||
program: (subject.availableSubjects || []).map(entry => programEntryDtoTransformer.transform(entry)),
|
||||
types: (subject.availableInternshipTypes || []).map(entry => internshipTypeDtoTransformer.transform(entry)),
|
||||
schema: (subject.reportSchema || []).map(entry => fieldDefinitionDtoTransformer.transform(entry)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -164,28 +78,3 @@ export const programEntryDtoTransformer: Transformer<ProgramEntryDTO, Internship
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
interface EditionUpdateDTO extends Identifiable {
|
||||
editionStart: string;
|
||||
editionFinish: string;
|
||||
reportingStart: string;
|
||||
course: CourseDTO;
|
||||
availableSubjectsIds: Identifier[],
|
||||
availableInternshipTypesIds: Identifier[],
|
||||
reportSchema: Identifier[],
|
||||
}
|
||||
|
||||
export const editionUpdateDtoTransformer: OneWayTransformer<Edition, EditionUpdateDTO> = {
|
||||
transform(subject: Edition, context?: undefined): EditionUpdateDTO {
|
||||
return {
|
||||
id: subject.id,
|
||||
editionFinish: subject.endDate.toISOString(),
|
||||
editionStart: subject.startDate.toISOString(),
|
||||
course: courseDtoTransformer.reverseTransform(subject.course),
|
||||
reportingStart: subject.reportingStart.toISOString(),
|
||||
availableSubjectsIds: subject.program.map(x => x.id).filter(x => typeof x !== "undefined") as Identifier[],
|
||||
availableInternshipTypesIds: subject.types.map(x => x.id).filter(x => typeof x !== "undefined") as Identifier[],
|
||||
reportSchema: subject.schema.map(x => x.id).filter(x => typeof x !== "undefined") as Identifier[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Address, Company, Identifiable, Internship, Mentor, Office, Stateful } from "@/data";
|
||||
import { momentSerializationTransformer, OneWayTransformer, Transformer } from "@/serialization";
|
||||
import { Address, Company, Identifiable, Internship, Mentor, Office } from "@/data";
|
||||
import { momentSerializationTransformer, OneWayTransformer } from "@/serialization";
|
||||
import { Nullable } from "@/helpers";
|
||||
import { MentorDTO, mentorDtoTransformer } from "@/api/dto/mentor";
|
||||
import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||
@ -7,14 +7,6 @@ import { Moment } from "moment-timezone";
|
||||
import { sampleStudent } from "@/provider/dummy";
|
||||
import { UploadType } from "@/api/upload";
|
||||
import { ProgramEntryDTO, programEntryDtoTransformer } from "@/api/dto/edition";
|
||||
import { StudentDTO } from "@/api/dto/student";
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { Report } from "@/data/report";
|
||||
|
||||
export interface StatefulDTO {
|
||||
state: SubmissionState;
|
||||
changeStateComment: string;
|
||||
}
|
||||
|
||||
export enum SubmissionState {
|
||||
Draft = "Draft",
|
||||
@ -24,50 +16,6 @@ export enum SubmissionState {
|
||||
Archival = "Archival",
|
||||
}
|
||||
|
||||
export const submissionStateDtoTransformer: Transformer<SubmissionState, SubmissionStatus> = {
|
||||
reverseTransform(subject: SubmissionStatus, context: undefined): SubmissionState {
|
||||
switch (subject) {
|
||||
case "draft":
|
||||
return SubmissionState.Draft;
|
||||
case "awaiting":
|
||||
return SubmissionState.Submitted;
|
||||
case "accepted":
|
||||
return SubmissionState.Accepted;
|
||||
case "declined":
|
||||
return SubmissionState.Rejected;
|
||||
}
|
||||
},
|
||||
transform(subject: SubmissionState, context: undefined): SubmissionStatus {
|
||||
switch (subject) {
|
||||
case SubmissionState.Draft:
|
||||
return "draft";
|
||||
case SubmissionState.Submitted:
|
||||
return "awaiting";
|
||||
case SubmissionState.Accepted:
|
||||
return "accepted";
|
||||
case SubmissionState.Rejected:
|
||||
return "declined";
|
||||
case SubmissionState.Archival:
|
||||
return "declined";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const statefulDtoTransformer: Transformer<StatefulDTO, Stateful> = {
|
||||
reverseTransform(subject: Stateful, context: undefined): StatefulDTO {
|
||||
return {
|
||||
changeStateComment: subject.comment,
|
||||
state: submissionStateDtoTransformer.reverseTransform(subject.state, context),
|
||||
};
|
||||
},
|
||||
transform(subject: StatefulDTO, context: undefined): Stateful {
|
||||
return {
|
||||
comment: subject.changeStateComment,
|
||||
state: submissionStateDtoTransformer.transform(subject.state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface NewBranchOffice extends Address {
|
||||
}
|
||||
|
||||
@ -92,50 +40,29 @@ export interface InternshipRegistrationUpdate {
|
||||
subjects: string[],
|
||||
}
|
||||
|
||||
export interface InternshipRegistrationDTO extends Identifiable, StatefulDTO {
|
||||
export interface InternshipRegistrationDTO extends Identifiable {
|
||||
start: string;
|
||||
end: string;
|
||||
type: InternshipTypeDTO,
|
||||
state: SubmissionState,
|
||||
mentor: MentorDTO,
|
||||
company: Company,
|
||||
branchAddress: Office,
|
||||
declaredHours: number,
|
||||
subjects: { subject: ProgramEntryDTO }[],
|
||||
submissionDate: string,
|
||||
}
|
||||
|
||||
export interface InternshipDocument extends Identifiable, Stateful {
|
||||
export interface InternshipDocument extends Identifiable {
|
||||
description: null,
|
||||
type: UploadType,
|
||||
}
|
||||
|
||||
export interface InternshipDocumentDTO extends Identifiable, StatefulDTO {
|
||||
description: null;
|
||||
type: UploadType;
|
||||
}
|
||||
|
||||
export interface InternshipReportDTO extends StatefulDTO, Identifiable {
|
||||
value: string;
|
||||
state: SubmissionState,
|
||||
}
|
||||
|
||||
const reference = (subject: Identifiable | null): Identifiable | null => subject && { id: subject.id };
|
||||
|
||||
export interface InternshipInfoDTO extends Identifiable {
|
||||
export interface InternshipInfoDTO {
|
||||
internshipRegistration: InternshipRegistrationDTO;
|
||||
documentation: InternshipDocumentDTO[],
|
||||
student: StudentDTO,
|
||||
report: InternshipReportDTO,
|
||||
grade: number,
|
||||
}
|
||||
|
||||
export const internshipReportDtoTransformer: OneWayTransformer<InternshipReportDTO, Report> = {
|
||||
transform(subject: InternshipReportDTO, context?: unknown): Report {
|
||||
return {
|
||||
id: subject.id,
|
||||
fields: JSON.parse(subject.value),
|
||||
...statefulDtoTransformer.transform(subject),
|
||||
}
|
||||
}
|
||||
documentation: InternshipDocument[],
|
||||
}
|
||||
|
||||
export const internshipRegistrationUpdateTransformer: OneWayTransformer<Nullable<Internship>, Nullable<InternshipRegistrationUpdate>> = {
|
||||
@ -179,12 +106,3 @@ export const internshipRegistrationDtoTransformer: OneWayTransformer<InternshipR
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const internshipDocumentDtoTransformer: OneWayTransformer<InternshipDocumentDTO, InternshipDocument> = {
|
||||
transform(dto: InternshipDocumentDTO, context?: unknown): InternshipDocument {
|
||||
return {
|
||||
...dto,
|
||||
...statefulDtoTransformer.transform(dto),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,9 @@ import * as type from "./type";
|
||||
import * as companies from "./companies";
|
||||
import * as internship from "./internship";
|
||||
import * as upload from "./upload";
|
||||
import * as report from "./report";
|
||||
|
||||
export const axios = Axios.create({
|
||||
baseURL: process.env.API_BASE_URL || `${window.location.protocol}//${window.location.hostname}/api/`,
|
||||
baseURL: process.env.API_BASE_URL || `https://${window.location.hostname}/api/`,
|
||||
})
|
||||
|
||||
axios.interceptors.request.use(config => {
|
||||
@ -42,8 +41,7 @@ const api = {
|
||||
type,
|
||||
companies,
|
||||
internship,
|
||||
upload,
|
||||
report,
|
||||
upload
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { Report, ReportFieldValues } from "@/data/report";
|
||||
import { axios } from "@/api/index";
|
||||
|
||||
const REPORT_SAVE_ENDPOINT = "/internship/report"
|
||||
|
||||
export async function save(report: Report) {
|
||||
await axios.post(REPORT_SAVE_ENDPOINT, report.fields);
|
||||
}
|
@ -8,7 +8,6 @@ export enum UploadType {
|
||||
Ipp = "IppScan",
|
||||
DeanConsent = "DeanConsent",
|
||||
Insurance = "NnwInsurance",
|
||||
InternshipEvaluation = "InternshipEvaluation"
|
||||
}
|
||||
|
||||
export interface DocumentFileInfo extends Identifiable {
|
||||
|
11
src/app.tsx
11
src/app.tsx
@ -1,6 +1,6 @@
|
||||
import React, { HTMLProps, useEffect } from 'react';
|
||||
import { Link, Route, Switch } from "react-router-dom"
|
||||
import { processMiddlewares, route, Routes, routes } from "@/routing";
|
||||
import { processMiddlewares, route, routes } from "@/routing";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
@ -97,7 +97,14 @@ function App() {
|
||||
</Container>
|
||||
</header>
|
||||
<main id="content">
|
||||
<Routes routes={ routes.filter(route => !route.tags || route.tags.length == 0) }/>
|
||||
{ <Switch>
|
||||
{ routes.map(({ name, content, middlewares = [], ...route }) =>
|
||||
<Route { ...route } key={ name } render={ () => {
|
||||
const Next = () => processMiddlewares([ ...middlewares, content ])
|
||||
return <Next />
|
||||
} } />
|
||||
) }
|
||||
</Switch> }
|
||||
</main>
|
||||
<footer className="footer">
|
||||
<Container style={{ display: 'flex', alignItems: "center" }}>
|
||||
|
@ -1,82 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ButtonProps,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogProps,
|
||||
DialogTitle, FormControl,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import { Button, ButtonGroup, Dialog, DialogActions, DialogContent, DialogTitle, Menu, MenuItem, TextField, Typography } from "@material-ui/core";
|
||||
import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useVerticalSpacing } from "@/styles";
|
||||
import { createPortal } from "react-dom";
|
||||
// @ts-ignore
|
||||
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||
// @ts-ignore
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||
|
||||
type AcceptSubmissionDialogProps = {
|
||||
onAccept: (comment?: string) => void;
|
||||
label: string;
|
||||
} & DialogProps;
|
||||
|
||||
export function AcceptSubmissionDialog({ onAccept, label, ...props }: AcceptSubmissionDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [comment, setComment] = useState<string>("");
|
||||
const classes = useVerticalSpacing(3);
|
||||
|
||||
return <Dialog maxWidth="xl" { ...props }>
|
||||
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
|
||||
<DialogContent className={ classes.root }>
|
||||
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
|
||||
<CKEditor data={ comment } editor={ ClassicEditor } onChange={ (_: any, ed: any) => setComment(ed.getData()) }/>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={ ev => props.onClose?.(ev, "backdropClick") }>
|
||||
{ t('cancel') }
|
||||
</Button>
|
||||
<Button onClick={ () => onAccept?.(comment) } color="primary" variant="contained">
|
||||
{ t('confirm') }
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
|
||||
type DiscardSubmissionDialogProps = {
|
||||
onDiscard: (comment: string) => void;
|
||||
label: string;
|
||||
} & DialogProps;
|
||||
|
||||
export function DiscardSubmissionDialog({ onDiscard, label, ...props }: DiscardSubmissionDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [comment, setComment] = useState<string>("");
|
||||
const classes = useVerticalSpacing(3);
|
||||
|
||||
return <Dialog maxWidth="xl" { ...props }>
|
||||
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
|
||||
<DialogContent className={ classes.root }>
|
||||
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
|
||||
<CKEditor data={ comment } editor={ ClassicEditor } onChange={ (_: any, ed: any) => setComment(ed.getData()) }/>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={ ev => props.onClose?.(ev, "backdropClick") }>
|
||||
{ t('cancel') }
|
||||
</Button>
|
||||
<Button onClick={ () => onDiscard?.(comment) } color="primary" variant="contained">
|
||||
{ t('confirm') }
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
|
||||
type AcceptanceActionsProps = {
|
||||
onAccept: (comment?: string) => void;
|
||||
@ -90,8 +17,19 @@ export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActi
|
||||
const [isDiscardModalOpen, setDiscardModelOpen] = useState<boolean>(false);
|
||||
const [isAcceptModalOpen, setAcceptModelOpen] = useState<boolean>(false);
|
||||
|
||||
const [comment, setComment] = useState<string>("");
|
||||
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const classes = useVerticalSpacing(3);
|
||||
|
||||
const handleAccept = () => {
|
||||
onAccept(comment);
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
onDiscard(comment);
|
||||
}
|
||||
|
||||
const handleAcceptModalClose = () => {
|
||||
setAcceptModelOpen(false);
|
||||
}
|
||||
@ -139,8 +77,39 @@ export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActi
|
||||
</Button>
|
||||
|
||||
{ createPortal(<>
|
||||
<DiscardSubmissionDialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md" onDiscard={ onDiscard } label={ label }/>
|
||||
<AcceptSubmissionDialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md" onAccept={ onAccept } label={ label }/>
|
||||
<Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md">
|
||||
<DialogTitle>{ t(label + ".discard.title") }</DialogTitle>
|
||||
<DialogContent className={ classes.root }>
|
||||
<Typography variant="body1">{ t(label + ".discard.info") }</Typography>
|
||||
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={ handleDiscardModalClose }>
|
||||
{ t('cancel') }
|
||||
</Button>
|
||||
<Button onClick={ handleDiscard } color="primary" variant="contained">
|
||||
{ t('confirm') }
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md">
|
||||
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
|
||||
<DialogContent className={ classes.root }>
|
||||
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
|
||||
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={ handleAcceptModalClose }>
|
||||
{ t('cancel') }
|
||||
</Button>
|
||||
<Button onClick={ handleAccept } color="primary" variant="contained">
|
||||
{ t('confirm') }
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>, document.getElementById("modals") as Element) }
|
||||
</>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AsyncResult } from "@/hooks";
|
||||
import React from "react";
|
||||
import { CircularProgress } from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Loading } from "@/components/loading";
|
||||
|
||||
|
@ -3,7 +3,7 @@ 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 "@/forms/ckeditor";
|
||||
import { CKEditorField } from "@/field/ckeditor";
|
||||
import { Actions } from "@/components/actions";
|
||||
import { Cancel, Send } from "mdi-material-ui";
|
||||
import { createPortal } from "react-dom";
|
||||
|
@ -75,7 +75,7 @@ export const FileInfo = ({ document, ...props }: FileInfoProps) => {
|
||||
<Async async={ fileinfo }>
|
||||
{ fileinfo => <div className={ classes.grid }>
|
||||
<div className={ classes.iconColumn }>
|
||||
<FileIcon mime={ fileinfo.mime || "" } className={ classes.icon } />
|
||||
<FileIcon mime={ fileinfo.mime } className={ classes.icon } />
|
||||
</div>
|
||||
<aside className={ classes.asideColumn }>
|
||||
<Typography variant="h5" className={ classes.header }>{ fileinfo.filename }</Typography>
|
||||
|
@ -24,7 +24,7 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
|
||||
<StudentPreview student={ proposal.intern } />
|
||||
</div>
|
||||
|
||||
{ proposal.company && proposal.office && <Section>
|
||||
<Section>
|
||||
<Label>{ t('internship.sections.place') }</Label>
|
||||
<Typography className="proposal__primary">
|
||||
{ proposal.company.name }
|
||||
@ -36,12 +36,12 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
|
||||
<Label>{ t('internship.office') }</Label>
|
||||
<Typography className="proposal__primary">{ t('internship.address.city', proposal.office.address) }</Typography>
|
||||
<Typography className="proposal__secondary">{ t('internship.address.street', proposal.office.address) }</Typography>
|
||||
</Section> }
|
||||
</Section>
|
||||
|
||||
{ proposal.type && <Section>
|
||||
<Section>
|
||||
<Label>{ t('internship.sections.kind') }</Label>
|
||||
<Typography className="proposal__primary">{ proposal.type.label.pl }</Typography>
|
||||
</Section> }
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Label>{ t('internship.sections.program') }</Label>
|
||||
|
@ -29,18 +29,19 @@ export const Step = (props: StepProps) => {
|
||||
{ label }
|
||||
<Box>
|
||||
{ state && <Typography variant="subtitle2" display="inline">{ state }</Typography> }
|
||||
{ (notBefore || until) && <Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography> }
|
||||
{ notBefore &&
|
||||
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
||||
{ t('not-before', { date: notBefore }) }
|
||||
</Typography> }
|
||||
{ until &&
|
||||
{ until && <>
|
||||
<Typography variant="subtitle2" display="inline" color="textSecondary"> • </Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary" display="inline">
|
||||
{ t('until', { date: until }) }
|
||||
{ isLate && <Typography color="error" display="inline"
|
||||
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
|
||||
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
|
||||
</Typography> }
|
||||
</Typography>
|
||||
</> }
|
||||
</Box>
|
||||
</StepLabel>
|
||||
{ children && <StepContent>{ children }</StepContent> }
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
|
||||
export type Identifier = string;
|
||||
|
||||
export interface Identifiable {
|
||||
@ -10,7 +8,3 @@ export type Multilingual<T> = {
|
||||
pl: T,
|
||||
en: T
|
||||
}
|
||||
export interface Stateful {
|
||||
comment: string;
|
||||
state: SubmissionStatus;
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Moment } from "moment-timezone";
|
||||
import { Course } from "@/data/course";
|
||||
import { Identifiable } from "@/data/common";
|
||||
import { InternshipProgramEntry, InternshipType } from "@/data/internship";
|
||||
import { ReportSchema } from "@/data/report";
|
||||
|
||||
export type Edition = {
|
||||
course: Course;
|
||||
@ -13,9 +11,6 @@ export type Edition = {
|
||||
reportingEnd: Moment,
|
||||
minimumInternshipHours: number;
|
||||
maximumInternshipHours?: number;
|
||||
program: InternshipProgramEntry[];
|
||||
types: InternshipType[];
|
||||
schema: ReportSchema;
|
||||
} & Identifiable
|
||||
|
||||
export type Deadlines = {
|
||||
|
@ -1,41 +0,0 @@
|
||||
import { Identifiable, Multilingual, Stateful } from "@/data/common";
|
||||
|
||||
interface PredefinedChoices {
|
||||
choices: Multilingual<string>[];
|
||||
}
|
||||
|
||||
export interface BaseFieldDefinition extends Identifiable {
|
||||
description: Multilingual<string>;
|
||||
label: Multilingual<string>;
|
||||
}
|
||||
|
||||
export interface TextFieldDefinition extends BaseFieldDefinition {
|
||||
type: "short-text" | "long-text";
|
||||
}
|
||||
|
||||
export type TextFieldValue = string;
|
||||
|
||||
export interface MultiChoiceFieldDefinition extends BaseFieldDefinition, PredefinedChoices {
|
||||
type: "checkbox";
|
||||
}
|
||||
|
||||
export type MultiChoiceValue = Multilingual<string>[];
|
||||
|
||||
export interface SingleChoiceFieldDefinition extends BaseFieldDefinition, PredefinedChoices {
|
||||
type: "radio" | "select";
|
||||
}
|
||||
|
||||
export type SingleChoiceValue = Multilingual<string>;
|
||||
|
||||
export type ReportFieldDefinition = TextFieldDefinition | MultiChoiceFieldDefinition | SingleChoiceFieldDefinition;
|
||||
export type ReportFieldValue = TextFieldValue | MultiChoiceValue | SingleChoiceValue;
|
||||
export type ReportFieldValues = { [field: string]: ReportFieldValue };
|
||||
export type ReportSchema = ReportFieldDefinition[];
|
||||
export type ReportFieldType = ReportFieldDefinition['type'];
|
||||
|
||||
export interface Report extends Stateful, Identifiable {
|
||||
fields: ReportFieldValues;
|
||||
}
|
||||
|
||||
export const reportFieldTypes: ReportFieldType[] = ["short-text", "long-text", "checkbox", "radio", "select"];
|
||||
|
@ -26,5 +26,3 @@ export function getMissingStudentData(student: Student): (keyof Student)[] {
|
||||
// !!student.course || "course",
|
||||
].filter(x => x !== true) as (keyof Student)[];
|
||||
}
|
||||
|
||||
export const fullname = (student: Student) => `${ student.name } ${ student.surname }`;
|
||||
|
@ -1,186 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { emptyReport, sampleReportSchema } from "@/provider/dummy/report";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Grid,
|
||||
Typography,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Radio,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem, FormHelperText
|
||||
} from "@material-ui/core";
|
||||
import { Actions } from "@/components";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { route } from "@/routing";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MultiChoiceFieldDefinition, Report, ReportFieldDefinition, ReportFieldValues, SingleChoiceFieldDefinition } from "@/data/report";
|
||||
import { TextField as TextFieldFormik } from "formik-material-ui";
|
||||
import { Field, Form, Formik, useFormik, useFormikContext } from "formik";
|
||||
import { Multilingual } from "@/data";
|
||||
import { Transformer } from "@/serialization";
|
||||
import api from "@/api";
|
||||
import { useCurrentEdition } from "@/hooks";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { Description as DescriptionIcon } from "@material-ui/icons";
|
||||
import { DropzoneArea } from "material-ui-dropzone";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
import { UploadType } from "@/api/upload";
|
||||
import { InternshipPlanActions, InternshipReportActions, useDispatch } from "@/state/actions";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "@/state/reducer";
|
||||
|
||||
export type ReportFieldProps<TField = ReportFieldDefinition> = {
|
||||
field: TField;
|
||||
}
|
||||
|
||||
export const name = ({ id }: ReportFieldDefinition) => `field_${id}`;
|
||||
|
||||
export const CustomField = ({ field, ...props }: ReportFieldProps) => {
|
||||
switch (field.type) {
|
||||
case "short-text":
|
||||
case "long-text":
|
||||
return <CustomField.Text {...props} field={ field } />
|
||||
case "checkbox":
|
||||
case "radio":
|
||||
return <CustomField.Choice {...props} field={ field }/>
|
||||
case "select":
|
||||
return <CustomField.Select {...props} field={ field }/>
|
||||
}
|
||||
}
|
||||
|
||||
CustomField.Text = ({ field }: ReportFieldProps) => {
|
||||
return <>
|
||||
<Field label={ field.label.pl } name={ name(field) }
|
||||
fullWidth
|
||||
rows={ field.type == "long-text" ? 4 : 1 }
|
||||
multiline={ field.type == "long-text" }
|
||||
component={ TextFieldFormik }
|
||||
/>
|
||||
<Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/>
|
||||
</>
|
||||
}
|
||||
|
||||
CustomField.Select = ({ field }: ReportFieldProps<SingleChoiceFieldDefinition>) => {
|
||||
const { t } = useTranslation();
|
||||
const id = `custom-field-${field.id}`;
|
||||
const { values, setFieldValue } = useFormikContext<any>();
|
||||
|
||||
const value = values[name(field)];
|
||||
|
||||
return <FormControl variant="outlined">
|
||||
<InputLabel htmlFor={id}>{ field.label.pl }</InputLabel>
|
||||
<Select label={ field.label.pl } name={ name(field) } id={id} value={ value } onChange={ ({ target }) => setFieldValue(name(field), target.value, false) }>
|
||||
{ field.choices.map(choice => <MenuItem value={ choice as any }>{ choice.pl }</MenuItem>) }
|
||||
</Select>
|
||||
<Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/>
|
||||
</FormControl>
|
||||
}
|
||||
|
||||
CustomField.Choice = ({ field }: ReportFieldProps<SingleChoiceFieldDefinition | MultiChoiceFieldDefinition>) => {
|
||||
const { t } = useTranslation();
|
||||
const { values, setFieldValue } = useFormikContext<any>();
|
||||
|
||||
const value = values[name(field)];
|
||||
|
||||
const isSelected = field.type == 'radio'
|
||||
? (checked: Multilingual<string>) => value == checked
|
||||
: (checked: Multilingual<string>) => (value || []).includes(checked)
|
||||
|
||||
const handleChange = field.type == 'radio'
|
||||
? (choice: Multilingual<string>) => () => setFieldValue(name(field), choice, false)
|
||||
: (choice: Multilingual<string>) => () => {
|
||||
const current = value || [];
|
||||
setFieldValue(name(field), !current.includes(choice) ? [ ...current, choice ] : current.filter((c: Multilingual<string>) => c != choice), false);
|
||||
}
|
||||
|
||||
const Component = field.type == 'radio' ? Radio : Checkbox;
|
||||
|
||||
return <FormControl component="fieldset">
|
||||
<FormLabel component="legend">{ field.label.pl }</FormLabel>
|
||||
<FormGroup>
|
||||
{ field.choices.map(choice => <FormControlLabel
|
||||
control={ <Component checked={ isSelected(choice) } onChange={ handleChange(choice) }/> }
|
||||
label={ choice.pl }
|
||||
/>) }
|
||||
</FormGroup>
|
||||
<Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/>
|
||||
</FormControl>
|
||||
}
|
||||
|
||||
export type ReportFormValues = ReportFieldValues;
|
||||
|
||||
const reportFormValuesTransformer: Transformer<Report, ReportFormValues, { report: Report }> = {
|
||||
reverseTransform(subject: ReportFormValues, context: { report: Report }): Report {
|
||||
return { ...context.report, fields: subject };
|
||||
},
|
||||
transform(subject: Report, context: undefined): ReportFormValues {
|
||||
return subject.fields;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ReportForm() {
|
||||
const edition = useCurrentEdition() as Edition;
|
||||
const report = emptyReport;
|
||||
const schema = edition.schema;
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [file, setFile] = useState<File>();
|
||||
const document = useSelector<AppState>(state => state.report.evaluation);
|
||||
|
||||
const handleSubmit = async (values: ReportFormValues) => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = reportFormValuesTransformer.reverseTransform(values, { report });
|
||||
await api.report.save(result);
|
||||
|
||||
let destination: InternshipDocument = document as any;
|
||||
|
||||
if (!destination) {
|
||||
destination = await api.upload.create(UploadType.InternshipEvaluation);
|
||||
}
|
||||
|
||||
await api.upload.upload(destination, file);
|
||||
};
|
||||
|
||||
return <Formik initialValues={ reportFormValuesTransformer.transform(report) } onSubmit={ handleSubmit }>
|
||||
{ ({ submitForm }) => <Form>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" component="p">{ t('forms.report.instructions') }</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/karta%20oceny%20praktyki" startIcon={ <DescriptionIcon /> }>
|
||||
{ t('steps.report.template') }
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") } onChange={ files => setFile(files[0]) }/>
|
||||
<FormHelperText>{ t('forms.report.dropzone-help') }</FormHelperText>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h3">{ t('forms.report.report') }</Typography>
|
||||
</Grid>
|
||||
{ schema.map(field => <Grid item xs={12}><CustomField field={ field }/></Grid>) }
|
||||
<Grid item xs={12}>
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" onClick={ submitForm }>
|
||||
{ t('confirm') }
|
||||
</Button>
|
||||
|
||||
<Button component={ RouterLink } to={ route("home") }>
|
||||
{ t('go-back') }
|
||||
</Button>
|
||||
</Actions>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Form> }
|
||||
</Formik>
|
||||
}
|
||||
|
@ -4,9 +4,8 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import "moment/locale/pl"
|
||||
import "moment/locale/en-gb"
|
||||
import moment, { isDuration, isMoment, Moment, unitOfTime } from "moment-timezone";
|
||||
import moment, { isDuration, isMoment, unitOfTime } from "moment-timezone";
|
||||
import { convertToRoman } from "@/utils/numbers";
|
||||
import MomentUtils from "@date-io/moment";
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@ -53,10 +52,4 @@ i18n
|
||||
document.documentElement.lang = i18n.language;
|
||||
moment.locale(i18n.language)
|
||||
|
||||
export class LocalizedMomentUtils extends MomentUtils {
|
||||
getDatePickerHeaderText(date: Moment): string {
|
||||
return this.format(date, "d MMM yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
|
@ -10,7 +10,13 @@ import { MuiPickersUtilsProvider } from "@material-ui/pickers";
|
||||
import moment, { Moment } from "moment-timezone";
|
||||
import { studentTheme } from "@/ui/theme";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { LocalizedMomentUtils } from "@/i18n";
|
||||
import MomentUtils from "@date-io/moment";
|
||||
|
||||
class LocalizedMomentUtils extends MomentUtils {
|
||||
getDatePickerHeaderText(date: Moment): string {
|
||||
return this.format(date, "d MMM yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { Course } from "@/data";
|
||||
import { axios } from "@/api";
|
||||
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
|
||||
import { encapsulate, OneOrMany } from "@/helpers";
|
||||
import { prepare } from "@/routing";
|
||||
|
||||
const COURSE_INDEX_ENDPOINT = '/management/course'
|
||||
const COURSE_ENDPOINT = COURSE_INDEX_ENDPOINT + "/:id";
|
||||
|
||||
export async function all(): Promise<Course[]> {
|
||||
const response = await axios.get<CourseDTO[]>(COURSE_INDEX_ENDPOINT);
|
||||
return response.data.map(dto => courseDtoTransformer.transform(dto))
|
||||
}
|
||||
|
||||
export async function remove(type: OneOrMany<Course>): Promise<void> {
|
||||
await Promise.all(encapsulate(type).map(
|
||||
type => axios.delete(prepare(COURSE_ENDPOINT, { id: type.id as string }))
|
||||
));
|
||||
}
|
||||
|
||||
export async function save(type: Course): Promise<Course> {
|
||||
await axios.put<Course>(
|
||||
COURSE_INDEX_ENDPOINT,
|
||||
courseDtoTransformer.reverseTransform(type)
|
||||
);
|
||||
|
||||
return type;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import { encapsulate, OneOrMany } from "@/helpers";
|
||||
import { axios } from "@/api";
|
||||
import { prepare } from "@/routing";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
|
||||
const DOCUMENT_ACCEPT_ENDPOINT = "/management/document/:id/accept";
|
||||
const DOCUMENT_REJECT_ENDPOINT = "/management/document/:id/reject";
|
||||
|
||||
export async function accept(document: OneOrMany<InternshipDocument>, comment?: string): Promise<void> {
|
||||
const documents = encapsulate(document)
|
||||
|
||||
await Promise.all(documents.map(document => axios.put(
|
||||
prepare(DOCUMENT_ACCEPT_ENDPOINT, { id: document.id || ""}),
|
||||
JSON.stringify(comment || ""),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
||||
|
||||
export async function discard(document: OneOrMany<InternshipDocument>, comment: string): Promise<void> {
|
||||
const documents = encapsulate(document)
|
||||
|
||||
await Promise.all(documents.map(document => axios.put(
|
||||
prepare(DOCUMENT_REJECT_ENDPOINT, { id: document.id || ""}),
|
||||
JSON.stringify(comment),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { axios } from "@/api";
|
||||
import { EditionDTO, editionDtoTransformer, editionUpdateDtoTransformer } from "@/api/dto/edition";
|
||||
import { EditionDTO, editionDtoTransformer } from "@/api/dto/edition";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { prepare } from "@/routing";
|
||||
|
||||
@ -15,12 +15,3 @@ export async function details(edition: string): Promise<Edition> {
|
||||
const response = await axios.get<EditionDTO>(prepare(MANAGEMENT_EDITION_ENDPOINT, { edition }));
|
||||
return editionDtoTransformer.transform(response.data);
|
||||
}
|
||||
|
||||
export async function save(edition: Edition): Promise<boolean> {
|
||||
const response = await axios.put<EditionDTO>(
|
||||
MANAGEMENT_EDITION_INDEX_ENDPOINT,
|
||||
editionUpdateDtoTransformer.transform(edition),
|
||||
);
|
||||
|
||||
return response.status == 200;
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { ReportFieldDefinition } from "@/data/report";
|
||||
import { axios } from "@/api";
|
||||
import { FieldDefinitionDTO, fieldDefinitionDtoTransformer } from "@/api/dto/edition";
|
||||
|
||||
const REPORT_FIELD_INDEX_ENDPOINT = "/management/report/fields"
|
||||
|
||||
export async function all(): Promise<ReportFieldDefinition[]> {
|
||||
const result = await axios.get<FieldDefinitionDTO[]>(REPORT_FIELD_INDEX_ENDPOINT);
|
||||
return (result.data || []).map(field => fieldDefinitionDtoTransformer.transform(field));
|
||||
}
|
||||
|
||||
export async function save(field: ReportFieldDefinition) {
|
||||
await axios.post(REPORT_FIELD_INDEX_ENDPOINT, fieldDefinitionDtoTransformer.reverseTransform(field));
|
||||
}
|
@ -1,21 +1,11 @@
|
||||
import * as course from "./course"
|
||||
import * as edition from "./edition"
|
||||
import * as page from "./page"
|
||||
import * as type from "./type"
|
||||
import * as internship from "./internship"
|
||||
import * as document from "./document"
|
||||
import * as field from "./field"
|
||||
import * as report from "./report"
|
||||
|
||||
export const api = {
|
||||
course,
|
||||
edition,
|
||||
page,
|
||||
type,
|
||||
internship,
|
||||
document,
|
||||
field,
|
||||
report
|
||||
type
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
@ -1,110 +0,0 @@
|
||||
import { Identifiable, Identifier, Internship } from "@/data";
|
||||
import moment, { Moment } from "moment-timezone";
|
||||
import { encapsulate, Nullable, OneOrMany } from "@/helpers";
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { axios } from "@/api";
|
||||
import { prepare, query } from "@/routing";
|
||||
import {
|
||||
InternshipDocument,
|
||||
InternshipDocumentDTO,
|
||||
internshipDocumentDtoTransformer,
|
||||
InternshipInfoDTO, internshipReportDtoTransformer,
|
||||
submissionStateDtoTransformer
|
||||
} from "@/api/dto/internship-registration";
|
||||
import { OneWayTransformer, Transformer } from "@/serialization";
|
||||
import { mentorDtoTransformer } from "@/api/dto/mentor";
|
||||
import { internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||
import { studentDtoTransfer } from "@/api/dto/student";
|
||||
import { programEntryDtoTransformer } from "@/api/dto/edition";
|
||||
import { UploadType } from "@/api/upload";
|
||||
import { Report } from "@/data/report";
|
||||
|
||||
export type InternshipSubmission = Nullable<Internship> & {
|
||||
state: SubmissionStatus,
|
||||
changed: Moment | null,
|
||||
ipp: InternshipDocument | null,
|
||||
report: Report | null,
|
||||
evaluation: InternshipDocument,
|
||||
grade: number | null,
|
||||
approvals: InternshipDocument[],
|
||||
}
|
||||
|
||||
const INTERNSHIP_MANAGEMENT_INDEX_ENDPOINT = "/management/internship";
|
||||
const INTERNSHIP_MANAGEMENT_ENDPOINT = "/management/internship/:id";
|
||||
const INTERNSHIP_GRADE_ENDPOINT = "/management/internship/:id/grade";
|
||||
const INTERNSHIP_ACCEPT_ENDPOINT = "/management/internship/:id/registration/accept";
|
||||
const INTERNSHIP_REJECT_ENDPOINT = "/management/internship/:id/registration/reject";
|
||||
|
||||
const internshipInfoDtoTransformer: OneWayTransformer<InternshipInfoDTO, InternshipSubmission> = {
|
||||
transform(subject: InternshipInfoDTO, context?: never): InternshipSubmission {
|
||||
// @ts-ignore
|
||||
const ipp = subject.documentation.find<InternshipDocumentDTO>(doc => doc.type === UploadType.Ipp);
|
||||
// @ts-ignore
|
||||
const evaluation = subject.documentation.find<InternshipDocumentDTO>(doc => doc.type === UploadType.InternshipEvaluation);
|
||||
const report = subject.report;
|
||||
|
||||
return {
|
||||
...subject,
|
||||
changed: moment(subject.internshipRegistration.submissionDate),
|
||||
company: subject.internshipRegistration.company,
|
||||
startDate: moment(subject.internshipRegistration.start),
|
||||
endDate: moment(subject.internshipRegistration.end),
|
||||
hours: subject.internshipRegistration.declaredHours,
|
||||
id: subject.id,
|
||||
intern: subject.student && studentDtoTransfer.transform(subject.student),
|
||||
isAccepted: false,
|
||||
lengthInWeeks: 0,
|
||||
mentor: subject.internshipRegistration.mentor && mentorDtoTransformer.transform(subject.internshipRegistration.mentor),
|
||||
office: subject.internshipRegistration.branchAddress,
|
||||
program: (subject.internshipRegistration.subjects || []).map(subject => programEntryDtoTransformer.transform(subject.subject)),
|
||||
state: submissionStateDtoTransformer.transform(subject.internshipRegistration.state),
|
||||
type: subject.internshipRegistration.type && internshipTypeDtoTransformer.transform(subject.internshipRegistration.type),
|
||||
ipp: ipp && internshipDocumentDtoTransformer.transform(ipp),
|
||||
report: report && internshipReportDtoTransformer.transform(report),
|
||||
approvals: (subject.documentation.filter(doc => doc.type === UploadType.DeanConsent).map(subject => internshipDocumentDtoTransformer.transform(subject))),
|
||||
evaluation: evaluation && internshipDocumentDtoTransformer.transform(evaluation),
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
export async function all(edition: Identifiable): Promise<InternshipSubmission[]> {
|
||||
const result = await axios.get<InternshipInfoDTO[]>(query(INTERNSHIP_MANAGEMENT_INDEX_ENDPOINT, { EditionId: edition.id || "" }));
|
||||
|
||||
return result.data.map(result => internshipInfoDtoTransformer.transform(result))
|
||||
}
|
||||
|
||||
export async function get(id: Identifier): Promise<InternshipSubmission> {
|
||||
const result = await axios.get<InternshipInfoDTO>(prepare(INTERNSHIP_MANAGEMENT_ENDPOINT, { id }))
|
||||
|
||||
return internshipInfoDtoTransformer.transform(result.data);
|
||||
}
|
||||
|
||||
export async function accept(internship: OneOrMany<Internship>, comment?: string): Promise<void> {
|
||||
const internships = encapsulate(internship)
|
||||
|
||||
await Promise.all(internships.map(internship => axios.put(
|
||||
prepare(INTERNSHIP_ACCEPT_ENDPOINT, { id: internship.id || ""}),
|
||||
JSON.stringify(comment || ""),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
||||
|
||||
export async function discard(internship: OneOrMany<Internship>, comment: string): Promise<void> {
|
||||
const internships = encapsulate(internship)
|
||||
|
||||
await Promise.all(internships.map(internship => axios.put(
|
||||
prepare(INTERNSHIP_REJECT_ENDPOINT, { id: internship.id || ""}),
|
||||
JSON.stringify(comment),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
||||
|
||||
export async function grade(internship: OneOrMany<Internship>, grade: number): Promise<void> {
|
||||
const internships = encapsulate(internship)
|
||||
|
||||
await Promise.all(internships.map(internship => axios.put(
|
||||
prepare(INTERNSHIP_GRADE_ENDPOINT, { id: internship.id || ""}),
|
||||
JSON.stringify(grade),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import { encapsulate, OneOrMany } from "@/helpers";
|
||||
import { axios } from "@/api";
|
||||
import { prepare } from "@/routing";
|
||||
import { Report } from "@/data/report";
|
||||
|
||||
const REPORT_ACCEPT_ENDPOINT = "/management/report/:id/accept";
|
||||
const REPORT_REJECT_ENDPOINT = "/management/report/:id/reject";
|
||||
|
||||
export async function accept(document: OneOrMany<Report>, comment?: string): Promise<void> {
|
||||
const documents = encapsulate(document)
|
||||
|
||||
await Promise.all(documents.map(document => axios.put(
|
||||
prepare(REPORT_ACCEPT_ENDPOINT, { id: document.id || ""}),
|
||||
JSON.stringify(comment),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
||||
|
||||
export async function discard(document: OneOrMany<Report>, comment: string): Promise<void> {
|
||||
const documents = encapsulate(document)
|
||||
|
||||
await Promise.all(documents.map(document => axios.put(
|
||||
prepare(REPORT_REJECT_ENDPOINT, { id: document.id || ""}),
|
||||
JSON.stringify(comment),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)))
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
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 { initialCourseFormValues, CourseForm, CourseFormValues, courseFormValuesTransformer } from "@/management/course/form";
|
||||
import { Course } from "@/data";
|
||||
|
||||
export type EditCourseDialogProps = {
|
||||
onSave?: (page: Course) => void;
|
||||
value?: Course;
|
||||
} & DialogProps;
|
||||
|
||||
export function EditCourseDialog({ onSave, value, ...props }: EditCourseDialogProps) {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(3);
|
||||
|
||||
const handleSubmit = (values: CourseFormValues) => {
|
||||
onSave?.(courseFormValuesTransformer.reverseTransform(values));
|
||||
};
|
||||
|
||||
const initialValues = value
|
||||
? courseFormValuesTransformer.transform(value)
|
||||
: initialCourseFormValues;
|
||||
|
||||
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>
|
||||
<CourseForm />
|
||||
</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>
|
||||
}
|
||||
|
@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
import { Course } from "@/data";
|
||||
import { Semester } from "@/data/student";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Field, FieldProps } from "formik";
|
||||
import { TextField as TextFieldFormik } from "formik-material-ui";
|
||||
import { Checkbox, Grid } from "@material-ui/core";
|
||||
import { identityTransformer, Transformer } from "@/serialization";
|
||||
|
||||
export type CourseFormValues = Omit<Course, 'id'>;
|
||||
|
||||
export const initialCourseFormValues: CourseFormValues = {
|
||||
name: "",
|
||||
desiredSemesters: []
|
||||
}
|
||||
|
||||
export const courseFormValuesTransformer: Transformer<Course, CourseFormValues> = identityTransformer;
|
||||
|
||||
export const DesiredSemestersField = ({ field, form, meta, ...props }: FieldProps<Semester[]>) => {
|
||||
const { name, value = [] } = field;
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
const toggle = (sid: Semester) => () => {
|
||||
if (!value.includes(sid)) {
|
||||
form.setFieldValue(name, [...value, sid]);
|
||||
} else {
|
||||
form.setFieldValue(name, value.filter((a) => a != sid));
|
||||
}
|
||||
}
|
||||
const isChecked = (sid: Semester) => value.includes(sid);
|
||||
|
||||
const desiredSemesterCheckboxes = [];
|
||||
for (var semesterId = 1; semesterId <= 10; semesterId++) {
|
||||
const sid = semesterId;
|
||||
|
||||
desiredSemesterCheckboxes.push(
|
||||
<Grid item xs={3}>
|
||||
<Checkbox edge="start" onChange={ toggle(sid) } checked={ isChecked(sid) }/>
|
||||
{ t("course.field.desiredSemester", {semesterId: semesterId})}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return <Grid container spacing={3}>
|
||||
{ desiredSemesterCheckboxes }
|
||||
</Grid>
|
||||
}
|
||||
|
||||
export function CourseForm() {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
return <div className={ spacing.vertical }>
|
||||
<Field label={ t("page.field.title") } name="name" fullWidth component={ TextFieldFormik }/>
|
||||
<Field name="desiredSemesters" component={ DesiredSemestersField } />
|
||||
</div>
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
import { Page } from "@/pages/base";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncState } from "@/hooks";
|
||||
import { Course } 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 { EditCourseDialog } from "@/management/course/edit";
|
||||
|
||||
const title = "course.index.title";
|
||||
|
||||
const label = (course: Course) => course?.name;
|
||||
|
||||
export const CourseManagement = () => {
|
||||
const { t } = useTranslation("management");
|
||||
const [result, setCoursesPromise] = useAsyncState<Course[]>();
|
||||
const [selected, setSelected] = useState<Course[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updateCourseList = () => {
|
||||
setCoursesPromise(api.course.all());
|
||||
}
|
||||
|
||||
const handleCourseDelete = async (type: OneOrMany<Course>) => {
|
||||
await api.course.remove(type);
|
||||
updateCourseList();
|
||||
}
|
||||
|
||||
useEffect(updateCourseList, []);
|
||||
|
||||
const DeleteCourseAction = createDeleteAction({ label, onDelete: handleCourseDelete });
|
||||
|
||||
const CreateCourseAction = () => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handleCourseCreation = async (value: Course) => {
|
||||
await api.course.save(value);
|
||||
setOpen(false);
|
||||
updateCourseList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||
{ open && createPortal(
|
||||
<EditCourseDialog open={ open } onSave={ handleCourseCreation } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const EditCourseAction = ({ resource }: { resource: Course }) => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handleCourseCreation = async (value: Course) => {
|
||||
await api.course.save(value);
|
||||
setOpen(false);
|
||||
updateCourseList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("actions.edit") as any }>
|
||||
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||
</Tooltip>
|
||||
{ open && createPortal(
|
||||
<EditCourseDialog open={ open } onSave={ handleCourseCreation } value={ resource } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const columns: Column<Course>[] = [
|
||||
{
|
||||
field: "id",
|
||||
title: "ID",
|
||||
width: 0,
|
||||
defaultSort: "asc",
|
||||
filtering: false,
|
||||
},
|
||||
{
|
||||
title: t("course.field.name"),
|
||||
render: type => type.name,
|
||||
customSort: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: t("course.field.desiredSemesters"),
|
||||
render: type => type.desiredSemesters.slice().sort().join(", "),
|
||||
sorting: false
|
||||
},
|
||||
actionsColumn(type => <>
|
||||
<DeleteCourseAction resource={ type }/>
|
||||
<EditCourseAction 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>
|
||||
<CreateCourseAction />
|
||||
<Button onClick={ updateCourseList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
<DeleteCourseAction resource={ selected }>
|
||||
{ action => <Button startIcon={ <Delete /> } onClick={ action }>{ t("actions.delete") }</Button> }
|
||||
</DeleteCourseAction>
|
||||
</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,68 +0,0 @@
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { createStyles, makeStyles } from "@material-ui/core/styles";
|
||||
import { Theme, Tooltip } from "@material-ui/core";
|
||||
import { green, orange, red } from "@material-ui/core/colors";
|
||||
import React, { HTMLProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { stateIcons } from "@/management/edition/proposal/common";
|
||||
import { Remove } from "@material-ui/icons";
|
||||
import { Close } from "mdi-material-ui";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
"root": {
|
||||
borderWidth: "2px",
|
||||
borderStyle: "solid",
|
||||
borderRadius: "100%",
|
||||
padding: "0.25rem",
|
||||
display: "inline-block",
|
||||
width: "2.25rem",
|
||||
height: "2.25rem",
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
transform: "scale(0.8)"
|
||||
},
|
||||
"icon": {
|
||||
position: "absolute",
|
||||
bottom: "-12px",
|
||||
right: "-12px",
|
||||
fontSize: "0.25rem",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "100%",
|
||||
transform: "scale(0.75)",
|
||||
padding: "3px",
|
||||
},
|
||||
awaiting: {
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
declined: {
|
||||
borderColor: red["600"],
|
||||
color: red["600"],
|
||||
},
|
||||
draft: {},
|
||||
accepted: {
|
||||
borderColor: green["600"],
|
||||
color: green["600"]
|
||||
}
|
||||
}))
|
||||
|
||||
export type StepStateProps = {
|
||||
state: SubmissionStatus | null;
|
||||
label: string;
|
||||
icon: React.ReactChild,
|
||||
} & HTMLProps<HTMLDivElement>;
|
||||
|
||||
export const StepState = ({ label, state, icon, ...props }: StepStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
const classes = useStyles();
|
||||
|
||||
return <Tooltip title={`${label} - ${t(`submission.status.${state || "empty"}`)}`}>
|
||||
<div className={ classNames(classes.root, state && classes[state]) } { ...props }>
|
||||
{ icon }
|
||||
<div className={ classes.icon }>
|
||||
{ state ? stateIcons[state] : <Close /> }
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { Nullable } from "@/helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FieldProps, Field, FieldArrayRenderProps, FieldArray, getIn } from "formik";
|
||||
import { identityTransformer, Transformer } from "@/serialization";
|
||||
import {
|
||||
Button, Card, CardContent, CardHeader,
|
||||
Checkbox,
|
||||
Grid, IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Paper,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Moment } from "moment-timezone";
|
||||
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
||||
import { TextField as TextFieldFormik } from "formik-material-ui";
|
||||
import { Course, Identifiable, InternshipProgramEntry, InternshipType } from "@/data";
|
||||
import { Autocomplete } from "@material-ui/lab";
|
||||
import { useAsync } from "@/hooks";
|
||||
import api from "@/management/api";
|
||||
import { Async } from "@/components/async";
|
||||
import { AccountCheck, ArrowDown, ArrowUp, ShieldCheck, TrashCan } from "mdi-material-ui";
|
||||
import { Actions } from "@/components";
|
||||
import { Add } from "@material-ui/icons";
|
||||
|
||||
export type EditionFormValues = Omit<Nullable<Edition>, "schema">;
|
||||
|
||||
export const initialEditionFormValues: EditionFormValues = {
|
||||
course: null,
|
||||
endDate: null,
|
||||
minimumInternshipHours: 80,
|
||||
proposalDeadline: null,
|
||||
reportingEnd: null,
|
||||
reportingStart: null,
|
||||
startDate: null,
|
||||
types: [],
|
||||
program: [],
|
||||
}
|
||||
|
||||
export const editionFormValuesTransformer: Transformer<Edition, EditionFormValues> = identityTransformer;
|
||||
|
||||
export function toggleValueInArray<T extends Identifiable>(array: T[], value: T, comparator: (a: T, b: T) => boolean = (a, b) => a == b): T[] {
|
||||
return array.findIndex(other => comparator(other, value)) === -1
|
||||
? [ ...array, value ]
|
||||
: array.filter(other => !comparator(other, value));
|
||||
}
|
||||
|
||||
export const ProgramField = ({ remove, swap, push, form, name, ...props }: FieldArrayRenderProps) => {
|
||||
const value = getIn(form.values, name) as InternshipProgramEntry[];
|
||||
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
return <>
|
||||
{ value.map((entry, index) => <Card>
|
||||
<CardHeader
|
||||
subheader={ t('edition.program.entry', { index: index + 1 }) }
|
||||
action={ <>
|
||||
{ index < value.length - 1 && <IconButton onClick={ () => swap(index, index + 1) }><ArrowDown /></IconButton> }
|
||||
{ index > 0 && <IconButton onClick={ () => swap(index, index - 1) }><ArrowUp /></IconButton> }
|
||||
<IconButton onClick={ () => remove(index) }><TrashCan /></IconButton>
|
||||
</> }
|
||||
/>
|
||||
<CardContent>
|
||||
<Field component={ TextFieldFormik }
|
||||
label={ t('edition.program.field.description') }
|
||||
name={`${name}[${index}].description`}
|
||||
fullWidth
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>) }
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => push({ description: "" }) }>{ t("actions.add") }</Button>
|
||||
</Actions>
|
||||
</>
|
||||
}
|
||||
|
||||
export const TypesField = ({ field, form, meta, ...props }: FieldProps<InternshipType[]>) => {
|
||||
const { name, value = [] } = field;
|
||||
|
||||
const types = useAsync(useCallback(() => api.type.all(), []));
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
const toggle = (type: InternshipType) => () => form.setFieldValue(name, toggleValueInArray(value, type, (a, b) => a.id == b.id));
|
||||
const isChecked = (type: InternshipType) => value.findIndex(v => v.id == type.id) !== -1;
|
||||
|
||||
return <Async async={ types }>
|
||||
{ types => <List>{
|
||||
types.map(type => <ListItem dense button onClick={ toggle(type) }>
|
||||
<ListItemIcon>
|
||||
<Checkbox edge="start" onChange={ toggle(type) } checked={ isChecked(type) }/>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<div>{ type.label.pl }</div>
|
||||
<Typography variant="caption">{ type.description?.pl }</Typography>
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<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>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>)
|
||||
}</List> }
|
||||
</Async>
|
||||
}
|
||||
|
||||
export const CoursePickerField = ({ field, form, meta, ...props }: FieldProps<Course>) => {
|
||||
const courses = useAsync(useCallback(() => api.course.all(), []));
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
return <Autocomplete
|
||||
options={ courses.isLoading ? [] : courses.value as Course[] }
|
||||
renderInput={ props => <TextField { ...props } label={ t("edition.field.course") } fullWidth/> }
|
||||
getOptionLabel={ course => course.name }
|
||||
value={ field.value }
|
||||
onChange={ (_, value) => form.setFieldValue(field.name, value, false) }
|
||||
onBlur={ field.onBlur }
|
||||
/>
|
||||
}
|
||||
|
||||
export const DatePickerField = ({ field, form, meta, ...props }: FieldProps<Moment>) => {
|
||||
const { value, onChange, onBlur } = field;
|
||||
|
||||
return <DatePicker value={ value }
|
||||
onChange={ onChange }
|
||||
onBlur={ onBlur }
|
||||
{ ...props }
|
||||
format="DD.MM.yyyy"
|
||||
disableToolbar fullWidth
|
||||
variant="inline"
|
||||
/>
|
||||
}
|
||||
|
||||
export const EditionForm = () => {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
return <div className={ spacing.vertical }>
|
||||
<Typography variant="h5">{ t("edition.fields.basic") }</Typography>
|
||||
<Grid container>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="startDate" component={ DatePickerField } label={ t("edition.field.start") } />
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="endDate" component={ DatePickerField } label={ t("edition.field.end") } />
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="course" component={ CoursePickerField } label={ t("edition.field.course") } />
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="minimumInternshipHours" component={ TextFieldFormik } label={ t("edition.field.minimumInternshipHours") } />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography variant="h5">{ t("edition.fields.deadlines") }</Typography>
|
||||
<Grid container>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="proposalDeadline" component={ DatePickerField } label={ t("edition.field.proposalDeadline") } />
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="reportingStart" component={ DatePickerField } label={ t("edition.field.reportingStart") } />
|
||||
</Grid>
|
||||
<Grid item xs={ 12 } md={ 6 }>
|
||||
<Field name="reportingEnd" component={ DatePickerField } label={ t("edition.field.reportingEnd") } />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography variant="h5">{ t("edition.fields.program") }</Typography>
|
||||
<FieldArray name="program" component={ ProgramField as any } />
|
||||
<Typography variant="h5">{ t("edition.fields.types") }</Typography>
|
||||
<Paper elevation={ 2 }>
|
||||
<Field name="types" component={ TypesField } />
|
||||
</Paper>
|
||||
</div>
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, FormControl, InputLabel, MenuItem, Select } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type GradeDialogProps = {
|
||||
internship: InternshipSubmission;
|
||||
onSubmit: (grade: number) => void;
|
||||
} & Omit<DialogProps, "onSubmit">;
|
||||
|
||||
export const GradeDialog = ({ internship, onSubmit, ...props }: GradeDialogProps) => {
|
||||
const [grade, setGrade] = useState<number | null>(internship.grade || null);
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setGrade(event.target.value as number);
|
||||
};
|
||||
|
||||
return <Dialog maxWidth="sm" fullWidth { ...props }>
|
||||
<DialogTitle>{ t("internship.grade") }</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="demo-simple-select-label">{ t("internship.grade") }</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={ grade }
|
||||
onChange={ handleChange }
|
||||
>
|
||||
<MenuItem value={ 2.0 }>2 - Niedostateczny</MenuItem>
|
||||
<MenuItem value={ 3.0 }>3 - Dostateczny</MenuItem>
|
||||
<MenuItem value={ 3.5 }>3.5 - Dostateczny plus</MenuItem>
|
||||
<MenuItem value={ 4.0 }>4 - Dobry</MenuItem>
|
||||
<MenuItem value={ 4.5 }>4.5 - Dobry plus</MenuItem>
|
||||
<MenuItem value={ 5.0 }>5 - Bardzo Dobry</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="primary" onClick={ () => onSubmit(grade as number) } disabled={ grade === null }>{ t("save") }</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
}
|
@ -1,281 +0,0 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncState } from "@/hooks";
|
||||
import { useSpacing } from "@/styles";
|
||||
import api from "@/management/api";
|
||||
import { Box, Button, Container, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { actionsColumn } from "@/management/common/helpers";
|
||||
import {
|
||||
Account,
|
||||
BriefcaseAccount,
|
||||
BriefcaseAccountOutline, CertificateOutline,
|
||||
FileChartOutline,
|
||||
FileFind,
|
||||
FormatPageBreak,
|
||||
Refresh,
|
||||
Star,
|
||||
StickerCheckOutline
|
||||
} from "mdi-material-ui";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Actions } from "@/components";
|
||||
import { BulkActions } from "@/management/common/BulkActions";
|
||||
import { Async } from "@/components/async";
|
||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||
import { EditionContext, EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { createPortal } from "react-dom";
|
||||
import { FileInfo } from "@/components/fileinfo";
|
||||
import { StepState } from "@/management/edition/common/StepState";
|
||||
import { fullname, Internship, isStudentDataComplete, Student } from "@/data";
|
||||
import { GradeDialog } from "@/management/edition/internship/grade";
|
||||
import { InternshipDetailsDialog } from "@/management/edition/proposal/details";
|
||||
import { AcceptanceActions } from "@/components/acceptance-action";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
import { StudentPreview } from "@/pages/user/profile";
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { ReportPreview } from "@/pages/steps/report";
|
||||
import { Report, ReportSchema } from "@/data/report";
|
||||
|
||||
const title = "edition.internships.title";
|
||||
|
||||
export const canGrade = (internship: InternshipSubmission) => !!(internship);
|
||||
|
||||
const ProposalAction = ({ internship, children }: { internship: InternshipSubmission, children: React.ReactChild }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
setOpen(false);
|
||||
api.internship.discard(internship as Internship, comment);
|
||||
}
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
setOpen(false);
|
||||
api.internship.accept(internship as Internship, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
{ internship.state
|
||||
? <div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children }</div>
|
||||
: children }
|
||||
{ createPortal(
|
||||
<InternshipDetailsDialog onDiscard={ handleDiscard } onAccept={ handleAccept } internship={ internship } open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const IPPAction = ({ internship, children }: { internship: InternshipSubmission, children: React.ReactChild }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
setOpen(false);
|
||||
api.document.discard(internship.ipp as InternshipDocument, comment);
|
||||
}
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
setOpen(false);
|
||||
api.document.accept(internship.ipp as InternshipDocument, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
{ internship.ipp
|
||||
? <div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children }</div>
|
||||
: children }
|
||||
{ createPortal(
|
||||
<Dialog maxWidth="md" fullWidth open={ open } onClose={ () => setOpen(false) }>
|
||||
<DialogTitle>{ fullname(internship.intern as Student) }</DialogTitle>
|
||||
<DialogContent>
|
||||
{ internship.ipp && <FileInfo document={ internship.ipp } /> }
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="internship"/>
|
||||
</DialogActions>
|
||||
</Dialog>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const ReportAction = ({ internship, children }: { internship: InternshipSubmission, children: React.ReactChild }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const edition = useContext(EditionContext);
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
setOpen(false);
|
||||
api.document.discard(internship.evaluation as InternshipDocument, comment);
|
||||
}
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
setOpen(false);
|
||||
api.document.accept(internship.evaluation as InternshipDocument, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
{ internship.report
|
||||
? <div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children }</div>
|
||||
: children }
|
||||
{ createPortal(
|
||||
<Dialog maxWidth="md" fullWidth open={ open } onClose={ () => setOpen(false) }>
|
||||
<DialogTitle>{ fullname(internship.intern as Student) }</DialogTitle>
|
||||
<DialogContent>
|
||||
{ internship.evaluation && <FileInfo document={ internship.evaluation } /> }
|
||||
<ReportPreview schema={ edition?.schema as ReportSchema } report={ internship.report as Report } />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="internship"/>
|
||||
</DialogActions>
|
||||
</Dialog>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const StudentAction = ({ internship, children }: { internship: InternshipSubmission, children: React.ReactChild }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return <>
|
||||
<div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children }</div>
|
||||
{ createPortal(
|
||||
<Dialog maxWidth="md" fullWidth open={ open } onClose={ () => setOpen(false) }>
|
||||
<DialogTitle>{ fullname(internship.intern as Student) }</DialogTitle>
|
||||
<DialogContent>
|
||||
{ internship.intern && <StudentPreview student={ internship.intern } /> }
|
||||
</DialogContent>
|
||||
</Dialog>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
export const InternshipState = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const studentDataState = internship.intern && isStudentDataComplete(internship.intern) ? "accepted" : null;
|
||||
const proposalState = internship.state;
|
||||
const ippState = internship.ipp?.state || null;
|
||||
const reportState = internship.evaluation?.state || null;
|
||||
const gradeState = internship.grade ? "accepted" : null;
|
||||
const approvalState = internship.approvals.reduce<SubmissionStatus | null>((status, document) => {
|
||||
switch (status) {
|
||||
case "awaiting":
|
||||
return status;
|
||||
case "declined":
|
||||
return document.state === "awaiting" ? document.state : status;
|
||||
case "draft":
|
||||
return ["awaiting", "declined"].includes(document.state) ? document.state : status;
|
||||
default:
|
||||
return document.state;
|
||||
}
|
||||
}, null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const spacing = useSpacing(0.25);
|
||||
|
||||
return <div className={ spacing.horizontal } style={{ display: "flex" }}>
|
||||
<StudentAction internship={ internship }>
|
||||
<StepState state={ studentDataState } label={ t("steps.personal-data.header") } icon={ <Account /> } />
|
||||
</StudentAction>
|
||||
<ProposalAction internship={ internship }>
|
||||
<StepState state={ proposalState } label={ t("steps.internship-proposal.header") } icon={ <BriefcaseAccount /> } />
|
||||
</ProposalAction>
|
||||
<IPPAction internship={ internship }>
|
||||
<StepState state={ ippState } label={ t("steps.plan.header") } icon={ <FormatPageBreak /> } />
|
||||
</IPPAction>
|
||||
<ReportAction internship={ internship }>
|
||||
<StepState state={ reportState } label={ t("steps.report.header") } icon={ <FileChartOutline /> } />
|
||||
</ReportAction>
|
||||
<StepState state={ gradeState } label={ t("steps.grade.header") } icon={ <Star /> } />
|
||||
<StepState state={ approvalState } label={ t("steps.approvals.header") } icon={ <CertificateOutline/> }
|
||||
style={ approvalState ? {} : { opacity: 0.2 } }
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const InternshipManagement = ({ edition }: EditionManagementProps) => {
|
||||
const { t } = useTranslation("management");
|
||||
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
|
||||
const [selected, setSelected] = useState<InternshipSubmission[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updateInternshipList = () => {
|
||||
setInternshipsPromise(api.internship.all(edition));
|
||||
}
|
||||
|
||||
useEffect(updateInternshipList, []);
|
||||
|
||||
const GradeAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleGradeSubmission = async (grade: number) => {
|
||||
await api.internship.grade(internship as Internship, grade);
|
||||
setOpen(false);
|
||||
updateInternshipList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("internship.grade") as string }>
|
||||
<IconButton onClick={ () => setOpen(true) }><StickerCheckOutline/></IconButton>
|
||||
</Tooltip>
|
||||
{ createPortal(
|
||||
<GradeDialog onSubmit={ handleGradeSubmission } internship={ internship } open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const columns: Column<InternshipSubmission>[] = [
|
||||
{
|
||||
title: t("internship.column.student"),
|
||||
render: internship => <>{internship.intern?.name} {internship.intern?.surname}</>,
|
||||
},
|
||||
{
|
||||
title: t("internship.column.album"),
|
||||
field: "intern.albumNumber",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.type"),
|
||||
field: "type.label.pl",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.status"),
|
||||
render: summary => <InternshipState internship={ summary } />
|
||||
},
|
||||
{
|
||||
title: t("internship.column.grade"),
|
||||
field: "grade",
|
||||
width: 0,
|
||||
},
|
||||
actionsColumn(internship => <>
|
||||
{ canGrade(internship) && <GradeAction internship={ internship } /> }
|
||||
</>)
|
||||
];
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<EditionManagement.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</EditionManagement.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||
<Actions>
|
||||
<Button onClick={ updateInternshipList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
|
||||
</BulkActions> }
|
||||
<Async async={ result } keepValue>{
|
||||
internships => <MaterialTable
|
||||
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||
columns={ columns }
|
||||
data={ internships }
|
||||
onSelectionChange={ internships => setSelected(internships) }
|
||||
options={ { selection: true, pageSize: 10 } }
|
||||
/>
|
||||
}</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import { useSpacing } from "@/styles";
|
||||
import api from "@/management/api";
|
||||
import { Box, Button, Container, IconButton, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { actionsColumn } from "@/management/common/helpers";
|
||||
import { FileDownloadOutline, FileFind, Refresh, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Actions } from "@/components";
|
||||
import { BulkActions } from "@/management/common/BulkActions";
|
||||
import { Async } from "@/components/async";
|
||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||
import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { route } from "@/routing";
|
||||
import { AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
|
||||
import { ProposalPreview } from "@/components/proposalPreview";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { StateLabel } from "@/management/edition/proposal/common";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Internship } from "@/data";
|
||||
import { FileInfo } from "@/components/fileinfo";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
|
||||
const title = "edition.ipp.title";
|
||||
|
||||
export const canEdit = (ipp: InternshipDocument | null) => !!(ipp && ipp.state != "draft");
|
||||
export const canDownload = (ipp: InternshipDocument | null) => !!(ipp && ipp.id);
|
||||
export const canAccept = (ipp: InternshipDocument | null) => !!(ipp && ["declined", "awaiting"].includes(ipp.state));
|
||||
export const canDiscard = (ipp: InternshipDocument | null) => !!(ipp && ["accepted", "awaiting"].includes(ipp.state));
|
||||
|
||||
export const PlanManagement = ({ edition }: EditionManagementProps) => {
|
||||
const { t } = useTranslation("management");
|
||||
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
|
||||
const [selected, setSelected] = useState<InternshipSubmission[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updateInternshipList = () => {
|
||||
setInternshipsPromise(api.internship.all(edition));
|
||||
}
|
||||
|
||||
useEffect(updateInternshipList, []);
|
||||
|
||||
const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleSubmissionAccept = async (comment?: string) => {
|
||||
setOpen(false);
|
||||
await api.document.accept(internship.ipp as InternshipDocument, comment);
|
||||
updateInternshipList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<IconButton onClick={ () => setOpen(true) }><StickerCheckOutline /></IconButton>
|
||||
{ createPortal(
|
||||
<AcceptSubmissionDialog onAccept={ handleSubmissionAccept } label="plan" open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const DiscardAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleSubmissionDiscard = async (comment: string) => {
|
||||
setOpen(false);
|
||||
await api.document.discard(internship.ipp as InternshipDocument, comment);
|
||||
updateInternshipList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<IconButton onClick={ () => setOpen(true) }><StickerRemoveOutline /></IconButton>
|
||||
{ createPortal(
|
||||
<DiscardSubmissionDialog onDiscard={ handleSubmissionDiscard } label="plan" open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const InternshipDetails = ({ summary }: { summary: InternshipSubmission }) => {
|
||||
return <Box m={ 3 }>
|
||||
{ summary.ipp ? <FileInfo document={ summary.ipp }/> : <Alert severity="warning" title={ t("ipp.no-submission.title") }>{ t("ipp.no-submission.info") }</Alert> }
|
||||
</Box>
|
||||
}
|
||||
|
||||
const columns: Column<InternshipSubmission>[] = [
|
||||
{
|
||||
title: t("internship.column.student"),
|
||||
render: internship => <>{internship.intern?.name} {internship.intern?.surname}</>,
|
||||
},
|
||||
{
|
||||
title: t("internship.column.album"),
|
||||
field: "intern.albumNumber",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.type"),
|
||||
field: "type.label.pl",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.changed"),
|
||||
render: summary => summary.changed?.format("yyyy-MM-DD")
|
||||
},
|
||||
{
|
||||
title: t("internship.column.status"),
|
||||
render: summary => <StateLabel state={ summary.ipp?.state || null } />
|
||||
},
|
||||
actionsColumn(internship => <>
|
||||
{ canAccept(internship.ipp) && <AcceptAction internship={ internship } /> }
|
||||
{ canDiscard(internship.ipp) && <DiscardAction internship={ internship } /> }
|
||||
{ canDownload(internship.ipp) && <IconButton component={ RouterLink } to={ route("management:edition_internship", { edition: edition.id || "", internship: internship.id || "" }) }><FileDownloadOutline /></IconButton> }
|
||||
</>)
|
||||
];
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<EditionManagement.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</EditionManagement.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||
<Actions>
|
||||
<Button onClick={ updateInternshipList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
|
||||
</BulkActions> }
|
||||
<Async async={ result } keepValue>{
|
||||
internships => <MaterialTable
|
||||
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||
columns={ columns }
|
||||
data={ internships }
|
||||
onSelectionChange={ internships => setSelected(internships) }
|
||||
options={ { selection: true, pageSize: 10 } }
|
||||
detailPanel={ summary => <InternshipDetails summary={ summary } /> }
|
||||
/>
|
||||
}</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
@ -1,24 +1,23 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync } from "@/hooks";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import api from "@/management/api";
|
||||
import { Async } from "@/components/async";
|
||||
import { Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { Container, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Action, Column } from "material-table";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { FileFind } from "mdi-material-ui";
|
||||
import { Pencil } from "mdi-material-ui";
|
||||
import { Management } from "../main";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { route } from "@/routing";
|
||||
import { actionsColumn } from "@/management/common/helpers";
|
||||
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]));
|
||||
const result = useAsync(useCallback(() => api.edition.details(edition), [ edition ]));
|
||||
|
||||
return <Async async={ result }>{ edition => <pre>{ JSON.stringify(edition, null, 2) }</pre> }</Async>
|
||||
}
|
||||
@ -27,15 +26,6 @@ export function EditionsManagement() {
|
||||
const { t } = useTranslation("management");
|
||||
const editions = useAsync(useCallback(api.edition.all, []));
|
||||
|
||||
const ManageEditionAction = ({ edition }: { edition: Edition }) => {
|
||||
const history = useHistory();
|
||||
const handlePagePreview = async () => history.push(route('management:edition_manage', { edition: edition.id || "" }));
|
||||
|
||||
return <Tooltip title={ t("actions.manage") as string }>
|
||||
<IconButton onClick={ handlePagePreview }><FileFind/></IconButton>
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
const columns: Column<Edition>[] = [
|
||||
{
|
||||
title: t("edition.field.id"),
|
||||
@ -57,9 +47,13 @@ export function EditionsManagement() {
|
||||
customSort: (a, b) => a.course.name.localeCompare(b.course.name),
|
||||
render: edition => edition.course.name,
|
||||
},
|
||||
actionsColumn(edition => <>
|
||||
<ManageEditionAction edition={ edition }/>
|
||||
</>),
|
||||
]
|
||||
|
||||
const actions: Action<Edition>[] = [
|
||||
{
|
||||
icon: () => <Pencil />,
|
||||
onClick: () => {},
|
||||
}
|
||||
]
|
||||
|
||||
return <Page>
|
||||
@ -75,9 +69,10 @@ export function EditionsManagement() {
|
||||
<MaterialTable
|
||||
columns={ columns }
|
||||
data={ editions }
|
||||
detailPanel={ edition => <EditionDetails edition={ edition.id as string }/> }
|
||||
actions={ actions }
|
||||
detailPanel={ edition => <EditionDetails edition={ edition.id as string } /> }
|
||||
title={ t("edition.index.title") }
|
||||
options={ { search: false } }
|
||||
options={{ search: false, actionsColumnIndex: -1 }}
|
||||
/>
|
||||
}
|
||||
</Async>
|
||||
|
@ -1,116 +0,0 @@
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouteMatch, Link as RouterLink } from "react-router-dom";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Container, Link, Paper, Typography } from "@material-ui/core";
|
||||
import { Management, ManagementLink } from "@/management/main";
|
||||
import { Edition } from "@/data/edition";
|
||||
import {
|
||||
AccountMultiple,
|
||||
BriefcaseAccount,
|
||||
CertificateOutline,
|
||||
CogOutline,
|
||||
FileAccountOutline,
|
||||
FileChartOutline,
|
||||
FileQuestionOutline,
|
||||
FormatPageBreak
|
||||
} from "mdi-material-ui";
|
||||
import { route, routes, Routes } from "@/routing";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import { useAsync } from "@/hooks";
|
||||
import api from "@/management/api";
|
||||
import { Async } from "@/components/async";
|
||||
import { OneOrMany } from "@/helpers";
|
||||
|
||||
const useSectionStyles = makeStyles((theme: Theme) => createStyles({
|
||||
header: {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: 0,
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.grey["800"],
|
||||
},
|
||||
}))
|
||||
|
||||
export function title(edition: Edition) {
|
||||
return `${ edition.course.name } - ${ edition.startDate.year() }`
|
||||
}
|
||||
|
||||
export const EditionContext = React.createContext<Edition | null>(null);
|
||||
|
||||
export const EditionManagement = ({ edition }: EditionManagementProps) => {
|
||||
const { t } = useTranslation("management");
|
||||
|
||||
const spacing = useSpacing(2);
|
||||
const classes = useSectionStyles();
|
||||
|
||||
return <Page>
|
||||
<Page.Header>
|
||||
<Management.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ title(edition) }</Typography>
|
||||
</Management.Breadcrumbs>
|
||||
<Page.Title>{ title(edition) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container className={ spacing.vertical }>
|
||||
<Paper elevation={ 2 }>
|
||||
<Typography className={ classes.header }>{ t("edition.manage.internships") }</Typography>
|
||||
<Management.Menu>
|
||||
<ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_internships", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.internships.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <FileQuestionOutline/> } route={ route("management:edition_proposals", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.proposals.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_ipp_index", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.ipp.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <FileChartOutline/> } route={ route("management:edition_reports", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.reports.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <CertificateOutline/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.dean-approvals.title") }
|
||||
</ManagementLink>
|
||||
</Management.Menu>
|
||||
</Paper>
|
||||
<Paper elevation={ 2 }>
|
||||
<Typography className={ classes.header }>{ t("edition.manage.management") }</Typography>
|
||||
<Management.Menu>
|
||||
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_schema", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.settings.schema") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <CogOutline/> } route={ route("management:edition_settings", { edition: edition.id || "" }) }>
|
||||
{ t("management:edition.settings.title") }
|
||||
</ManagementLink>
|
||||
</Management.Menu>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
||||
|
||||
EditionManagement.Breadcrumbs = ({ children }: { children: OneOrMany<React.ReactChild> }) => {
|
||||
const edition = useContext<Edition | null>(EditionContext);
|
||||
|
||||
return <Management.Breadcrumbs>
|
||||
{ edition && (children
|
||||
? <Link to={ route("management:edition_manage", { edition: edition.id || "" }) } component={ RouterLink }>{ title(edition) }</Link>
|
||||
: <Typography color="textPrimary">{ title(edition) }</Typography>
|
||||
) }
|
||||
{ children }
|
||||
</Management.Breadcrumbs>
|
||||
}
|
||||
|
||||
export type EditionManagementProps = {
|
||||
edition: Edition;
|
||||
}
|
||||
|
||||
export const EditionRouter = () => {
|
||||
const { params } = useRouteMatch();
|
||||
|
||||
const edition = useAsync<Edition>(useCallback(() => api.edition.details(params.edition), [params.edition]));
|
||||
|
||||
return <Async async={ edition }>{
|
||||
result => <EditionContext.Provider value={ result }>
|
||||
<Routes routes={ routes.filter(route => (route.tags || []).includes("edition")) } edition={ result }/>
|
||||
</EditionContext.Provider>
|
||||
}</Async>
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
import React from "react";
|
||||
import { ClockOutline, FileQuestion, NotebookCheckOutline, NotebookEditOutline, NotebookRemoveOutline } from "mdi-material-ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Chip } from "@material-ui/core";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import { green, orange, red } from "@material-ui/core/colors";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { HourglassEmptyRounded } from "@material-ui/icons";
|
||||
|
||||
const useStateLabelStyles = makeStyles((theme: Theme) => createStyles<SubmissionStatus, {}>({
|
||||
awaiting: {
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
declined: {
|
||||
borderColor: red["600"],
|
||||
color: red["600"],
|
||||
},
|
||||
draft: {},
|
||||
accepted: {
|
||||
borderColor: green["600"],
|
||||
color: green["600"]
|
||||
}
|
||||
}))
|
||||
|
||||
export type StateLabelProps = {
|
||||
state: SubmissionStatus | null;
|
||||
};
|
||||
|
||||
export const isValidState = (state: string | null) => ["accepted", "draft", "awaiting", "declined"].includes(state as string)
|
||||
|
||||
export const stateIcons: { [sate in SubmissionStatus]: React.ReactElement } = {
|
||||
accepted: <NotebookCheckOutline/>,
|
||||
awaiting: <HourglassEmptyRounded/>,
|
||||
declined: <NotebookRemoveOutline/>,
|
||||
draft: <NotebookEditOutline/>
|
||||
}
|
||||
|
||||
export const StateLabel = ({ state }: StateLabelProps) => {
|
||||
|
||||
const classes = useStateLabelStyles();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return isValidState(state)
|
||||
? <Chip icon={ stateIcons[state as SubmissionStatus] } label={ t(`translation:submission.status.${ state }`) } variant="outlined" className={ classes[state as SubmissionStatus] }/>
|
||||
: <Chip icon={ <FileQuestion /> } label={ t(`translation:submission.status.empty`) } variant="outlined"/>
|
||||
}
|
||||
|
||||
export const canEdit = (internship: InternshipSubmission) => internship.state != "draft";
|
||||
export const canAccept = (internship: InternshipSubmission) => ["declined", "awaiting"].includes(internship.state);
|
||||
export const canDiscard = (internship: InternshipSubmission) => ["accepted", "awaiting"].includes(internship.state);
|
@ -1,35 +0,0 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core";
|
||||
import { fullname, Internship, Student } from "@/data";
|
||||
import { ProposalPreview } from "@/components/proposalPreview";
|
||||
import { AcceptanceActions } from "@/components/acceptance-action";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import api from "@/management/api";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import { Async } from "@/components/async";
|
||||
|
||||
export type InternshipDetailsDialogProps = {
|
||||
internship: InternshipSubmission;
|
||||
onAccept: (comment?: string) => void;
|
||||
onDiscard: (comment: string) => void;
|
||||
} & DialogProps;
|
||||
|
||||
export const InternshipDetailsDialog = ({ internship, onAccept, onDiscard, ...props }: InternshipDetailsDialogProps) => {
|
||||
const [ details, setPromise ] = useAsyncState();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
setPromise(api.internship.get(internship.id as string));
|
||||
}
|
||||
}, [ props.open, internship.id ])
|
||||
|
||||
return <Dialog maxWidth="lg" fullWidth { ...props }>
|
||||
<DialogTitle>{ fullname(internship.intern as Student) }</DialogTitle>
|
||||
<DialogContent>
|
||||
<Async async={details}>{ internship => <ProposalPreview proposal={ internship as Internship }/> }</Async>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<AcceptanceActions onAccept={ onAccept } onDiscard={ onDiscard } label="internship"/>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import { useSpacing } from "@/styles";
|
||||
import api from "@/management/api";
|
||||
import { Box, Button, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { actionsColumn } from "@/management/common/helpers";
|
||||
import { FileFind, Refresh, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Actions } from "@/components";
|
||||
import { BulkActions } from "@/management/common/BulkActions";
|
||||
import { Async } from "@/components/async";
|
||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||
import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
||||
import { AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
|
||||
import { ProposalPreview } from "@/components/proposalPreview";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { canAccept, canDiscard, StateLabel } from "@/management/edition/proposal/common";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Internship } from "@/data";
|
||||
import { InternshipDetailsDialog } from "@/management/edition/proposal/details";
|
||||
|
||||
const title = "edition.internships.title";
|
||||
|
||||
export const ProposalManagement = ({ edition }: EditionManagementProps) => {
|
||||
const { t } = useTranslation("management");
|
||||
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
|
||||
const [selected, setSelected] = useState<InternshipSubmission[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updateInternshipList = () => {
|
||||
setInternshipsPromise(api.internship.all(edition));
|
||||
}
|
||||
|
||||
useEffect(updateInternshipList, []);
|
||||
|
||||
const handleSubmissionDiscard = async (internship: InternshipSubmission, comment: string) => {
|
||||
await api.internship.discard(internship as Internship, comment);
|
||||
updateInternshipList();
|
||||
}
|
||||
const handleSubmissionAccept = async (internship: InternshipSubmission, comment?: string) => {
|
||||
await api.internship.accept(internship as Internship, comment);
|
||||
updateInternshipList();
|
||||
}
|
||||
|
||||
const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
setOpen(false);
|
||||
handleSubmissionAccept(internship, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("translation:accept") as any }><IconButton onClick={ () => setOpen(true) }><StickerCheckOutline /></IconButton></Tooltip>
|
||||
{ createPortal(
|
||||
<AcceptSubmissionDialog onAccept={ handleAccept } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const DiscardAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
setOpen(false);
|
||||
handleSubmissionDiscard(internship, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("translation:discard") as any }><IconButton onClick={ () => setOpen(true) }><StickerRemoveOutline /></IconButton></Tooltip>
|
||||
{ createPortal(
|
||||
<DiscardSubmissionDialog onDiscard={ handleDiscard } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const PreviewAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
setOpen(false);
|
||||
handleSubmissionDiscard(internship, comment);
|
||||
}
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
setOpen(false);
|
||||
handleSubmissionAccept(internship, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("translation:preview") as any }><IconButton onClick={ () => setOpen(true) }><FileFind /></IconButton></Tooltip>
|
||||
{ createPortal(
|
||||
<InternshipDetailsDialog onDiscard={ handleDiscard } onAccept={ handleAccept } internship={ internship } open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const InternshipDetails = ({ summary }: { summary: InternshipSubmission }) => {
|
||||
const internship = useAsync(useCallback(() => api.internship.get(summary.id || ""), [ summary.id ]))
|
||||
return <Box m={ 3 }><Async async={ internship }>{ internship => <ProposalPreview proposal={ internship as Internship } /> }</Async> </Box>
|
||||
}
|
||||
|
||||
const columns: Column<InternshipSubmission>[] = [
|
||||
{
|
||||
title: t("internship.column.student"),
|
||||
render: internship => <>{internship.intern?.name} {internship.intern?.surname}</>,
|
||||
},
|
||||
{
|
||||
title: t("internship.column.album"),
|
||||
field: "intern.albumNumber",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.type"),
|
||||
field: "type.label.pl",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.changed"),
|
||||
render: summary => summary.changed?.format("yyyy-MM-DD")
|
||||
},
|
||||
{
|
||||
title: t("internship.column.status"),
|
||||
render: summary => <StateLabel state={ summary.state } />
|
||||
},
|
||||
actionsColumn(internship => <>
|
||||
{ canAccept(internship) && <AcceptAction internship={ internship } /> }
|
||||
{ canDiscard(internship) && <DiscardAction internship={ internship } /> }
|
||||
<PreviewAction internship={ internship } />
|
||||
</>)
|
||||
];
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<EditionManagement.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</EditionManagement.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||
<Actions>
|
||||
<Button onClick={ updateInternshipList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
|
||||
</BulkActions> }
|
||||
<Async async={ result } keepValue>{
|
||||
internships => <MaterialTable
|
||||
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||
columns={ columns }
|
||||
data={ internships }
|
||||
onSelectionChange={ internships => setSelected(internships) }
|
||||
options={ { selection: true, pageSize: 10 } }
|
||||
detailPanel={ summary => <InternshipDetails summary={ summary } /> }
|
||||
/>
|
||||
}</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Button, Card, CardContent, CardHeader, Checkbox, Container, Typography } from "@material-ui/core";
|
||||
import { Async } from "@/components/async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync } from "@/hooks";
|
||||
import api from "@/management/api";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { EditionManagement, EditionManagementProps } from "./manage";
|
||||
import { ReportFieldDefinition } from "@/data/report";
|
||||
import { FieldPreview } from "@/management/report/fields/list";
|
||||
import { toggleValueInArray } from "@/management/edition/form";
|
||||
import { Actions } from "@/components";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
const title = "edition.settings.schema";
|
||||
|
||||
export function EditionReportSchema({ edition }: EditionManagementProps) {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
const history = useHistory();
|
||||
|
||||
const fields = useAsync<ReportFieldDefinition[]>(useCallback(() => api.field.all(), []))
|
||||
const [selected, setSelected] = useState<ReportFieldDefinition[]>(edition.schema);
|
||||
|
||||
const isSelected = (field: ReportFieldDefinition) => selected.findIndex(f => f.id === field.id) !== -1;
|
||||
const handleCheckboxClick = (field: ReportFieldDefinition) => () => {
|
||||
setSelected(toggleValueInArray(selected, field, (a, b) => a.id === b.id));
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
await api.edition.save({ ...edition, schema: selected });
|
||||
history.push("management:edition_manage", { edition: edition.id as string })
|
||||
}
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="md">
|
||||
<EditionManagement.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</EditionManagement.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="md" className={ spacing.vertical }>
|
||||
<Async async={ fields }>
|
||||
{ fields => <>
|
||||
{ fields.map(field => <div style={{ display: "flex", alignItems: "start" }}>
|
||||
<Checkbox onClick={ handleCheckboxClick(field) } checked={ isSelected(field) }/>
|
||||
<Card style={{ flex: "1 1 auto" }}>
|
||||
<CardHeader subheader={ field.label.pl } />
|
||||
<CardContent><FieldPreview field={ field }/></CardContent>
|
||||
</Card>
|
||||
</div>) }
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" onClick={ handleSave }>{ t("save") }</Button>
|
||||
</Actions>
|
||||
</> }
|
||||
</Async>
|
||||
</Container>
|
||||
</Page>;
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import { useSpacing } from "@/styles";
|
||||
import api from "@/management/api";
|
||||
import { Box, Button, Container, IconButton, Typography } from "@material-ui/core";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { actionsColumn } from "@/management/common/helpers";
|
||||
import { FileDownloadOutline, FileFind, Refresh, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Actions } from "@/components";
|
||||
import { BulkActions } from "@/management/common/BulkActions";
|
||||
import { Async } from "@/components/async";
|
||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||
import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
||||
import { AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { StateLabel } from "@/management/edition/proposal/common";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Stateful } from "@/data";
|
||||
import { Report } from "@/data/report";
|
||||
|
||||
const title = "edition.reports.title";
|
||||
|
||||
export const canAccept = (subject: Stateful | null) => !!(subject && ["declined", "awaiting"].includes(subject.state));
|
||||
export const canDiscard = (subject: Stateful | null) => !!(subject && ["accepted", "awaiting"].includes(subject.state));
|
||||
|
||||
export const ReportManagement = ({ edition }: EditionManagementProps) => {
|
||||
const { t } = useTranslation("management");
|
||||
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
|
||||
const [selected, setSelected] = useState<InternshipSubmission[]>([]);
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const updateInternshipList = () => {
|
||||
setInternshipsPromise(api.internship.all(edition));
|
||||
}
|
||||
|
||||
useEffect(updateInternshipList, []);
|
||||
|
||||
const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleSubmissionAccept = async (comment?: string) => {
|
||||
setOpen(false);
|
||||
await api.report.accept(internship.report as Report, comment);
|
||||
updateInternshipList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<IconButton onClick={ () => setOpen(true) }><StickerCheckOutline /></IconButton>
|
||||
{ createPortal(
|
||||
<AcceptSubmissionDialog onAccept={ handleSubmissionAccept } label="plan" open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const DiscardAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleSubmissionDiscard = async (comment: string) => {
|
||||
setOpen(false);
|
||||
await api.report.discard(internship.report as Report, comment);
|
||||
updateInternshipList();
|
||||
}
|
||||
|
||||
return <>
|
||||
<IconButton onClick={ () => setOpen(true) }><StickerRemoveOutline /></IconButton>
|
||||
{ createPortal(
|
||||
<DiscardSubmissionDialog onDiscard={ handleSubmissionDiscard } label="plan" open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const InternshipDetails = ({ summary }: { summary: InternshipSubmission }) => {
|
||||
return <Box m={ 3 }>
|
||||
{ summary.report && JSON.stringify(summary.report) }
|
||||
</Box>
|
||||
}
|
||||
|
||||
const columns: Column<InternshipSubmission>[] = [
|
||||
{
|
||||
title: t("internship.column.student"),
|
||||
render: internship => <>{internship.intern?.name} {internship.intern?.surname}</>,
|
||||
},
|
||||
{
|
||||
title: t("internship.column.album"),
|
||||
field: "intern.albumNumber",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.type"),
|
||||
field: "type.label.pl",
|
||||
},
|
||||
{
|
||||
title: t("internship.column.changed"),
|
||||
render: summary => summary.changed?.format("yyyy-MM-DD")
|
||||
},
|
||||
{
|
||||
title: t("internship.column.status"),
|
||||
render: summary => <StateLabel state={ summary.report?.state || null } />
|
||||
},
|
||||
actionsColumn(internship => <>
|
||||
{ canAccept(internship.report) && <AcceptAction internship={ internship } /> }
|
||||
{ canDiscard(internship.report) && <DiscardAction internship={ internship } /> }
|
||||
</>)
|
||||
];
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="lg">
|
||||
<EditionManagement.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</EditionManagement.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
||||
<Actions>
|
||||
<Button onClick={ updateInternshipList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
{ selected.length > 0 && <BulkActions>
|
||||
|
||||
</BulkActions> }
|
||||
<Async async={ result } keepValue>{
|
||||
internships => <MaterialTable
|
||||
title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
|
||||
columns={ columns }
|
||||
data={ internships }
|
||||
onSelectionChange={ internships => setSelected(internships) }
|
||||
options={ { selection: true, pageSize: 10 } }
|
||||
detailPanel={ summary => <InternshipDetails summary={ summary } /> }
|
||||
/>
|
||||
}</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Container, Divider, Typography, Button } from "@material-ui/core";
|
||||
import { Async } from "@/components/async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
import { useAsync } from "@/hooks";
|
||||
import { Edition } from "@/data/edition";
|
||||
import api from "@/management/api";
|
||||
import { Form, Formik } from "formik";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { EditionForm, EditionFormValues, editionFormValuesTransformer } from "@/management/edition/form";
|
||||
import { Actions } from "@/components";
|
||||
import { Save } from "@material-ui/icons";
|
||||
import { Cancel } from "mdi-material-ui";
|
||||
import { EditionManagement } from "./manage";
|
||||
|
||||
const title = "edition.settings.title";
|
||||
|
||||
export function EditionSettings() {
|
||||
const { t } = useTranslation("management");
|
||||
const { params } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
const edition = useAsync<Edition>(useCallback(() => api.edition.details(params.edition), [params.edition]))
|
||||
|
||||
const handleSubmit = async (values: EditionFormValues) => {
|
||||
const result: Edition = {
|
||||
...edition.value,
|
||||
...editionFormValuesTransformer.reverseTransform(values)
|
||||
};
|
||||
|
||||
await api.edition.save(result);
|
||||
|
||||
history.push("management:edition_manage", { edition: edition.value.id as string })
|
||||
};
|
||||
|
||||
return <Page>
|
||||
<Page.Header maxWidth="md">
|
||||
<EditionManagement.Breadcrumbs>
|
||||
<Typography color="textPrimary">{ t(title) }</Typography>
|
||||
</EditionManagement.Breadcrumbs>
|
||||
<Page.Title>{ t(title) }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth="md">
|
||||
<Async async={ edition }>
|
||||
{ edition =>
|
||||
<Formik initialValues={ edition } onSubmit={ handleSubmit }>
|
||||
<Form className={ spacing.vertical }>
|
||||
<EditionForm />
|
||||
<Divider />
|
||||
<Actions>
|
||||
<Button variant="contained" color="primary" type="submit" startIcon={ <Save /> }>{ t("save") }</Button>
|
||||
<Button startIcon={ <Cancel /> }>{ t("cancel") }</Button>
|
||||
</Actions>
|
||||
</Form>
|
||||
</Formik>
|
||||
}
|
||||
</Async>
|
||||
</Container>
|
||||
</Page>;
|
||||
}
|
@ -4,13 +4,7 @@ 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, FormatPageBreak, TableOfContents } from "mdi-material-ui";
|
||||
|
||||
export const ManagementLink = ({ icon, route, children }: ManagementLinkProps) =>
|
||||
<ListItem button component={ RouterLink } to={ route }>
|
||||
<ListItemIcon>{ icon }</ListItemIcon>
|
||||
<ListItemText>{ children }</ListItemText>
|
||||
</ListItem>
|
||||
import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline } from "mdi-material-ui";
|
||||
|
||||
export const Management = {
|
||||
Breadcrumbs: ({ children, ...props }: BreadcrumbsProps) => {
|
||||
@ -20,9 +14,7 @@ export const Management = {
|
||||
<Link component={ RouterLink } to={ route("management:index") }>{ t("management:title") }</Link>
|
||||
{ children }
|
||||
</Page.Breadcrumbs>;
|
||||
},
|
||||
Menu: List,
|
||||
MenuItem: ManagementLink,
|
||||
}
|
||||
}
|
||||
|
||||
type ManagementLinkProps = React.PropsWithChildren<{
|
||||
@ -30,6 +22,12 @@ type ManagementLinkProps = React.PropsWithChildren<{
|
||||
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();
|
||||
|
||||
@ -39,23 +37,17 @@ export const ManagementIndex = () => {
|
||||
</Page.Header>
|
||||
<Container>
|
||||
<Paper elevation={ 2 }>
|
||||
<Management.Menu>
|
||||
<ManagementLink icon={ <TableOfContents /> } route={ route("management:courses") }>
|
||||
{ t("management:course.index.title") }
|
||||
</ManagementLink>
|
||||
<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={ <FormatPageBreak/> } route={ route("management:report_fields") }>
|
||||
{ t("management:report-fields.title") }
|
||||
</ManagementLink>
|
||||
<ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }>
|
||||
{ t("management:page.index.title") }
|
||||
</ManagementLink>
|
||||
</Management.Menu>
|
||||
</List>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Page>
|
||||
|
@ -6,7 +6,7 @@ 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 "@/forms/ckeditor";
|
||||
import { CKEditorField } from "@/field/ckeditor";
|
||||
|
||||
export type StaticPageFormValues = StaticPage;
|
||||
|
||||
|
@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core";
|
||||
import { ReportFieldDefinition } from "@/data/report";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Form, Formik } from "formik";
|
||||
import { Actions } from "@/components";
|
||||
import { Save } from "@material-ui/icons";
|
||||
import { Cancel } from "mdi-material-ui";
|
||||
import { FieldDefinitionForm, FieldDefinitionFormValues, fieldFormValuesTransformer, initialFieldFormValues } from "@/management/report/fields/form";
|
||||
|
||||
export type EditFieldDialogProps = {
|
||||
onSave?: (field: ReportFieldDefinition) => void;
|
||||
field?: ReportFieldDefinition;
|
||||
} & DialogProps;
|
||||
|
||||
export function EditFieldDefinitionDialog({ onSave, field, ...props }: EditFieldDialogProps) {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(3);
|
||||
|
||||
const handleSubmit = (values: FieldDefinitionFormValues) => {
|
||||
onSave?.(fieldFormValuesTransformer.reverseTransform(values));
|
||||
};
|
||||
|
||||
const initialValues = field
|
||||
? fieldFormValuesTransformer.transform(field)
|
||||
: initialFieldFormValues;
|
||||
|
||||
return <Dialog { ...props } maxWidth="md">
|
||||
<Formik initialValues={ initialValues } onSubmit={ handleSubmit }>
|
||||
<Form className={ spacing.vertical }>
|
||||
<DialogTitle>{ t(field ? "report-field.edit.title" : "report-field.create.title") }</DialogTitle>
|
||||
<DialogContent>
|
||||
<FieldDefinitionForm />
|
||||
</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>
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import React from "react";
|
||||
import { ReportFieldDefinition, reportFieldTypes } from "@/data/report";
|
||||
import { identityTransformer, Transformer } from "@/serialization";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { Field, FieldArray, FieldProps, useFormikContext } from "formik";
|
||||
import { TextField as TextFieldFormik, Select } from "formik-material-ui";
|
||||
import { FormControl, InputLabel, Typography, MenuItem, Card, Box, Button, CardContent, CardHeader, IconButton } from "@material-ui/core";
|
||||
import { CKEditorField } from "@/forms/ckeditor";
|
||||
import { Multilingual } from "@/data";
|
||||
import { Actions } from "@/components";
|
||||
import { Add } from "@material-ui/icons";
|
||||
import { TrashCan } from "mdi-material-ui";
|
||||
import { FieldPreview } from "@/management/report/fields/list";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
|
||||
export type FieldDefinitionFormValues = ReportFieldDefinition | { type: string };
|
||||
|
||||
export const initialFieldFormValues: FieldDefinitionFormValues = {
|
||||
type: "short-text",
|
||||
description: {
|
||||
pl: "",
|
||||
en: "",
|
||||
},
|
||||
label: {
|
||||
pl: "",
|
||||
en: "",
|
||||
},
|
||||
choices: [],
|
||||
}
|
||||
|
||||
export const fieldFormValuesTransformer: Transformer<ReportFieldDefinition, FieldDefinitionFormValues> = identityTransformer;
|
||||
export type ChoiceFieldProps = { name: string };
|
||||
|
||||
const ChoiceField = ({ field, form, meta }: FieldProps) => {
|
||||
const { name } = field;
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
return <div className={ spacing.vertical }>
|
||||
<Field label={ t("translation:language.pl") } name={`${name}.pl`} fullWidth component={ TextFieldFormik }/>
|
||||
<Field label={ t("translation:language.en") } name={`${name}.en`} fullWidth component={ TextFieldFormik }/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
preview: {
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: "#e9f0f5",
|
||||
},
|
||||
}))
|
||||
|
||||
export function FieldDefinitionForm() {
|
||||
const { t } = useTranslation("management");
|
||||
const spacing = useSpacing(2);
|
||||
const { values } = useFormikContext<any>();
|
||||
const classes = useStyles();
|
||||
|
||||
return <div className={ spacing.vertical }>
|
||||
<FormControl variant="outlined">
|
||||
<InputLabel htmlFor="report-field-type">{ t("report-field.field.type") }</InputLabel>
|
||||
<Field
|
||||
component={Select}
|
||||
name="type"
|
||||
label={ t("report-field.field.name") }
|
||||
inputProps={{ id: 'report-field-type', }}
|
||||
>
|
||||
{ reportFieldTypes.map(type => <MenuItem value={ type }>{ t(`report-field.type.${type}`) }</MenuItem>)}
|
||||
</Field>
|
||||
</FormControl>
|
||||
<Typography variant="subtitle2">{ t("report-field.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("report-field.field.description") }</Typography>
|
||||
<Field label={ t("translation:language.pl") } name="description.pl" fullWidth component={ CKEditorField }/>
|
||||
<Field label={ t("translation:language.en") } name="description.en" fullWidth component={ CKEditorField }/>
|
||||
|
||||
{ ["radio", "select", "checkbox"].includes(values.type) && <>
|
||||
<Typography variant="subtitle2">{ t("report-field.field.choices") }</Typography>
|
||||
<FieldArray name="choices" render={ helper => <>
|
||||
{ values.choices.map((value: Multilingual<string>, index: number) => <Card>
|
||||
<CardHeader subheader={ t("report-field.field.choice", { index: index + 1 }) } action={ <>
|
||||
<IconButton onClick={ () => helper.remove(index) }>
|
||||
<TrashCan />
|
||||
</IconButton>
|
||||
</> }/>
|
||||
<CardContent>
|
||||
<Field name={`choices[${index}]`} component={ ChoiceField } />
|
||||
</CardContent>
|
||||
</Card>) }
|
||||
<Actions>
|
||||
<Button variant="contained" startIcon={ <Add /> } color="primary" onClick={() => helper.push({ pl: "", en: "" })}>{ t("actions.add") }</Button>
|
||||
</Actions>
|
||||
</> } />
|
||||
</> }
|
||||
|
||||
<div className={ classes.preview }>
|
||||
<Typography variant="subtitle2">{ t("report-field.preview") }</Typography>
|
||||
<FieldPreview field={ fieldFormValuesTransformer.reverseTransform(values) }/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Page } from "@/pages/base";
|
||||
import { Management } from "@/management/main";
|
||||
import { Box, Button, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MaterialTable, { Column } from "material-table";
|
||||
import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers";
|
||||
import { MultilingualCell } from "@/management/common/MultilangualCell";
|
||||
import { ReportFieldDefinition } from "@/data/report";
|
||||
import { Formik } from "formik";
|
||||
import { CustomField } from "@/forms/report";
|
||||
import { Add, Edit } from "@material-ui/icons";
|
||||
import { createPortal } from "react-dom";
|
||||
import { createDeleteAction } from "@/management/common/DeleteResourceAction";
|
||||
import { EditFieldDefinitionDialog } from "@/management/report/fields/edit";
|
||||
import api from "@/management/api";
|
||||
import { useAsync, useAsyncState } from "@/hooks";
|
||||
import { Async } from "@/components/async";
|
||||
import { Actions } from "@/components";
|
||||
import { Refresh } from "mdi-material-ui";
|
||||
import { useSpacing } from "@/styles";
|
||||
|
||||
const title = "report-fields.title";
|
||||
|
||||
export const FieldPreview = ({ field }: { field: ReportFieldDefinition }) => {
|
||||
return <Formik initialValues={{}} onSubmit={() => {}}>
|
||||
<CustomField field={ field }/>
|
||||
</Formik>
|
||||
}
|
||||
|
||||
export const ReportFields = () => {
|
||||
const { t } = useTranslation("management");
|
||||
const [fields, setFieldsPromise] = useAsyncState<ReportFieldDefinition[]>();
|
||||
|
||||
const updateFieldList = () => {
|
||||
setFieldsPromise(api.field.all());
|
||||
}
|
||||
|
||||
useEffect(updateFieldList, []);
|
||||
|
||||
const handleFieldDeletion = () => {}
|
||||
|
||||
const CreateFieldAction = () => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handleFieldCreation = async (value: ReportFieldDefinition) => {
|
||||
await api.field.save(value);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Button variant="contained" color="primary" startIcon={ <Add /> } onClick={ () => setOpen(true) }>{ t("create") }</Button>
|
||||
{ open && createPortal(
|
||||
<EditFieldDefinitionDialog open={ open } onSave={ handleFieldCreation } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const DeleteFieldAction = createDeleteAction<ReportFieldDefinition>({ label: field => field.label.pl, onDelete: handleFieldDeletion })
|
||||
const EditFieldAction = ({ field }: { field: ReportFieldDefinition }) => {
|
||||
const [ open, setOpen ] = useState<boolean>(false);
|
||||
|
||||
const handleFieldSave = async (field: ReportFieldDefinition) => {
|
||||
await api.field.save(field);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Tooltip title={ t("actions.edit") as any }>
|
||||
<IconButton onClick={ () => setOpen(true) }><Edit /></IconButton>
|
||||
</Tooltip>
|
||||
{ open && createPortal(
|
||||
<EditFieldDefinitionDialog open={ open } onSave={ handleFieldSave } field={ field } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
const columns: Column<ReportFieldDefinition>[] = [
|
||||
{
|
||||
title: t("report-field.field.label"),
|
||||
customSort: fieldComparator('label', multilingualStringComparator),
|
||||
cellStyle: { whiteSpace: "nowrap" },
|
||||
render: field => <MultilingualCell value={ field.label }/>,
|
||||
},
|
||||
{
|
||||
title: t("report-field.field.type"),
|
||||
cellStyle: { whiteSpace: "nowrap" },
|
||||
render: field => t(`report-field.type.${field.type}`),
|
||||
},
|
||||
actionsColumn(field => <>
|
||||
<EditFieldAction field={ field }/>
|
||||
<DeleteFieldAction resource={ field }/>
|
||||
</>),
|
||||
]
|
||||
|
||||
const spacing = useSpacing(2);
|
||||
|
||||
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>
|
||||
<CreateFieldAction />
|
||||
<Button onClick={ updateFieldList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
|
||||
</Actions>
|
||||
<Async async={ fields }>
|
||||
{ fields => <MaterialTable
|
||||
columns={ columns }
|
||||
data={ fields }
|
||||
title={ t(title) }
|
||||
detailPanel={ field => <Box p={3}><FieldPreview field={ field } /></Box> }
|
||||
/> }
|
||||
</Async>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
@ -1,35 +1,15 @@
|
||||
import { Route } from "@/routing";
|
||||
import { isManagerMiddleware } from "@/management/middleware";
|
||||
import { CourseManagement } from "@/management/course/list";
|
||||
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";
|
||||
import { EditionRouter, EditionManagement } from "@/management/edition/manage";
|
||||
import { EditionSettings } from "@/management/edition/settings";
|
||||
import { ProposalManagement } from "@/management/edition/proposal/list";
|
||||
import { PlanManagement } from "@/management/edition/ipp/list";
|
||||
import { ReportFields } from "@/management/report/fields/list";
|
||||
import { ReportManagement } from "@/management/edition/report/list";
|
||||
import { InternshipManagement } from "@/management/edition/internship/list";
|
||||
import { EditionReportSchema } from "@/management/edition/report-schema";
|
||||
|
||||
export const managementRoutes: Route[] = ([
|
||||
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
||||
|
||||
{ name: "courses", path: "/courses", content: CourseManagement },
|
||||
{ name: "edition_router", path: "/editions/:edition", content: EditionRouter },
|
||||
{ name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] },
|
||||
{ name: "edition_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"], exact: true },
|
||||
{ name: "edition_internships", path: "/editions/:edition/internships", content: InternshipManagement, tags: ["edition"] },
|
||||
{ name: "edition_proposals", path: "/editions/:edition/proposals", content: ProposalManagement, tags: ["edition"] },
|
||||
{ name: "edition_reports", path: "/editions/:edition/reports", content: ReportManagement, tags: ["edition"] },
|
||||
{ name: "edition_schema", path: "/editions/:edition/schema", content: EditionReportSchema, tags: ["edition"] },
|
||||
{ name: "edition_ipp_index", path: "/editions/:edition/ipp", content: PlanManagement, tags: ["edition"] },
|
||||
{ name: "editions", path: "/editions", content: EditionsManagement },
|
||||
|
||||
{ name: "report_fields", path: "/fields", content: ReportFields },
|
||||
{ name: "types", path: "/types", content: InternshipTypeManagement },
|
||||
{ name: "static_pages", path: "/static-pages", content: StaticPageManagement }
|
||||
] as Route[]).map(
|
||||
|
@ -5,7 +5,7 @@ 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 "@/forms/ckeditor";
|
||||
import { CKEditorField } from "@/field/ckeditor";
|
||||
import { AccountCheck, ShieldCheck } from "mdi-material-ui";
|
||||
import { identityTransformer, Transformer } from "@/serialization";
|
||||
import { LabelWithIcon } from "@/management/common/LabelWithIcon";
|
||||
|
@ -17,8 +17,10 @@ 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";
|
||||
|
@ -8,7 +8,7 @@ export type PageProps = {
|
||||
} & BoxProps;
|
||||
|
||||
export type PageHeaderProps = {
|
||||
maxWidth?: "sm" | "md" | "lg" | "xl" | false
|
||||
maxWidth?: "sm" | "md" | "lg" | false
|
||||
} & HTMLProps<HTMLDivElement>
|
||||
|
||||
export const Page = ({ title, children, ...props }: PageProps) => {
|
||||
|
@ -51,6 +51,20 @@ export const InternshipProposalFormPage = () => {
|
||||
export const InternshipProposalPreviewPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const proposal = useSelector<AppState, Internship | null>(state => state.proposal.proposal && internshipSerializationTransformer.reverseTransform(state.proposal.proposal));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
dispatch({ type: InternshipProposalActions.Approve, comment: comment || null });
|
||||
history.push(route("home"));
|
||||
}
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
dispatch({ type: InternshipProposalActions.Decline, comment: comment });
|
||||
history.push(route("home"));
|
||||
}
|
||||
|
||||
const classes = useVerticalSpacing(3);
|
||||
|
||||
return <Page title={ t("") }>
|
||||
@ -66,6 +80,8 @@ export const InternshipProposalPreviewPage = () => {
|
||||
{ proposal && <ProposalPreview proposal={ proposal } /> }
|
||||
|
||||
<Actions>
|
||||
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="internship" />
|
||||
|
||||
<Button component={ RouterLink } to={ route("home") }>
|
||||
{ t('go-back') }
|
||||
</Button>
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { Page } from "@/pages/base";
|
||||
import { Container, Link, Typography } from "@material-ui/core";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { route } from "@/routing";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReportForm from "@/forms/report";
|
||||
|
||||
export const SubmitReportPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Page title={ t("steps.report.submit") }>
|
||||
<Page.Header maxWidth="md">
|
||||
<Page.Breadcrumbs>
|
||||
<Link component={ RouterLink } to={ route("home") }>{ t('pages.my-internship.header') }</Link>
|
||||
<Typography color="textPrimary">{ t("steps.report.submit") }</Typography>
|
||||
</Page.Breadcrumbs>
|
||||
<Page.Title>{ t("steps.report.submit") }</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth={ "md" }>
|
||||
<ReportForm/>
|
||||
</Container>
|
||||
</Page>
|
||||
}
|
||||
|
||||
export default SubmitReportPage;
|
@ -13,17 +13,11 @@ import { PlanStep } from "@/pages/steps/plan";
|
||||
import { InsuranceState } from "@/state/reducer/insurance";
|
||||
import { InsuranceStep } from "@/pages/steps/insurance";
|
||||
import { StudentStep } from "@/pages/steps/student";
|
||||
import { useCurrentEdition, useDeadlines } from "@/hooks";
|
||||
import api from "@/api";
|
||||
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, InternshipReportActions, useDispatch } from "@/state/actions";
|
||||
import {
|
||||
internshipDocumentDtoTransformer,
|
||||
internshipRegistrationDtoTransformer,
|
||||
internshipReportDtoTransformer, SubmissionState,
|
||||
submissionStateDtoTransformer
|
||||
} from "@/api/dto/internship-registration";
|
||||
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, useDispatch } from "@/state/actions";
|
||||
import { internshipRegistrationDtoTransformer } from "@/api/dto/internship-registration";
|
||||
import { UploadType } from "@/api/upload";
|
||||
import { ReportStep } from "@/pages/steps/report";
|
||||
import { GradeStep } from "@/pages/steps/grade";
|
||||
|
||||
export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
||||
const internship = await api.internship.get();
|
||||
@ -31,41 +25,22 @@ export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: InternshipProposalActions.Receive,
|
||||
state: internship.internshipRegistration.state,
|
||||
comment: internship.internshipRegistration.changeStateComment,
|
||||
internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration),
|
||||
grade: internship.grade,
|
||||
})
|
||||
|
||||
const plan = internship.documentation.find(doc => doc.type === UploadType.Ipp);
|
||||
const evaluation = internship.documentation.find(doc => doc.type === UploadType.InternshipEvaluation);
|
||||
const report = internship.report;
|
||||
|
||||
if (plan) {
|
||||
dispatch({
|
||||
type: InternshipPlanActions.Receive,
|
||||
document: internshipDocumentDtoTransformer.transform(plan),
|
||||
document: plan,
|
||||
state: plan.state,
|
||||
comment: plan.changeStateComment,
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
type: InternshipPlanActions.Reset,
|
||||
})
|
||||
}
|
||||
|
||||
if (report) {
|
||||
dispatch({
|
||||
type: InternshipReportActions.Receive,
|
||||
report: internshipReportDtoTransformer.transform(report),
|
||||
state: evaluation?.state || SubmissionState.Draft,
|
||||
comment: evaluation?.changeStateComment || "",
|
||||
evaluation: evaluation,
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
type: InternshipReportActions.Reset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const MainPage = () => {
|
||||
@ -73,8 +48,10 @@ export const MainPage = () => {
|
||||
|
||||
const student = useSelector<AppState, Student | null>(state => state.student);
|
||||
|
||||
const deadlines = useDeadlines();
|
||||
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
|
||||
const dispatch = useDispatch();
|
||||
const edition = useCurrentEdition();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateInternshipInfo);
|
||||
@ -92,8 +69,8 @@ export const MainPage = () => {
|
||||
if (insurance.required)
|
||||
yield <InsuranceStep key="insurance"/>;
|
||||
|
||||
yield <ReportStep key="report"/>;
|
||||
yield <GradeStep key="grade"/>;
|
||||
yield <Step label={ t('steps.report.header') } until={ deadlines.report } notBefore={ edition?.reportingStart } key="report"/>
|
||||
yield <Step label={ t('steps.grade.header') } key="grade"/>
|
||||
}
|
||||
|
||||
return <Page>
|
||||
|
@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
import { StepProps, Typography } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { Actions, Step } from "@/components";
|
||||
import { ContactButton } from "@/pages/steps/common";
|
||||
|
||||
|
||||
export const GradeStep = (props: StepProps) => {
|
||||
const { t } = useTranslation();
|
||||
const grade = useSelector<AppState, number | null>(state => state.proposal.grade);
|
||||
|
||||
return <Step { ...props } label={ t('steps.grade.header') } completed={ !!grade } active={ true }>
|
||||
{ grade ? <>
|
||||
<Typography variant="h1">{ grade }</Typography>
|
||||
<Actions>
|
||||
<ContactButton />
|
||||
</Actions>
|
||||
</> : <>{ t("steps.grade.wait") }</> }
|
||||
</Step>
|
||||
}
|
@ -3,7 +3,7 @@ import { AppState } from "@/state/reducer";
|
||||
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
|
||||
import { FileUploadOutline } from "mdi-material-ui/index";
|
||||
import { FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index";
|
||||
import { route } from "@/routing";
|
||||
import { Link as RouterLink, useHistory } from "react-router-dom";
|
||||
import { Actions, Step } from "@/components";
|
||||
@ -15,6 +15,7 @@ import { useDeadlines } from "@/hooks";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
import { FileInfo } from "@/components/fileinfo";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { AcceptanceActions } from "@/components/acceptance-action";
|
||||
import { InternshipPlanActions, useDispatch } from "@/state/actions";
|
||||
|
||||
const PlanActions = () => {
|
||||
@ -47,6 +48,7 @@ const PlanActions = () => {
|
||||
switch (status) {
|
||||
case "awaiting":
|
||||
return <Actions>
|
||||
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="plan" />
|
||||
</Actions>
|
||||
case "accepted":
|
||||
return <Actions>
|
||||
@ -75,7 +77,7 @@ export const PlanComment = (props: HTMLProps<HTMLDivElement>) => {
|
||||
|
||||
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
|
||||
<AlertTitle>{ t('comments') }</AlertTitle>
|
||||
<div dangerouslySetInnerHTML={{ __html: comment }} />
|
||||
{ comment }
|
||||
</Alert> : null
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
|
||||
|
||||
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
|
||||
<AlertTitle>{ t('comments') }</AlertTitle>
|
||||
<div dangerouslySetInnerHTML={{ __html: comment }}/>
|
||||
{ comment }
|
||||
</Alert> : null
|
||||
}
|
||||
|
||||
|
@ -1,157 +0,0 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Button, ButtonProps, Dialog, DialogContent, DialogProps, DialogTitle, StepProps, Typography } from "@material-ui/core";
|
||||
import { FileFind, FileUploadOutline } from "mdi-material-ui/index";
|
||||
import { route } from "@/routing";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Actions, Step } from "@/components";
|
||||
import React, { HTMLProps, useState } from "react";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||
import { ContactButton, Status } from "@/pages/steps/common";
|
||||
import { useCurrentEdition, useDeadlines } from "@/hooks";
|
||||
import { useSpacing } from "@/styles";
|
||||
import { MultiChoiceValue, Report, ReportSchema, SingleChoiceValue, TextFieldValue } from "@/data/report";
|
||||
import { createPortal } from "react-dom";
|
||||
import { getInternshipReport } from "@/state/reducer/report";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { FileInfo } from "@/components/fileinfo";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
|
||||
export type ReportPreviewProps = {
|
||||
schema: ReportSchema,
|
||||
report: Report,
|
||||
}
|
||||
|
||||
export const ReportPreview = ({ schema, report }: ReportPreviewProps) => {
|
||||
return <>{ schema.map(field => {
|
||||
const value = report.fields[`field_${ field.id }`];
|
||||
const { t } = useTranslation();
|
||||
|
||||
const Value = () => {
|
||||
switch (field.type) {
|
||||
case "checkbox":
|
||||
return <ul>{ ((value as MultiChoiceValue).map(selection => <li>{ selection.pl }</li>)) }</ul>
|
||||
case "radio":
|
||||
case "select":
|
||||
return <div>{ (value as SingleChoiceValue).pl }</div>
|
||||
case "long-text":
|
||||
case "short-text":
|
||||
return <p style={ { marginTop: "0" } }>{ value as TextFieldValue }</p>
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Typography variant="subtitle2">{ field.label.pl }</Typography>
|
||||
{ value ? <Value/> : t("no-value") }
|
||||
</>
|
||||
}) }</>
|
||||
}
|
||||
|
||||
export type ReportPreviewDialogProps = {
|
||||
report: Report;
|
||||
} & DialogProps;
|
||||
|
||||
export const ReportPreviewDialog = ({ report, ...props }: ReportPreviewDialogProps) => {
|
||||
const edition = useCurrentEdition() as Edition;
|
||||
const schema = edition.schema || [];
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Dialog { ...props } maxWidth="md" fullWidth>
|
||||
<DialogTitle>{ t("steps.report.header") }</DialogTitle>
|
||||
<DialogContent><ReportPreview schema={ schema } report={ report }/></DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
|
||||
const ReportActions = () => {
|
||||
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.report));
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormAction = ({ children = t('steps.report.submit'), ...props }: ButtonProps) =>
|
||||
<Button to={ route("internship_report") } variant="contained" color="primary" component={ RouterLink }
|
||||
startIcon={ <FileUploadOutline/> } { ...props as any }>
|
||||
{ children }
|
||||
</Button>
|
||||
|
||||
const ReviewAction = (props: ButtonProps) => {
|
||||
const [open, setOpen,] = useState<boolean>(false);
|
||||
const report = useSelector<AppState, Report>(state => getInternshipReport(state.report) as Report);
|
||||
|
||||
return <>
|
||||
<Button startIcon={ <FileFind/> }
|
||||
onClick={ () => setOpen(true) }
|
||||
{ ...props as any }>
|
||||
{ t('review') }
|
||||
</Button>
|
||||
{ createPortal(
|
||||
<ReportPreviewDialog report={ report } open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case "awaiting":
|
||||
return <Actions>
|
||||
<ReviewAction/>
|
||||
<FormAction>{ t('send-again') }</FormAction>
|
||||
</Actions>
|
||||
case "accepted":
|
||||
return <Actions>
|
||||
<FormAction variant="outlined" color="secondary">{ t('send-again') }</FormAction>
|
||||
</Actions>
|
||||
case "declined":
|
||||
return <Actions>
|
||||
<FormAction>{ t('send-again') }</FormAction>
|
||||
<ContactButton/>
|
||||
</Actions>
|
||||
case "draft":
|
||||
return <Actions>
|
||||
<FormAction/>
|
||||
</Actions>
|
||||
|
||||
default:
|
||||
return <Actions/>
|
||||
}
|
||||
}
|
||||
|
||||
export const ReportComment = (props: HTMLProps<HTMLDivElement>) => {
|
||||
const { comment, declined } = useSelector<AppState, SubmissionState>(state => state.report);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
|
||||
<AlertTitle>{ t('comments') }</AlertTitle>
|
||||
<div dangerouslySetInnerHTML={{ __html: comment }}/>
|
||||
</Alert> : null
|
||||
}
|
||||
|
||||
export const ReportStep = (props: StepProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submission = useSelector<AppState, SubmissionState>(state => state.report);
|
||||
const evaluation = useSelector<AppState, InternshipDocument | null>(state => state.report.evaluation);
|
||||
const spacing = useSpacing(2);
|
||||
const edition = useCurrentEdition();
|
||||
|
||||
const status = getSubmissionStatus(submission);
|
||||
const deadlines = useDeadlines();
|
||||
|
||||
const { sent, declined, comment } = submission;
|
||||
|
||||
return <Step { ...props }
|
||||
label={ t('steps.report.header') }
|
||||
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
|
||||
until={ deadlines.report }
|
||||
notBefore={ edition?.reportingStart }
|
||||
state={ <Status submission={ submission }/> }>
|
||||
<div className={ spacing.vertical }>
|
||||
<p>{ t(`steps.report.info.${ status }`) }</p>
|
||||
|
||||
<ReportComment />
|
||||
{ evaluation && <FileInfo document={ evaluation } /> }
|
||||
<ReportActions/>
|
||||
</div>
|
||||
</Step>;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { Report, ReportSchema } from "@/data/report";
|
||||
import { Stateful } from "@/data";
|
||||
|
||||
const choices = [1, 2, 3, 4, 5].map(n => ({
|
||||
pl: `Wybór ${n}`,
|
||||
en: `Choice ${n}`
|
||||
}))
|
||||
|
||||
export const sampleReportSchema: ReportSchema = [
|
||||
{
|
||||
type: "short-text",
|
||||
id: "short",
|
||||
description: {
|
||||
en: "Text field, with <strong>HTML</strong> description",
|
||||
pl: "Pole tekstowe, z opisem w formacie <strong>HTML</strong>"
|
||||
},
|
||||
label: {
|
||||
en: "Text Field",
|
||||
pl: "Pole tekstowe",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "long-text",
|
||||
id: "long",
|
||||
description: {
|
||||
en: "Long text field, with <strong>HTML</strong> description",
|
||||
pl: "Długie pole tekstowe, z opisem w formacie <strong>HTML</strong>"
|
||||
},
|
||||
label: {
|
||||
en: "Long Text Field",
|
||||
pl: "Długie Pole tekstowe",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
id: "radio",
|
||||
description: {
|
||||
en: "single choice field, with <strong>HTML</strong> description",
|
||||
pl: "Pole jednokrotnego wyboru, z opisem w formacie <strong>HTML</strong>"
|
||||
},
|
||||
choices,
|
||||
label: {
|
||||
en: "Single choice field",
|
||||
pl: "Pole jednokrotnego wyboru",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
id: "select",
|
||||
description: {
|
||||
en: "select field, with <strong>HTML</strong> description",
|
||||
pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie <strong>HTML</strong>"
|
||||
},
|
||||
choices,
|
||||
label: {
|
||||
en: "Select field",
|
||||
pl: "Pole jednokrotnego wyboru (selectbox)",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
id: "multi",
|
||||
description: {
|
||||
en: "Multiple choice field, with <strong>HTML</strong> description",
|
||||
pl: "Pole wielokrotnego wyboru, z opisem w formacie <strong>HTML</strong>"
|
||||
},
|
||||
choices,
|
||||
label: {
|
||||
en: "Multi choice field",
|
||||
pl: "Pole wielokrotnego wyboru",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const emptyReport: Report = {
|
||||
fields: {
|
||||
"field_short": "Testowa wartość",
|
||||
"field_select": choices[0],
|
||||
"field_multi": [ choices[1], choices[2] ],
|
||||
},
|
||||
comment: "",
|
||||
state: "draft",
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { ReactComponentElement } from "react";
|
||||
import { MainPage } from "@/pages/main";
|
||||
import { RouteProps, Switch, Route as RouteComponent } from "react-router-dom";
|
||||
import { RouteProps } from "react-router-dom";
|
||||
import { InternshipProposalFormPage, InternshipProposalPreviewPage } from "@/pages/internship/proposal";
|
||||
import { FallbackPage } from "@/pages/fallback";
|
||||
import SubmitPlanPage from "@/pages/internship/plan";
|
||||
@ -11,12 +11,10 @@ import { isLoggedInMiddleware, isReadyMiddleware } from "@/middleware";
|
||||
import UserFillPage from "@/pages/user/fill";
|
||||
import UserProfilePage from "@/pages/user/profile";
|
||||
import { managementRoutes } from "@/management/routing";
|
||||
import SubmitReportPage from "@/pages/internship/report";
|
||||
|
||||
export type Route = {
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
content: (props?: any) => ReactComponentElement<any>,
|
||||
content: () => ReactComponentElement<any>,
|
||||
condition?: () => boolean,
|
||||
middlewares?: Middleware<any, any>[],
|
||||
} & RouteProps;
|
||||
@ -46,7 +44,6 @@ export const routes: Route[] = [
|
||||
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
{ name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
{ name: "internship_report", path: "/internship/report", exact: true, content: () => <SubmitReportPage/>, middlewares: [ isReadyMiddleware ] },
|
||||
|
||||
// user
|
||||
{ name: "user_login", path: "/user/login", content: () => <UserLoginPage/> },
|
||||
@ -57,7 +54,7 @@ export const routes: Route[] = [
|
||||
|
||||
// fallback route for 404 pages
|
||||
{ name: "fallback", path: "*", content: () => <FallbackPage/> },
|
||||
].map(route => ({ tags: [], ...route }))
|
||||
]
|
||||
|
||||
const routeNameMap = new Map(routes.filter(({ name }) => !!name).map(({ name, path }) => [name, path instanceof Array ? path[0] : path])) as Map<string, string>
|
||||
|
||||
@ -81,19 +78,3 @@ export const query = (url: string, params: URLParams) => {
|
||||
|
||||
return url + (query.length > 0 ? `?${ query }` : '');
|
||||
}
|
||||
|
||||
export type RoutesProps = {
|
||||
routes: Route[];
|
||||
[prop: string]: any;
|
||||
};
|
||||
|
||||
export function Routes({ routes, ...props }: RoutesProps) {
|
||||
return <Switch>
|
||||
{ routes.map(({ name, content, middlewares = [], ...route }) =>
|
||||
<RouteComponent { ...route } key={ name } render={ () => {
|
||||
const Next = () => processMiddlewares([ ...middlewares, (_, ...props) => content(...props) ], props)
|
||||
return <Next />
|
||||
} } />
|
||||
) }
|
||||
</Switch>
|
||||
}
|
||||
|
@ -14,9 +14,6 @@ export const editionSerializationTransformer: SerializationTransformer<Edition>
|
||||
reportingStart: momentSerializationTransformer.transform(subject.reportingStart),
|
||||
startDate: momentSerializationTransformer.transform(subject.startDate),
|
||||
endDate: momentSerializationTransformer.transform(subject.endDate),
|
||||
schema: subject.schema,
|
||||
types: subject.types,
|
||||
program: subject.program
|
||||
}
|
||||
},
|
||||
reverseTransform(subject: Serializable<Edition>, context?: unknown): Edition {
|
||||
@ -29,9 +26,6 @@ export const editionSerializationTransformer: SerializationTransformer<Edition>
|
||||
reportingStart: momentSerializationTransformer.reverseTransform(subject.reportingStart) as Moment,
|
||||
startDate: momentSerializationTransformer.reverseTransform(subject.startDate) as Moment,
|
||||
endDate: momentSerializationTransformer.reverseTransform(subject.endDate) as Moment,
|
||||
schema: subject.schema as any,
|
||||
types: subject.types,
|
||||
program: subject.program
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
import { identityTransformer, Serializable, Transformer } from "@/serialization/types";
|
||||
import { Report } from "@/data/report";
|
||||
|
||||
export const reportSerializationTransformer: Transformer<Report, Serializable<Report>> = identityTransformer;
|
@ -9,7 +9,6 @@ import { UserAction, UserActions } from "@/state/actions/user";
|
||||
import { ThunkDispatch } from "redux-thunk";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { StudentAction, StudentActions } from "@/state/actions/student";
|
||||
import { InternshipReportAction, InternshipReportActions } from "@/state/actions/report";
|
||||
|
||||
export * from "./base"
|
||||
export * from "./edition"
|
||||
@ -18,7 +17,6 @@ export * from "./proposal"
|
||||
export * from "./plan"
|
||||
export * from "./user"
|
||||
export * from "./student"
|
||||
export * from "./report"
|
||||
|
||||
export type Action
|
||||
= UserAction
|
||||
@ -27,7 +25,6 @@ export type Action
|
||||
| InternshipProposalAction
|
||||
| StudentAction
|
||||
| InternshipPlanAction
|
||||
| InternshipReportAction
|
||||
| InsuranceAction;
|
||||
|
||||
export const Actions = {
|
||||
@ -38,7 +35,6 @@ export const Actions = {
|
||||
...InternshipPlanActions,
|
||||
...InsuranceActions,
|
||||
...StudentActions,
|
||||
...InternshipReportActions,
|
||||
}
|
||||
export type Actions = typeof Actions;
|
||||
export type AppDispatch = ThunkDispatch<AppState, any, Action>;
|
||||
|
@ -33,7 +33,6 @@ export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction
|
||||
export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction<InternshipPlanActions.Receive> {
|
||||
document: InternshipDocument;
|
||||
state: SubmissionState;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface SavePlanAction extends SaveSubmissionAction<InternshipPlanActions.Save> {
|
||||
|
@ -27,9 +27,7 @@ export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAc
|
||||
|
||||
export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction<InternshipProposalActions.Receive> {
|
||||
internship: Internship;
|
||||
state: SubmissionState;
|
||||
comment?: string;
|
||||
grade?: number;
|
||||
state: SubmissionState,
|
||||
}
|
||||
|
||||
export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> {
|
||||
|
@ -1,49 +0,0 @@
|
||||
import { Internship } from "@/data";
|
||||
import {
|
||||
ReceiveSubmissionApproveAction,
|
||||
ReceiveSubmissionDeclineAction,
|
||||
ReceiveSubmissionUpdateAction,
|
||||
SaveSubmissionAction,
|
||||
SendSubmissionAction
|
||||
} from "@/state/actions/submission";
|
||||
import { SubmissionState } from "@/api/dto/internship-registration";
|
||||
import { Report } from "@/data/report";
|
||||
|
||||
export enum InternshipReportActions {
|
||||
Send = "SEND_REPORT",
|
||||
Save = "SAVE_REPORT",
|
||||
Approve = "RECEIVE_REPORT_APPROVE",
|
||||
Decline = "RECEIVE_REPORT_DECLINE",
|
||||
Receive = "RECEIVE_REPORT_STATE",
|
||||
Reset = "RESET_REPORT",
|
||||
}
|
||||
|
||||
export interface SendReportAction extends SendSubmissionAction<InternshipReportActions.Send> {
|
||||
}
|
||||
|
||||
export interface ResetReportAction extends SendSubmissionAction<InternshipReportActions.Reset> {
|
||||
}
|
||||
|
||||
export interface ReceiveReportApproveAction extends ReceiveSubmissionApproveAction<InternshipReportActions.Approve> {
|
||||
}
|
||||
|
||||
export interface ReceiveReportDeclineAction extends ReceiveSubmissionDeclineAction<InternshipReportActions.Decline> {
|
||||
}
|
||||
|
||||
export interface ReceiveReportUpdateAction extends ReceiveSubmissionUpdateAction<InternshipReportActions.Receive> {
|
||||
report: Report;
|
||||
state: SubmissionState,
|
||||
comment: string,
|
||||
}
|
||||
|
||||
export interface SaveReportAction extends SaveSubmissionAction<InternshipReportActions.Save> {
|
||||
report: Report;
|
||||
}
|
||||
|
||||
export type InternshipReportAction
|
||||
= SendReportAction
|
||||
| SaveReportAction
|
||||
| ResetReportAction
|
||||
| ReceiveReportApproveAction
|
||||
| ReceiveReportDeclineAction
|
||||
| ReceiveReportUpdateAction;
|
@ -7,7 +7,6 @@ import internshipProposalReducer from "@/state/reducer/proposal";
|
||||
import internshipPlanReducer from "@/state/reducer/plan";
|
||||
import insuranceReducer from "@/state/reducer/insurance";
|
||||
import userReducer from "@/state/reducer/user";
|
||||
import internshipReportReducer from "@/state/reducer/report";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
student: studentReducer,
|
||||
@ -17,7 +16,6 @@ const rootReducer = combineReducers({
|
||||
plan: internshipPlanReducer,
|
||||
insurance: insuranceReducer,
|
||||
user: userReducer,
|
||||
report: internshipReportReducer,
|
||||
})
|
||||
|
||||
export type AppState = ReturnType<typeof rootReducer>;
|
||||
|
@ -54,7 +54,6 @@ const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPla
|
||||
ApiSubmissionState.Rejected,
|
||||
ApiSubmissionState.Submitted
|
||||
].includes(action.state),
|
||||
comment: action.comment || null,
|
||||
document: action.document,
|
||||
}
|
||||
|
||||
|
@ -15,14 +15,12 @@ import { SubmissionState as ApiSubmissionState } from "@/api/dto/internship-regi
|
||||
|
||||
export type InternshipProposalState = SubmissionState & MayRequireDeanApproval & {
|
||||
proposal: Serializable<Internship> | null;
|
||||
grade: number | null;
|
||||
}
|
||||
|
||||
const defaultInternshipProposalState: InternshipProposalState = {
|
||||
...defaultDeanApprovalsState,
|
||||
...defaultSubmissionState,
|
||||
proposal: null,
|
||||
grade: null,
|
||||
}
|
||||
|
||||
export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
|
||||
@ -60,8 +58,6 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter
|
||||
ApiSubmissionState.Submitted
|
||||
].includes(action.state),
|
||||
proposal: internshipSerializationTransformer.transform(action.internship),
|
||||
comment: action.comment || "",
|
||||
grade: action.grade || null,
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
|
@ -1,72 +0,0 @@
|
||||
import { InternshipReportAction, InternshipReportActions } from "@/state/actions";
|
||||
import { Serializable } from "@/serialization/types";
|
||||
import {
|
||||
createSubmissionReducer,
|
||||
defaultDeanApprovalsState,
|
||||
defaultSubmissionState,
|
||||
SubmissionState
|
||||
} from "@/state/reducer/submission";
|
||||
import { Reducer } from "react";
|
||||
import { SubmissionAction } from "@/state/actions/submission";
|
||||
import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
|
||||
import { Report } from "@/data/report";
|
||||
import { reportSerializationTransformer } from "@/serialization/report";
|
||||
|
||||
export type InternshipReportState = SubmissionState & {
|
||||
report: Serializable<Report> | null;
|
||||
evaluation: InternshipDocument | null;
|
||||
}
|
||||
|
||||
const defaultInternshipReportState: InternshipReportState = {
|
||||
...defaultDeanApprovalsState,
|
||||
...defaultSubmissionState,
|
||||
report: null,
|
||||
evaluation: null,
|
||||
}
|
||||
|
||||
export const getInternshipReport = ({ report }: InternshipReportState): Report | null =>
|
||||
report && reportSerializationTransformer.reverseTransform(report);
|
||||
|
||||
const internshipReportSubmissionReducer: Reducer<InternshipReportState, InternshipReportAction> = createSubmissionReducer({
|
||||
[InternshipReportActions.Approve]: SubmissionAction.Approve,
|
||||
[InternshipReportActions.Decline]: SubmissionAction.Decline,
|
||||
[InternshipReportActions.Receive]: SubmissionAction.Receive,
|
||||
[InternshipReportActions.Save]: SubmissionAction.Save,
|
||||
[InternshipReportActions.Send]: SubmissionAction.Send,
|
||||
})
|
||||
|
||||
const internshipReportReducer = (state: InternshipReportState = defaultInternshipReportState, action: InternshipReportAction): InternshipReportState => {
|
||||
state = internshipReportSubmissionReducer(state, action);
|
||||
|
||||
switch (action.type) {
|
||||
case InternshipReportActions.Reset:
|
||||
return defaultInternshipReportState;
|
||||
case InternshipReportActions.Save:
|
||||
case InternshipReportActions.Send:
|
||||
return {
|
||||
...state,
|
||||
}
|
||||
case InternshipReportActions.Receive:
|
||||
if (state.overwritten) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
accepted: action.state === ApiSubmissionState.Accepted,
|
||||
declined: action.state === ApiSubmissionState.Rejected,
|
||||
sent: [
|
||||
ApiSubmissionState.Accepted,
|
||||
ApiSubmissionState.Rejected,
|
||||
ApiSubmissionState.Submitted
|
||||
].includes(action.state),
|
||||
report: reportSerializationTransformer.transform(action.report),
|
||||
comment: action.comment,
|
||||
evaluation: action.evaluation,
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default internshipReportReducer;
|
@ -11,75 +11,15 @@ actions:
|
||||
preview: Podgląd
|
||||
delete: Usuń
|
||||
edit: Edytuj
|
||||
add: Dodaj
|
||||
manage: Zarządzaj
|
||||
|
||||
internship:
|
||||
grade: Oceń praktykę
|
||||
column:
|
||||
student: Imię i Nazwisko
|
||||
album: Numer Albumu
|
||||
type: Rodzaj praktyki
|
||||
status: Status
|
||||
changed: Data aktualizacji
|
||||
grade: Ocena
|
||||
|
||||
edition:
|
||||
internships:
|
||||
title: Praktyki
|
||||
proposals:
|
||||
title: Zgłoszenia praktyk
|
||||
ipp:
|
||||
title: Indywidualne Plany Praktyk
|
||||
index:
|
||||
title: "Edycje praktyk"
|
||||
reports:
|
||||
title: "Raporty praktyki"
|
||||
dean-approvals:
|
||||
title: "Zgody dziekana"
|
||||
field:
|
||||
id: Identyfikator
|
||||
start: Początek
|
||||
end: Koniec
|
||||
course: Kierunek
|
||||
reportingStart: Początek raportowania
|
||||
reportingEnd: Koniec raportowania
|
||||
proposalDeadline: Termin zgłaszania praktyk
|
||||
minimumInternshipHours: Minimalna liczba godzin
|
||||
fields:
|
||||
basic: "Podstawowe"
|
||||
deadlines: "Terminy"
|
||||
program: "Ramowy program praktyk"
|
||||
types: "Dostępne typy praktyki"
|
||||
manage:
|
||||
management: "Zarządzanie edycją"
|
||||
internships: "Zarządzanie praktykami"
|
||||
settings:
|
||||
title: "Konfiguracja edycji"
|
||||
schema: "Pola formularza raportu praktyki"
|
||||
program:
|
||||
entry: "Punkt ramowego programu praktyki #{{ index }}"
|
||||
field:
|
||||
description: "Opis"
|
||||
|
||||
report-fields:
|
||||
title: "Pola formularza raportu praktyki"
|
||||
|
||||
report-field:
|
||||
preview: Podgląd
|
||||
field:
|
||||
type: "Rodzaj"
|
||||
name: "Unikalny identyfikator"
|
||||
label: "Etykieta"
|
||||
description: "Opis"
|
||||
choices: "Możliwe wybory"
|
||||
choice: "Wybór #{{ index }}"
|
||||
type:
|
||||
select: "Pole wyboru"
|
||||
radio: "Jednokrotny wybór (radio)"
|
||||
checkbox: "Wielokrotny wybór (checkboxy)"
|
||||
short-text: "Pole krótkiej odpowiedzi"
|
||||
long-text: "Pole długiej odpowiedzi"
|
||||
|
||||
type:
|
||||
index:
|
||||
|
@ -111,13 +111,6 @@ forms:
|
||||
fields:
|
||||
key: Klucz dostępu do edycji
|
||||
|
||||
report:
|
||||
report: Raport
|
||||
dropzone-help: Skan karty oceny w formacie PDF
|
||||
instructions: >
|
||||
Poproś swojego opiekuna o wypełnienie karty oceny praktyki - następnie zeskanuj ją i zamieść wynikowy plik poniże.
|
||||
Dodatkowo wypełnij wszystkie pola formularza raportu praktyki w celu sfinalizowania praktyki.
|
||||
|
||||
student:
|
||||
name: imię
|
||||
surname: mazwisko
|
||||
@ -132,7 +125,6 @@ submission:
|
||||
accepted: "zaakceptowano"
|
||||
declined: "do poprawy"
|
||||
draft: "wersja robocza"
|
||||
empty: "brak zgłoszenia"
|
||||
|
||||
internship:
|
||||
validation:
|
||||
@ -226,22 +218,8 @@ steps:
|
||||
download: Twój indywidualny program praktyki
|
||||
report:
|
||||
header: "Raport z praktyki"
|
||||
info:
|
||||
draft: >
|
||||
Po ukończeniu praktyki należy wypełnić z niej raport oraz przesłać ocenę praktyki przygotowaną przez Twojego zakładowego opiekuna praktyki.
|
||||
awaiting: >
|
||||
Twój raport musi zostać zweryfikowany i zatwierdzony. Po weryfikacji zostaniesz poinformowany o
|
||||
akceptacji bądź konieczności wprowadzenia zmian.
|
||||
accepted: >
|
||||
Twój raport został zweryfikowany i zaakceptowany.
|
||||
declined: >
|
||||
Twój raport został zweryfikowany i odrzucony. Popraw zgłoszone uwagi i wyślij raport ponownie. W razie
|
||||
pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku.
|
||||
submit: Uzupełnij raport
|
||||
template: "Szablon karty oceny prakyki"
|
||||
grade:
|
||||
header: "Ocena z praktyki"
|
||||
wait: "W tym miejscu pojawi się ocena z praktyki, po wystawieniu jej przez pełnomocnika praktyk ds. Twojego kierunku"
|
||||
insurance:
|
||||
header: "Ubezpieczenie NNW"
|
||||
instructions: >
|
||||
|
@ -6842,9 +6842,9 @@ md5.js@^1.3.4:
|
||||
safe-buffer "^5.1.2"
|
||||
|
||||
mdi-material-ui@^6.17.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.21.0.tgz#e052215b0534e6c20abeb7e89c3fd8a421a519fd"
|
||||
integrity sha512-rcO7KmaOhZq4H7vHYpwnjMqHfuJh0PmpEJNssEofWaqoSEABmIwRHUNmdJDPrjrBCTUm4m7tpYexqPOYzkb1Eg==
|
||||
version "6.17.0"
|
||||
resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.17.0.tgz#da69f0b7d7c6fc2255e6007ed8b8ca858c1aede7"
|
||||
integrity sha512-eOprRu31lklPIS1WGe3cM0G/8glKl1WKRvewxjDrgXH2Ryxxg7uQ+uwDUwUEONtLku0p2ZOLzgXUIy2uRy5rLg==
|
||||
|
||||
mdn-data@2.0.4:
|
||||
version "2.0.4"
|
||||
|
Loading…
Reference in New Issue
Block a user