Add internship summaries
This commit is contained in:
parent
9dbdde6baa
commit
6be3fd12f9
src
api/dto
data
forms
management
api
edition
main.tsxreport/fields
routing.tsxpages/steps
serialization
translations
@ -1,4 +1,4 @@
|
|||||||
import { Identifiable, InternshipProgramEntry } from "@/data";
|
import { Identifiable, Identifier, InternshipProgramEntry } from "@/data";
|
||||||
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
|
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
|
||||||
import { OneWayTransformer, Transformer } from "@/serialization";
|
import { OneWayTransformer, Transformer } from "@/serialization";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
@ -79,7 +79,7 @@ export interface FieldDefinitionDTO extends Identifiable {
|
|||||||
export const fieldDefinitionDtoTransformer: Transformer<FieldDefinitionDTO, ReportFieldDefinition> = {
|
export const fieldDefinitionDtoTransformer: Transformer<FieldDefinitionDTO, ReportFieldDefinition> = {
|
||||||
transform(dto: FieldDefinitionDTO, context?: unknown): ReportFieldDefinition {
|
transform(dto: FieldDefinitionDTO, context?: unknown): ReportFieldDefinition {
|
||||||
return {
|
return {
|
||||||
...dto,
|
id: dto.id,
|
||||||
choices: (dto.choices || []).map(choice => JSON.parse(choice)),
|
choices: (dto.choices || []).map(choice => JSON.parse(choice)),
|
||||||
description: {
|
description: {
|
||||||
pl: dto.description,
|
pl: dto.description,
|
||||||
@ -94,7 +94,7 @@ export const fieldDefinitionDtoTransformer: Transformer<FieldDefinitionDTO, Repo
|
|||||||
},
|
},
|
||||||
reverseTransform(subject: ReportFieldDefinition, context?: unknown): FieldDefinitionDTO {
|
reverseTransform(subject: ReportFieldDefinition, context?: unknown): FieldDefinitionDTO {
|
||||||
return {
|
return {
|
||||||
...subject,
|
id: subject.id,
|
||||||
choices: "choices" in subject && subject.choices.map(choice => JSON.stringify(choice)) || [],
|
choices: "choices" in subject && subject.choices.map(choice => JSON.stringify(choice)) || [],
|
||||||
description: subject.description.pl,
|
description: subject.description.pl,
|
||||||
descriptionEng: subject.description.en,
|
descriptionEng: subject.description.en,
|
||||||
@ -164,3 +164,28 @@ 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[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -125,6 +125,7 @@ export interface InternshipInfoDTO extends Identifiable {
|
|||||||
documentation: InternshipDocumentDTO[],
|
documentation: InternshipDocumentDTO[],
|
||||||
student: StudentDTO,
|
student: StudentDTO,
|
||||||
report: InternshipReportDTO,
|
report: InternshipReportDTO,
|
||||||
|
grade: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const internshipReportDtoTransformer: OneWayTransformer<InternshipReportDTO, Report> = {
|
export const internshipReportDtoTransformer: OneWayTransformer<InternshipReportDTO, Report> = {
|
||||||
|
@ -26,3 +26,5 @@ export function getMissingStudentData(student: Student): (keyof Student)[] {
|
|||||||
// !!student.course || "course",
|
// !!student.course || "course",
|
||||||
].filter(x => x !== true) as (keyof Student)[];
|
].filter(x => x !== true) as (keyof Student)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fullname = (student: Student) => `${ student.name } ${ student.surname }`;
|
||||||
|
@ -24,6 +24,8 @@ import { Field, Form, Formik, useFormik, useFormikContext } from "formik";
|
|||||||
import { Multilingual } from "@/data";
|
import { Multilingual } from "@/data";
|
||||||
import { Transformer } from "@/serialization";
|
import { Transformer } from "@/serialization";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
|
import { useCurrentEdition } from "@/hooks";
|
||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
|
||||||
export type ReportFieldProps<TField = ReportFieldDefinition> = {
|
export type ReportFieldProps<TField = ReportFieldDefinition> = {
|
||||||
field: TField;
|
field: TField;
|
||||||
@ -115,12 +117,13 @@ const reportFormValuesTransformer: Transformer<Report, ReportFormValues, { repor
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ReportForm() {
|
export default function ReportForm() {
|
||||||
|
const edition = useCurrentEdition() as Edition;
|
||||||
const report = emptyReport;
|
const report = emptyReport;
|
||||||
const schema = sampleReportSchema;
|
const schema = edition.schema;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSubmit = async (values: ReportFormValues) => {
|
const handleSubmit = async (values: ReportFormValues) => {
|
||||||
const result = reportFormValuesTransformer.reverseTransform(values);
|
const result = reportFormValuesTransformer.reverseTransform(values, { report });
|
||||||
await api.report.save(result);
|
await api.report.save(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Course } from "@/data";
|
import { Course } from "@/data";
|
||||||
import { sampleCourse } from "@/provider/dummy";
|
import { sampleCourse } from "@/provider/dummy";
|
||||||
|
import { axios } from "@/api";
|
||||||
|
import { EditionDTO, editionDtoTransformer } from "@/api/dto/edition";
|
||||||
|
|
||||||
|
const COURSE_INDEX_ENDPOINT = "/management/course";
|
||||||
|
|
||||||
export async function all(): Promise<Course[]> {
|
export async function all(): Promise<Course[]> {
|
||||||
return [
|
const response = await axios.get<Course[]>(COURSE_INDEX_ENDPOINT);
|
||||||
sampleCourse,
|
return response.data;
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export async function accept(document: OneOrMany<InternshipDocument>, comment?:
|
|||||||
|
|
||||||
await Promise.all(documents.map(document => axios.put(
|
await Promise.all(documents.map(document => axios.put(
|
||||||
prepare(DOCUMENT_ACCEPT_ENDPOINT, { id: document.id || ""}),
|
prepare(DOCUMENT_ACCEPT_ENDPOINT, { id: document.id || ""}),
|
||||||
JSON.stringify(comment),
|
JSON.stringify(comment || ""),
|
||||||
{ headers: { 'Content-Type': 'application/json' } }
|
{ headers: { 'Content-Type': 'application/json' } }
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { axios } from "@/api";
|
import { axios } from "@/api";
|
||||||
import { EditionDTO, editionDtoTransformer } from "@/api/dto/edition";
|
import { EditionDTO, editionDtoTransformer, editionUpdateDtoTransformer } from "@/api/dto/edition";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import { prepare } from "@/routing";
|
import { prepare } from "@/routing";
|
||||||
|
|
||||||
@ -15,3 +15,12 @@ export async function details(edition: string): Promise<Edition> {
|
|||||||
const response = await axios.get<EditionDTO>(prepare(MANAGEMENT_EDITION_ENDPOINT, { edition }));
|
const response = await axios.get<EditionDTO>(prepare(MANAGEMENT_EDITION_ENDPOINT, { edition }));
|
||||||
return editionDtoTransformer.transform(response.data);
|
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;
|
||||||
|
}
|
||||||
|
@ -24,10 +24,13 @@ export type InternshipSubmission = Nullable<Internship> & {
|
|||||||
changed: Moment | null,
|
changed: Moment | null,
|
||||||
ipp: InternshipDocument | null,
|
ipp: InternshipDocument | null,
|
||||||
report: Report | null,
|
report: Report | null,
|
||||||
|
grade: number | null,
|
||||||
|
approvals: InternshipDocument[],
|
||||||
}
|
}
|
||||||
|
|
||||||
const INTERNSHIP_MANAGEMENT_INDEX_ENDPOINT = "/management/internship";
|
const INTERNSHIP_MANAGEMENT_INDEX_ENDPOINT = "/management/internship";
|
||||||
const INTERNSHIP_MANAGEMENT_ENDPOINT = "/management/internship/:id";
|
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_ACCEPT_ENDPOINT = "/management/internship/:id/registration/accept";
|
||||||
const INTERNSHIP_REJECT_ENDPOINT = "/management/internship/:id/registration/reject";
|
const INTERNSHIP_REJECT_ENDPOINT = "/management/internship/:id/registration/reject";
|
||||||
|
|
||||||
@ -38,6 +41,7 @@ const internshipInfoDtoTransformer: Transformer<InternshipInfoDTO, InternshipSub
|
|||||||
const report = subject.report;
|
const report = subject.report;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...subject,
|
||||||
changed: moment(subject.internshipRegistration.submissionDate),
|
changed: moment(subject.internshipRegistration.submissionDate),
|
||||||
company: subject.internshipRegistration.company,
|
company: subject.internshipRegistration.company,
|
||||||
startDate: moment(subject.internshipRegistration.start),
|
startDate: moment(subject.internshipRegistration.start),
|
||||||
@ -53,7 +57,8 @@ const internshipInfoDtoTransformer: Transformer<InternshipInfoDTO, InternshipSub
|
|||||||
state: submissionStateDtoTransformer.transform(subject.internshipRegistration.state),
|
state: submissionStateDtoTransformer.transform(subject.internshipRegistration.state),
|
||||||
type: subject.internshipRegistration.type && internshipTypeDtoTransformer.transform(subject.internshipRegistration.type),
|
type: subject.internshipRegistration.type && internshipTypeDtoTransformer.transform(subject.internshipRegistration.type),
|
||||||
ipp: ipp && internshipDocumentDtoTransformer.transform(ipp),
|
ipp: ipp && internshipDocumentDtoTransformer.transform(ipp),
|
||||||
report: report && internshipReportDtoTransformer.transform(report)
|
report: report && internshipReportDtoTransformer.transform(report),
|
||||||
|
approvals: (subject.documentation.filter(doc => doc.type === UploadType.DeanConsent).map(subject => internshipDocumentDtoTransformer.transform(subject)))
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
reverseTransform(subject: InternshipSubmission, context: undefined): InternshipInfoDTO {
|
reverseTransform(subject: InternshipSubmission, context: undefined): InternshipInfoDTO {
|
||||||
@ -78,7 +83,7 @@ export async function accept(internship: OneOrMany<Internship>, comment?: string
|
|||||||
|
|
||||||
await Promise.all(internships.map(internship => axios.put(
|
await Promise.all(internships.map(internship => axios.put(
|
||||||
prepare(INTERNSHIP_ACCEPT_ENDPOINT, { id: internship.id || ""}),
|
prepare(INTERNSHIP_ACCEPT_ENDPOINT, { id: internship.id || ""}),
|
||||||
JSON.stringify(comment),
|
JSON.stringify(comment || ""),
|
||||||
{ headers: { 'Content-Type': 'application/json' } }
|
{ headers: { 'Content-Type': 'application/json' } }
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@ -92,3 +97,13 @@ export async function discard(internship: OneOrMany<Internship>, comment: string
|
|||||||
{ headers: { 'Content-Type': 'application/json' } }
|
{ 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' } }
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
68
src/management/edition/common/StepState.tsx
Normal file
68
src/management/edition/common/StepState.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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>
|
||||||
|
}
|
@ -31,7 +31,7 @@ import { AccountCheck, ArrowDown, ArrowUp, ShieldCheck, TrashCan } from "mdi-mat
|
|||||||
import { Actions } from "@/components";
|
import { Actions } from "@/components";
|
||||||
import { Add } from "@material-ui/icons";
|
import { Add } from "@material-ui/icons";
|
||||||
|
|
||||||
export type EditionFormValues = Nullable<Edition>;
|
export type EditionFormValues = Omit<Nullable<Edition>, "schema">;
|
||||||
|
|
||||||
export const initialEditionFormValues: EditionFormValues = {
|
export const initialEditionFormValues: EditionFormValues = {
|
||||||
course: null,
|
course: null,
|
||||||
@ -47,7 +47,7 @@ export const initialEditionFormValues: EditionFormValues = {
|
|||||||
|
|
||||||
export const editionFormValuesTransformer: Transformer<Edition, EditionFormValues> = identityTransformer;
|
export const editionFormValuesTransformer: Transformer<Edition, EditionFormValues> = identityTransformer;
|
||||||
|
|
||||||
function toggleValueInArray<T extends Identifiable>(array: T[], value: T, comparator: (a: T, b: T) => boolean = (a, b) => a == b): T[] {
|
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
|
return array.findIndex(other => comparator(other, value)) === -1
|
||||||
? [ ...array, value ]
|
? [ ...array, value ]
|
||||||
: array.filter(other => !comparator(other, value));
|
: array.filter(other => !comparator(other, value));
|
||||||
@ -121,7 +121,7 @@ export const CoursePickerField = ({ field, form, meta, ...props }: FieldProps<Co
|
|||||||
renderInput={ props => <TextField { ...props } label={ t("edition.field.course") } fullWidth/> }
|
renderInput={ props => <TextField { ...props } label={ t("edition.field.course") } fullWidth/> }
|
||||||
getOptionLabel={ course => course.name }
|
getOptionLabel={ course => course.name }
|
||||||
value={ field.value }
|
value={ field.value }
|
||||||
onChange={ field.onChange }
|
onChange={ (_, value) => form.setFieldValue(field.name, value, false) }
|
||||||
onBlur={ field.onBlur }
|
onBlur={ field.onBlur }
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import React, { useCallback } from "react";
|
|
||||||
import { Link as RouterLink, useRouteMatch } from "react-router-dom";
|
|
||||||
import api from "@/management/api";
|
|
||||||
import { Page } from "@/pages/base";
|
|
||||||
import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
|
||||||
import { Container, Link, Typography } from "@material-ui/core";
|
|
||||||
import { Async } from "@/components/async";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useAsync } from "@/hooks";
|
|
||||||
import { Student } from "@/data";
|
|
||||||
import { route } from "@/routing";
|
|
||||||
import { ProposalPreview } from "@/components/proposalPreview";
|
|
||||||
import { AcceptanceActions } from "@/components/acceptance-action";
|
|
||||||
import { useSpacing } from "@/styles";
|
|
||||||
|
|
||||||
const fullname = (student: Student) => `${student.name} ${student.surname}`;
|
|
||||||
|
|
||||||
export const InternshipDetails = ({ edition }: EditionManagementProps) => {
|
|
||||||
const { params } = useRouteMatch();
|
|
||||||
const internship = useAsync(useCallback(() => api.internship.get(params.internship), [ params.internship ]));
|
|
||||||
const { t } = useTranslation("management");
|
|
||||||
const spacing = useSpacing(2);
|
|
||||||
|
|
||||||
return <Async async={ internship }>
|
|
||||||
{ internship => <Page>
|
|
||||||
<Page.Header maxWidth="lg">
|
|
||||||
<EditionManagement.Breadcrumbs>
|
|
||||||
<Link to={ route("management:edition_internships", { edition: edition.id || "" }) } component={ RouterLink }>{ t("edition.internships.title") }</Link>
|
|
||||||
<Typography color="textPrimary">{ fullname(internship.intern) }</Typography>
|
|
||||||
</EditionManagement.Breadcrumbs>
|
|
||||||
<Page.Title>{ fullname(internship.intern) }</Page.Title>
|
|
||||||
</Page.Header>
|
|
||||||
<Container maxWidth="lg" className={ spacing.vertical }>
|
|
||||||
<ProposalPreview proposal={ internship } />
|
|
||||||
<AcceptanceActions onAccept={ () => {} } onDiscard={ () => {} } label="internship" />
|
|
||||||
</Container>
|
|
||||||
</Page> }
|
|
||||||
</Async>
|
|
||||||
}
|
|
43
src/management/edition/internship/grade.tsx
Normal file
43
src/management/edition/internship/grade.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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,29 +1,158 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsync, useAsyncState } from "@/hooks";
|
import { useAsyncState } from "@/hooks";
|
||||||
import { useSpacing } from "@/styles";
|
import { useSpacing } from "@/styles";
|
||||||
import api from "@/management/api";
|
import api from "@/management/api";
|
||||||
import { Box, Button, Container, IconButton, Tooltip, Typography } from "@material-ui/core";
|
import { Box, Button, Container, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tooltip, Typography } from "@material-ui/core";
|
||||||
import MaterialTable, { Column } from "material-table";
|
import MaterialTable, { Column } from "material-table";
|
||||||
import { actionsColumn } from "@/management/common/helpers";
|
import { actionsColumn } from "@/management/common/helpers";
|
||||||
import { FileFind, Refresh, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
|
import {
|
||||||
|
Account,
|
||||||
|
BriefcaseAccount,
|
||||||
|
BriefcaseAccountOutline, CertificateOutline,
|
||||||
|
FileChartOutline,
|
||||||
|
FileFind,
|
||||||
|
FormatPageBreak,
|
||||||
|
Refresh,
|
||||||
|
Star,
|
||||||
|
StickerCheckOutline
|
||||||
|
} from "mdi-material-ui";
|
||||||
import { Page } from "@/pages/base";
|
import { Page } from "@/pages/base";
|
||||||
import { Actions } from "@/components";
|
import { Actions } from "@/components";
|
||||||
import { BulkActions } from "@/management/common/BulkActions";
|
import { BulkActions } from "@/management/common/BulkActions";
|
||||||
import { Async } from "@/components/async";
|
import { Async } from "@/components/async";
|
||||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||||
import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
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 { InternshipSubmission } from "@/management/api/internship";
|
||||||
import { canAccept, canDiscard, StateLabel } from "@/management/edition/internship/common";
|
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Internship } from "@/data";
|
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";
|
||||||
|
|
||||||
const title = "edition.internships.title";
|
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 <>
|
||||||
|
<div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children || <FileFind /> }</div>
|
||||||
|
{ 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 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.report?.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>
|
||||||
|
<StepState state={ reportState } label={ t("steps.report.header") } icon={ <FileChartOutline /> } />
|
||||||
|
<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) => {
|
export const InternshipManagement = ({ edition }: EditionManagementProps) => {
|
||||||
const { t } = useTranslation("management");
|
const { t } = useTranslation("management");
|
||||||
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
|
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
|
||||||
@ -36,47 +165,26 @@ export const InternshipManagement = ({ edition }: EditionManagementProps) => {
|
|||||||
|
|
||||||
useEffect(updateInternshipList, []);
|
useEffect(updateInternshipList, []);
|
||||||
|
|
||||||
const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
|
const GradeAction = ({ internship }: { internship: InternshipSubmission }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleSubmissionAccept = async (comment?: string) => {
|
const handleGradeSubmission = async (grade: number) => {
|
||||||
|
await api.internship.grade(internship as Internship, grade);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
await api.internship.accept(internship as Internship, comment);
|
|
||||||
updateInternshipList();
|
updateInternshipList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Tooltip title={ t("translation:accept") as any }><IconButton onClick={ () => setOpen(true) }><StickerCheckOutline /></IconButton></Tooltip>
|
<Tooltip title={ t("internship.grade") as string }>
|
||||||
|
<IconButton onClick={ () => setOpen(true) }><StickerCheckOutline/></IconButton>
|
||||||
|
</Tooltip>
|
||||||
{ createPortal(
|
{ createPortal(
|
||||||
<AcceptSubmissionDialog onAccept={ handleSubmissionAccept } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
|
<GradeDialog onSubmit={ handleGradeSubmission } internship={ internship } open={ open } onClose={ () => setOpen(false) }/>,
|
||||||
document.getElementById("modals") as Element,
|
document.getElementById("modals") as Element,
|
||||||
) }
|
) }
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiscardAction = ({ internship }: { internship: InternshipSubmission }) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmissionDiscard = async (comment: string) => {
|
|
||||||
setOpen(false);
|
|
||||||
await api.internship.discard(internship as Internship, comment);
|
|
||||||
updateInternshipList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<Tooltip title={ t("translation:discard") as any }><IconButton onClick={ () => setOpen(true) }><StickerRemoveOutline /></IconButton></Tooltip>
|
|
||||||
{ createPortal(
|
|
||||||
<DiscardSubmissionDialog onDiscard={ handleSubmissionDiscard } label="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>[] = [
|
const columns: Column<InternshipSubmission>[] = [
|
||||||
{
|
{
|
||||||
title: t("internship.column.student"),
|
title: t("internship.column.student"),
|
||||||
@ -91,17 +199,16 @@ export const InternshipManagement = ({ edition }: EditionManagementProps) => {
|
|||||||
field: "type.label.pl",
|
field: "type.label.pl",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("internship.column.changed"),
|
title: t("internship.column.status"),
|
||||||
render: summary => summary.changed?.format("yyyy-MM-DD")
|
render: summary => <InternshipState internship={ summary } />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("internship.column.status"),
|
title: t("internship.column.grade"),
|
||||||
render: summary => <StateLabel state={ summary.state } />
|
field: "grade",
|
||||||
|
width: 0,
|
||||||
},
|
},
|
||||||
actionsColumn(internship => <>
|
actionsColumn(internship => <>
|
||||||
{ canAccept(internship) && <AcceptAction internship={ internship } /> }
|
{ canGrade(internship) && <GradeAction internship={ internship } /> }
|
||||||
{ canDiscard(internship) && <DiscardAction internship={ internship } /> }
|
|
||||||
<IconButton component={ RouterLink } to={ route("management:edition_internship", { edition: edition.id || "", internship: internship.id || "" }) }><FileFind /></IconButton>
|
|
||||||
</>)
|
</>)
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -126,7 +233,6 @@ export const InternshipManagement = ({ edition }: EditionManagementProps) => {
|
|||||||
data={ internships }
|
data={ internships }
|
||||||
onSelectionChange={ internships => setSelected(internships) }
|
onSelectionChange={ internships => setSelected(internships) }
|
||||||
options={ { selection: true, pageSize: 10 } }
|
options={ { selection: true, pageSize: 10 } }
|
||||||
detailPanel={ summary => <InternshipDetails summary={ summary } /> }
|
|
||||||
/>
|
/>
|
||||||
}</Async>
|
}</Async>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -18,7 +18,7 @@ import { route } from "@/routing";
|
|||||||
import { AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
|
import { AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
|
||||||
import { ProposalPreview } from "@/components/proposalPreview";
|
import { ProposalPreview } from "@/components/proposalPreview";
|
||||||
import { InternshipSubmission } from "@/management/api/internship";
|
import { InternshipSubmission } from "@/management/api/internship";
|
||||||
import { StateLabel } from "@/management/edition/internship/common";
|
import { StateLabel } from "@/management/edition/proposal/common";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Internship } from "@/data";
|
import { Internship } from "@/data";
|
||||||
import { FileInfo } from "@/components/fileinfo";
|
import { FileInfo } from "@/components/fileinfo";
|
||||||
|
@ -5,7 +5,16 @@ import { Page } from "@/pages/base";
|
|||||||
import { Container, Link, Paper, Typography } from "@material-ui/core";
|
import { Container, Link, Paper, Typography } from "@material-ui/core";
|
||||||
import { Management, ManagementLink } from "@/management/main";
|
import { Management, ManagementLink } from "@/management/main";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import { AccountMultiple, BriefcaseAccount, CertificateOutline, CogOutline, FileChartOutline, FormatPageBreak } from "mdi-material-ui";
|
import {
|
||||||
|
AccountMultiple,
|
||||||
|
BriefcaseAccount,
|
||||||
|
CertificateOutline,
|
||||||
|
CogOutline,
|
||||||
|
FileAccountOutline,
|
||||||
|
FileChartOutline,
|
||||||
|
FileQuestionOutline,
|
||||||
|
FormatPageBreak
|
||||||
|
} from "mdi-material-ui";
|
||||||
import { route, routes, Routes } from "@/routing";
|
import { route, routes, Routes } from "@/routing";
|
||||||
import { useSpacing } from "@/styles";
|
import { useSpacing } from "@/styles";
|
||||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||||
@ -31,7 +40,6 @@ export const EditionContext = React.createContext<Edition | null>(null);
|
|||||||
|
|
||||||
export const EditionManagement = ({ edition }: EditionManagementProps) => {
|
export const EditionManagement = ({ edition }: EditionManagementProps) => {
|
||||||
const { t } = useTranslation("management");
|
const { t } = useTranslation("management");
|
||||||
const { params } = useRouteMatch();
|
|
||||||
|
|
||||||
const spacing = useSpacing(2);
|
const spacing = useSpacing(2);
|
||||||
const classes = useSectionStyles();
|
const classes = useSectionStyles();
|
||||||
@ -50,6 +58,9 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
|
|||||||
<ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_internships", { edition: edition.id || "" }) }>
|
<ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_internships", { edition: edition.id || "" }) }>
|
||||||
{ t("management:edition.internships.title") }
|
{ t("management:edition.internships.title") }
|
||||||
</ManagementLink>
|
</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 || "" }) }>
|
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_ipp_index", { edition: edition.id || "" }) }>
|
||||||
{ t("management:edition.ipp.title") }
|
{ t("management:edition.ipp.title") }
|
||||||
</ManagementLink>
|
</ManagementLink>
|
||||||
@ -64,8 +75,8 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
|
|||||||
<Paper elevation={ 2 }>
|
<Paper elevation={ 2 }>
|
||||||
<Typography className={ classes.header }>{ t("edition.manage.management") }</Typography>
|
<Typography className={ classes.header }>{ t("edition.manage.management") }</Typography>
|
||||||
<Management.Menu>
|
<Management.Menu>
|
||||||
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
|
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_schema", { edition: edition.id || "" }) }>
|
||||||
{ t("management:edition.report-fields.title") }
|
{ t("management:edition.settings.schema") }
|
||||||
</ManagementLink>
|
</ManagementLink>
|
||||||
<ManagementLink icon={ <CogOutline/> } route={ route("management:edition_settings", { edition: edition.id || "" }) }>
|
<ManagementLink icon={ <CogOutline/> } route={ route("management:edition_settings", { edition: edition.id || "" }) }>
|
||||||
{ t("management:edition.settings.title") }
|
{ t("management:edition.settings.title") }
|
||||||
|
@ -6,11 +6,12 @@ import { Chip } from "@material-ui/core";
|
|||||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||||
import { green, orange, red } from "@material-ui/core/colors";
|
import { green, orange, red } from "@material-ui/core/colors";
|
||||||
import { InternshipSubmission } from "@/management/api/internship";
|
import { InternshipSubmission } from "@/management/api/internship";
|
||||||
|
import { HourglassEmptyRounded } from "@material-ui/icons";
|
||||||
|
|
||||||
const useStateLabelStyles = makeStyles((theme: Theme) => createStyles<SubmissionStatus, {}>({
|
const useStateLabelStyles = makeStyles((theme: Theme) => createStyles<SubmissionStatus, {}>({
|
||||||
awaiting: {
|
awaiting: {
|
||||||
borderColor: orange["800"],
|
borderColor: theme.palette.primary.main,
|
||||||
color: orange["800"],
|
color: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
declined: {
|
declined: {
|
||||||
borderColor: red["600"],
|
borderColor: red["600"],
|
||||||
@ -29,20 +30,21 @@ export type StateLabelProps = {
|
|||||||
|
|
||||||
export const isValidState = (state: string | null) => ["accepted", "draft", "awaiting", "declined"].includes(state as string)
|
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) => {
|
export const StateLabel = ({ state }: StateLabelProps) => {
|
||||||
const icons: { [sate in SubmissionStatus]: React.ReactElement } = {
|
|
||||||
accepted: <NotebookCheckOutline/>,
|
|
||||||
awaiting: <ClockOutline/>,
|
|
||||||
declined: <NotebookRemoveOutline/>,
|
|
||||||
draft: <NotebookEditOutline/>
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = useStateLabelStyles();
|
const classes = useStateLabelStyles();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return isValidState(state)
|
return isValidState(state)
|
||||||
? <Chip icon={ icons[state as SubmissionStatus] } label={ t(`translation:submission.status.${ state }`) } variant="outlined" className={ classes[state as SubmissionStatus] }/>
|
? <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"/>
|
: <Chip icon={ <FileQuestion /> } label={ t(`translation:submission.status.empty`) } variant="outlined"/>
|
||||||
}
|
}
|
||||||
|
|
35
src/management/edition/proposal/details.tsx
Normal file
35
src/management/edition/proposal/details.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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>
|
||||||
|
}
|
162
src/management/edition/proposal/list.tsx
Normal file
162
src/management/edition/proposal/list.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
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>
|
||||||
|
}
|
60
src/management/edition/report-schema.tsx
Normal file
60
src/management/edition/report-schema.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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>;
|
||||||
|
}
|
@ -13,17 +13,11 @@ import { BulkActions } from "@/management/common/BulkActions";
|
|||||||
import { Async } from "@/components/async";
|
import { Async } from "@/components/async";
|
||||||
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
|
||||||
import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
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 { AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
|
||||||
import { ProposalPreview } from "@/components/proposalPreview";
|
|
||||||
import { InternshipSubmission } from "@/management/api/internship";
|
import { InternshipSubmission } from "@/management/api/internship";
|
||||||
import { StateLabel } from "@/management/edition/internship/common";
|
import { StateLabel } from "@/management/edition/proposal/common";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Internship, Stateful } from "@/data";
|
import { Stateful } from "@/data";
|
||||||
import { FileInfo } from "@/components/fileinfo";
|
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
|
||||||
import { Report } from "@/data/report";
|
import { Report } from "@/data/report";
|
||||||
|
|
||||||
const title = "edition.reports.title";
|
const title = "edition.reports.title";
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { Page } from "@/pages/base";
|
import { Page } from "@/pages/base";
|
||||||
import { Management } from "@/management/main";
|
|
||||||
import { Container, Divider, Typography, Button } from "@material-ui/core";
|
import { Container, Divider, Typography, Button } from "@material-ui/core";
|
||||||
import { Async } from "@/components/async";
|
import { Async } from "@/components/async";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouteMatch } from "react-router-dom";
|
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||||
import { useAsync } from "@/hooks";
|
import { useAsync } from "@/hooks";
|
||||||
import { Edition } from "@/data/edition";
|
import { Edition } from "@/data/edition";
|
||||||
import api from "@/management/api";
|
import api from "@/management/api";
|
||||||
import { Form, Formik } from "formik";
|
import { Form, Formik } from "formik";
|
||||||
import { useSpacing } from "@/styles";
|
import { useSpacing } from "@/styles";
|
||||||
import { EditionForm } from "@/management/edition/form";
|
import { EditionForm, EditionFormValues, editionFormValuesTransformer } from "@/management/edition/form";
|
||||||
import { Actions } from "@/components";
|
import { Actions } from "@/components";
|
||||||
import { Save } from "@material-ui/icons";
|
import { Save } from "@material-ui/icons";
|
||||||
import { Cancel } from "mdi-material-ui";
|
import { Cancel } from "mdi-material-ui";
|
||||||
@ -21,11 +20,21 @@ const title = "edition.settings.title";
|
|||||||
export function EditionSettings() {
|
export function EditionSettings() {
|
||||||
const { t } = useTranslation("management");
|
const { t } = useTranslation("management");
|
||||||
const { params } = useRouteMatch();
|
const { params } = useRouteMatch();
|
||||||
|
const history = useHistory();
|
||||||
const spacing = useSpacing(2);
|
const spacing = useSpacing(2);
|
||||||
|
|
||||||
const edition = useAsync<Edition>(useCallback(() => api.edition.details(params.edition), [params.edition]))
|
const edition = useAsync<Edition>(useCallback(() => api.edition.details(params.edition), [params.edition]))
|
||||||
|
|
||||||
const handleSubmit = () => {};
|
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.id as string })
|
||||||
|
};
|
||||||
|
|
||||||
return <Page>
|
return <Page>
|
||||||
<Page.Header maxWidth="md">
|
<Page.Header maxWidth="md">
|
||||||
|
@ -47,7 +47,7 @@ export const ManagementIndex = () => {
|
|||||||
{ t("management:type.index.title") }
|
{ t("management:type.index.title") }
|
||||||
</ManagementLink>
|
</ManagementLink>
|
||||||
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:report_fields") }>
|
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:report_fields") }>
|
||||||
{ t("management:edition.report-fields.title") }
|
{ t("management:report-fields.title") }
|
||||||
</ManagementLink>
|
</ManagementLink>
|
||||||
<ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }>
|
<ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }>
|
||||||
{ t("management:page.index.title") }
|
{ t("management:page.index.title") }
|
||||||
|
@ -20,7 +20,7 @@ import { Actions } from "@/components";
|
|||||||
import { Refresh } from "mdi-material-ui";
|
import { Refresh } from "mdi-material-ui";
|
||||||
import { useSpacing } from "@/styles";
|
import { useSpacing } from "@/styles";
|
||||||
|
|
||||||
const title = "edition.report-fields.title";
|
const title = "report-fields.title";
|
||||||
|
|
||||||
export const FieldPreview = ({ field }: { field: ReportFieldDefinition }) => {
|
export const FieldPreview = ({ field }: { field: ReportFieldDefinition }) => {
|
||||||
return <Formik initialValues={{}} onSubmit={() => {}}>
|
return <Formik initialValues={{}} onSubmit={() => {}}>
|
||||||
|
@ -7,11 +7,12 @@ import StaticPageManagement from "@/management/page/list";
|
|||||||
import { InternshipTypeManagement } from "@/management/type/list";
|
import { InternshipTypeManagement } from "@/management/type/list";
|
||||||
import { EditionRouter, EditionManagement } from "@/management/edition/manage";
|
import { EditionRouter, EditionManagement } from "@/management/edition/manage";
|
||||||
import { EditionSettings } from "@/management/edition/settings";
|
import { EditionSettings } from "@/management/edition/settings";
|
||||||
import { InternshipManagement } from "@/management/edition/internship/list";
|
import { ProposalManagement } from "@/management/edition/proposal/list";
|
||||||
import { InternshipDetails } from "@/management/edition/internship/details";
|
|
||||||
import { PlanManagement } from "@/management/edition/ipp/list";
|
import { PlanManagement } from "@/management/edition/ipp/list";
|
||||||
import { ReportFields } from "@/management/report/fields/list";
|
import { ReportFields } from "@/management/report/fields/list";
|
||||||
import { ReportManagement } from "@/management/edition/report/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[] = ([
|
export const managementRoutes: Route[] = ([
|
||||||
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
{ name: "index", path: "/", content: ManagementIndex, exact: true },
|
||||||
@ -19,9 +20,10 @@ export const managementRoutes: Route[] = ([
|
|||||||
{ name: "edition_router", path: "/editions/:edition", content: EditionRouter },
|
{ name: "edition_router", path: "/editions/:edition", content: EditionRouter },
|
||||||
{ name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] },
|
{ 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_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"], exact: true },
|
||||||
{ name: "edition_internship", path: "/editions/:edition/internships/:internship", content: InternshipDetails, tags: ["edition"] },
|
|
||||||
{ name: "edition_internships", path: "/editions/:edition/internships", content: InternshipManagement, tags: ["edition"] },
|
{ 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_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: "edition_ipp_index", path: "/editions/:edition/ipp", content: PlanManagement, tags: ["edition"] },
|
||||||
{ name: "editions", path: "/editions", content: EditionsManagement },
|
{ name: "editions", path: "/editions", content: EditionsManagement },
|
||||||
|
|
||||||
|
@ -12,25 +12,53 @@ import { Alert, AlertTitle } from "@material-ui/lab";
|
|||||||
import { ContactButton, Status } from "@/pages/steps/common";
|
import { ContactButton, Status } from "@/pages/steps/common";
|
||||||
import { useCurrentEdition, useDeadlines } from "@/hooks";
|
import { useCurrentEdition, useDeadlines } from "@/hooks";
|
||||||
import { useSpacing } from "@/styles";
|
import { useSpacing } from "@/styles";
|
||||||
import { Report } from "@/data/report";
|
import { MultiChoiceValue, Report, ReportSchema, SingleChoiceValue, TextFieldValue } from "@/data/report";
|
||||||
import { sampleReportSchema } from "@/provider/dummy/report";
|
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { getInternshipReport } from "@/state/reducer/report";
|
import { getInternshipReport } from "@/state/reducer/report";
|
||||||
|
import { Edition } from "@/data/edition";
|
||||||
|
|
||||||
|
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>{ value as TextFieldValue }</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Typography variant="subtitle2">{ field.label.pl }</Typography>
|
||||||
|
{ value ? <Value/> : t("no-value") }
|
||||||
|
</>
|
||||||
|
}) }</>
|
||||||
|
}
|
||||||
|
|
||||||
export type ReportPreviewDialogProps = {
|
export type ReportPreviewDialogProps = {
|
||||||
report: Report;
|
report: Report;
|
||||||
} & DialogProps;
|
} & DialogProps;
|
||||||
|
|
||||||
export const ReportPreviewDialog = ({ report, ...props }: ReportPreviewDialogProps) => {
|
export const ReportPreviewDialog = ({ report, ...props }: ReportPreviewDialogProps) => {
|
||||||
const schema = sampleReportSchema;
|
const edition = useCurrentEdition() as Edition;
|
||||||
|
const schema = edition.schema || [];
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <Dialog { ...props } maxWidth="md">
|
return <Dialog { ...props } maxWidth="md" fullWidth>
|
||||||
<DialogTitle>{ t("steps.report.preview") }</DialogTitle>
|
<DialogTitle>{ t("steps.report.header") }</DialogTitle>
|
||||||
<DialogContent>{ schema.map(field => <>
|
<DialogContent><ReportPreview schema={ schema } report={ report }/></DialogContent>
|
||||||
<Typography variant="subtitle2">{ field.label.pl }</Typography>
|
|
||||||
{ JSON.stringify(report.fields[`field_${field.id}`]) }
|
|
||||||
</> )}</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +68,8 @@ const ReportActions = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const FormAction = ({ children = t('steps.report.submit'), ...props }: ButtonProps) =>
|
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 }>
|
<Button to={ route("internship_report") } variant="contained" color="primary" component={ RouterLink }
|
||||||
|
startIcon={ <FileUploadOutline/> } { ...props as any }>
|
||||||
{ children }
|
{ children }
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@ -64,7 +93,8 @@ const ReportActions = () => {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case "awaiting":
|
case "awaiting":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<ReviewAction />
|
<ReviewAction/>
|
||||||
|
<FormAction>{ t('send-again') }</FormAction>
|
||||||
</Actions>
|
</Actions>
|
||||||
case "accepted":
|
case "accepted":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
@ -77,7 +107,7 @@ const ReportActions = () => {
|
|||||||
</Actions>
|
</Actions>
|
||||||
case "draft":
|
case "draft":
|
||||||
return <Actions>
|
return <Actions>
|
||||||
<FormAction />
|
<FormAction/>
|
||||||
</Actions>
|
</Actions>
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -112,7 +142,7 @@ export const ReportStep = (props: StepProps) => {
|
|||||||
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
|
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
|
||||||
until={ deadlines.report }
|
until={ deadlines.report }
|
||||||
notBefore={ edition?.reportingStart }
|
notBefore={ edition?.reportingStart }
|
||||||
state={ <Status submission={ submission } /> }>
|
state={ <Status submission={ submission }/> }>
|
||||||
<div className={ spacing.vertical }>
|
<div className={ spacing.vertical }>
|
||||||
<p>{ t(`steps.report.info.${ status }`) }</p>
|
<p>{ t(`steps.report.info.${ status }`) }</p>
|
||||||
|
|
||||||
|
@ -14,6 +14,9 @@ export const editionSerializationTransformer: SerializationTransformer<Edition>
|
|||||||
reportingStart: momentSerializationTransformer.transform(subject.reportingStart),
|
reportingStart: momentSerializationTransformer.transform(subject.reportingStart),
|
||||||
startDate: momentSerializationTransformer.transform(subject.startDate),
|
startDate: momentSerializationTransformer.transform(subject.startDate),
|
||||||
endDate: momentSerializationTransformer.transform(subject.endDate),
|
endDate: momentSerializationTransformer.transform(subject.endDate),
|
||||||
|
schema: subject.schema,
|
||||||
|
types: subject.types,
|
||||||
|
program: subject.program
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reverseTransform(subject: Serializable<Edition>, context?: unknown): Edition {
|
reverseTransform(subject: Serializable<Edition>, context?: unknown): Edition {
|
||||||
@ -26,6 +29,9 @@ export const editionSerializationTransformer: SerializationTransformer<Edition>
|
|||||||
reportingStart: momentSerializationTransformer.reverseTransform(subject.reportingStart) as Moment,
|
reportingStart: momentSerializationTransformer.reverseTransform(subject.reportingStart) as Moment,
|
||||||
startDate: momentSerializationTransformer.reverseTransform(subject.startDate) as Moment,
|
startDate: momentSerializationTransformer.reverseTransform(subject.startDate) as Moment,
|
||||||
endDate: momentSerializationTransformer.reverseTransform(subject.endDate) as Moment,
|
endDate: momentSerializationTransformer.reverseTransform(subject.endDate) as Moment,
|
||||||
|
schema: subject.schema as any,
|
||||||
|
types: subject.types,
|
||||||
|
program: subject.program
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -12,17 +12,22 @@ actions:
|
|||||||
delete: Usuń
|
delete: Usuń
|
||||||
edit: Edytuj
|
edit: Edytuj
|
||||||
add: Dodaj
|
add: Dodaj
|
||||||
|
manage: Zarządzaj
|
||||||
|
|
||||||
internship:
|
internship:
|
||||||
|
grade: Oceń praktykę
|
||||||
column:
|
column:
|
||||||
student: Imię i Nazwisko
|
student: Imię i Nazwisko
|
||||||
album: Numer Albumu
|
album: Numer Albumu
|
||||||
type: Rodzaj praktyki
|
type: Rodzaj praktyki
|
||||||
status: Status
|
status: Status
|
||||||
changed: Data aktualizacji
|
changed: Data aktualizacji
|
||||||
|
grade: Ocena
|
||||||
|
|
||||||
edition:
|
edition:
|
||||||
internships:
|
internships:
|
||||||
|
title: Praktyki
|
||||||
|
proposals:
|
||||||
title: Zgłoszenia praktyk
|
title: Zgłoszenia praktyk
|
||||||
ipp:
|
ipp:
|
||||||
title: Indywidualne Plany Praktyk
|
title: Indywidualne Plany Praktyk
|
||||||
@ -30,6 +35,8 @@ edition:
|
|||||||
title: "Edycje praktyk"
|
title: "Edycje praktyk"
|
||||||
reports:
|
reports:
|
||||||
title: "Raporty praktyki"
|
title: "Raporty praktyki"
|
||||||
|
dean-approvals:
|
||||||
|
title: "Zgody dziekana"
|
||||||
field:
|
field:
|
||||||
id: Identyfikator
|
id: Identyfikator
|
||||||
start: Początek
|
start: Początek
|
||||||
@ -44,15 +51,19 @@ edition:
|
|||||||
deadlines: "Terminy"
|
deadlines: "Terminy"
|
||||||
program: "Ramowy program praktyk"
|
program: "Ramowy program praktyk"
|
||||||
types: "Dostępne typy praktyki"
|
types: "Dostępne typy praktyki"
|
||||||
report-fields:
|
|
||||||
title: "Pola formularza raportu praktyki"
|
|
||||||
manage:
|
manage:
|
||||||
management: "Zarządzanie edycją"
|
management: "Zarządzanie edycją"
|
||||||
internships: "Zarządzanie praktykami"
|
internships: "Zarządzanie praktykami"
|
||||||
settings:
|
settings:
|
||||||
title: "Konfiguracja edycji"
|
title: "Konfiguracja edycji"
|
||||||
|
schema: "Pola formularza raportu praktyki"
|
||||||
program:
|
program:
|
||||||
entry: "Punkt ramowego programu praktyki #{{ index }}"
|
entry: "Punkt ramowego programu praktyki #{{ index }}"
|
||||||
|
field:
|
||||||
|
description: "Opis"
|
||||||
|
|
||||||
|
report-fields:
|
||||||
|
title: "Pola formularza raportu praktyki"
|
||||||
|
|
||||||
report-field:
|
report-field:
|
||||||
preview: Podgląd
|
preview: Podgląd
|
||||||
|
Loading…
Reference in New Issue
Block a user