Add internship summaries

This commit is contained in:
Kacper Donat 2021-01-18 00:22:57 +01:00
parent 9dbdde6baa
commit 6be3fd12f9
27 changed files with 704 additions and 146 deletions

View File

@ -1,4 +1,4 @@
import { Identifiable, InternshipProgramEntry } from "@/data";
import { Identifiable, Identifier, InternshipProgramEntry } from "@/data";
import { CourseDTO, courseDtoTransformer } from "@/api/dto/course";
import { OneWayTransformer, Transformer } from "@/serialization";
import { Edition } from "@/data/edition";
@ -79,7 +79,7 @@ export interface FieldDefinitionDTO extends Identifiable {
export const fieldDefinitionDtoTransformer: Transformer<FieldDefinitionDTO, ReportFieldDefinition> = {
transform(dto: FieldDefinitionDTO, context?: unknown): ReportFieldDefinition {
return {
...dto,
id: dto.id,
choices: (dto.choices || []).map(choice => JSON.parse(choice)),
description: {
pl: dto.description,
@ -94,7 +94,7 @@ export const fieldDefinitionDtoTransformer: Transformer<FieldDefinitionDTO, Repo
},
reverseTransform(subject: ReportFieldDefinition, context?: unknown): FieldDefinitionDTO {
return {
...subject,
id: subject.id,
choices: "choices" in subject && subject.choices.map(choice => JSON.stringify(choice)) || [],
description: subject.description.pl,
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[],
}
}
}

View File

@ -125,6 +125,7 @@ export interface InternshipInfoDTO extends Identifiable {
documentation: InternshipDocumentDTO[],
student: StudentDTO,
report: InternshipReportDTO,
grade: number,
}
export const internshipReportDtoTransformer: OneWayTransformer<InternshipReportDTO, Report> = {

View File

@ -26,3 +26,5 @@ 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 }`;

View File

@ -24,6 +24,8 @@ 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";
export type ReportFieldProps<TField = ReportFieldDefinition> = {
field: TField;
@ -115,12 +117,13 @@ const reportFormValuesTransformer: Transformer<Report, ReportFormValues, { repor
}
export default function ReportForm() {
const edition = useCurrentEdition() as Edition;
const report = emptyReport;
const schema = sampleReportSchema;
const schema = edition.schema;
const { t } = useTranslation();
const handleSubmit = async (values: ReportFormValues) => {
const result = reportFormValuesTransformer.reverseTransform(values);
const result = reportFormValuesTransformer.reverseTransform(values, { report });
await api.report.save(result);
};

View File

@ -1,8 +1,11 @@
import { Course } from "@/data";
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[]> {
return [
sampleCourse,
];
const response = await axios.get<Course[]>(COURSE_INDEX_ENDPOINT);
return response.data;
}

View File

@ -11,7 +11,7 @@ export async function accept(document: OneOrMany<InternshipDocument>, comment?:
await Promise.all(documents.map(document => axios.put(
prepare(DOCUMENT_ACCEPT_ENDPOINT, { id: document.id || ""}),
JSON.stringify(comment),
JSON.stringify(comment || ""),
{ headers: { 'Content-Type': 'application/json' } }
)))
}

View File

@ -1,5 +1,5 @@
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 { 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 }));
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;
}

View File

@ -24,10 +24,13 @@ export type InternshipSubmission = Nullable<Internship> & {
changed: Moment | null,
ipp: InternshipDocument | null,
report: Report | null,
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";
@ -38,6 +41,7 @@ const internshipInfoDtoTransformer: Transformer<InternshipInfoDTO, InternshipSub
const report = subject.report;
return {
...subject,
changed: moment(subject.internshipRegistration.submissionDate),
company: subject.internshipRegistration.company,
startDate: moment(subject.internshipRegistration.start),
@ -53,7 +57,8 @@ const internshipInfoDtoTransformer: Transformer<InternshipInfoDTO, InternshipSub
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)
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 {
@ -78,7 +83,7 @@ export async function accept(internship: OneOrMany<Internship>, comment?: string
await Promise.all(internships.map(internship => axios.put(
prepare(INTERNSHIP_ACCEPT_ENDPOINT, { id: internship.id || ""}),
JSON.stringify(comment),
JSON.stringify(comment || ""),
{ headers: { 'Content-Type': 'application/json' } }
)))
}
@ -92,3 +97,13 @@ export async function discard(internship: OneOrMany<Internship>, comment: string
{ 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' } }
)))
}

View 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>
}

View File

@ -31,7 +31,7 @@ import { AccountCheck, ArrowDown, ArrowUp, ShieldCheck, TrashCan } from "mdi-mat
import { Actions } from "@/components";
import { Add } from "@material-ui/icons";
export type EditionFormValues = Nullable<Edition>;
export type EditionFormValues = Omit<Nullable<Edition>, "schema">;
export const initialEditionFormValues: EditionFormValues = {
course: null,
@ -47,7 +47,7 @@ export const initialEditionFormValues: EditionFormValues = {
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
? [ ...array, 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/> }
getOptionLabel={ course => course.name }
value={ field.value }
onChange={ field.onChange }
onChange={ (_, value) => form.setFieldValue(field.name, value, false) }
onBlur={ field.onBlur }
/>
}

View File

@ -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>
}

View 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>
}

View File

@ -1,29 +1,158 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsync, useAsyncState } from "@/hooks";
import { useAsyncState } from "@/hooks";
import { useSpacing } from "@/styles";
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 { 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 { 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 { canAccept, canDiscard, StateLabel } from "@/management/edition/internship/common";
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";
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) => {
const { t } = useTranslation("management");
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
@ -36,47 +165,26 @@ export const InternshipManagement = ({ edition }: EditionManagementProps) => {
useEffect(updateInternshipList, []);
const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
const GradeAction = ({ internship }: { internship: InternshipSubmission }) => {
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);
await api.internship.accept(internship as Internship, comment);
updateInternshipList();
}
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(
<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,
) }
</>;
}
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>[] = [
{
title: t("internship.column.student"),
@ -91,17 +199,16 @@ export const InternshipManagement = ({ edition }: EditionManagementProps) => {
field: "type.label.pl",
},
{
title: t("internship.column.changed"),
render: summary => summary.changed?.format("yyyy-MM-DD")
title: t("internship.column.status"),
render: summary => <InternshipState internship={ summary } />
},
{
title: t("internship.column.status"),
render: summary => <StateLabel state={ summary.state } />
title: t("internship.column.grade"),
field: "grade",
width: 0,
},
actionsColumn(internship => <>
{ canAccept(internship) && <AcceptAction internship={ internship } /> }
{ canDiscard(internship) && <DiscardAction internship={ internship } /> }
<IconButton component={ RouterLink } to={ route("management:edition_internship", { edition: edition.id || "", internship: internship.id || "" }) }><FileFind /></IconButton>
{ canGrade(internship) && <GradeAction internship={ internship } /> }
</>)
];
@ -126,7 +233,6 @@ export const InternshipManagement = ({ edition }: EditionManagementProps) => {
data={ internships }
onSelectionChange={ internships => setSelected(internships) }
options={ { selection: true, pageSize: 10 } }
detailPanel={ summary => <InternshipDetails summary={ summary } /> }
/>
}</Async>
</Container>

View File

@ -18,7 +18,7 @@ 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/internship/common";
import { StateLabel } from "@/management/edition/proposal/common";
import { createPortal } from "react-dom";
import { Internship } from "@/data";
import { FileInfo } from "@/components/fileinfo";

View File

@ -5,7 +5,16 @@ 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, 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 { useSpacing } from "@/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) => {
const { t } = useTranslation("management");
const { params } = useRouteMatch();
const spacing = useSpacing(2);
const classes = useSectionStyles();
@ -50,6 +58,9 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
<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>
@ -64,8 +75,8 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
<Paper elevation={ 2 }>
<Typography className={ classes.header }>{ t("edition.manage.management") }</Typography>
<Management.Menu>
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
{ t("management:edition.report-fields.title") }
<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") }

View File

@ -6,11 +6,12 @@ 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: orange["800"],
color: orange["800"],
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
},
declined: {
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 stateIcons: { [sate in SubmissionStatus]: React.ReactElement } = {
accepted: <NotebookCheckOutline/>,
awaiting: <HourglassEmptyRounded/>,
declined: <NotebookRemoveOutline/>,
draft: <NotebookEditOutline/>
}
export const StateLabel = ({ state }: StateLabelProps) => {
const icons: { [sate in SubmissionStatus]: React.ReactElement } = {
accepted: <NotebookCheckOutline/>,
awaiting: <ClockOutline/>,
declined: <NotebookRemoveOutline/>,
draft: <NotebookEditOutline/>
}
const classes = useStateLabelStyles();
const { t } = useTranslation();
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"/>
}

View 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>
}

View 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>
}

View 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>;
}

View File

@ -13,17 +13,11 @@ 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/internship/common";
import { StateLabel } from "@/management/edition/proposal/common";
import { createPortal } from "react-dom";
import { Internship, Stateful } from "@/data";
import { FileInfo } from "@/components/fileinfo";
import { Alert } from "@material-ui/lab";
import { InternshipDocument } from "@/api/dto/internship-registration";
import { Stateful } from "@/data";
import { Report } from "@/data/report";
const title = "edition.reports.title";

View File

@ -1,16 +1,15 @@
import React, { useCallback } from "react";
import { Page } from "@/pages/base";
import { Management } from "@/management/main";
import { Container, Divider, Typography, Button } from "@material-ui/core";
import { Async } from "@/components/async";
import { useTranslation } from "react-i18next";
import { useRouteMatch } from "react-router-dom";
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 } from "@/management/edition/form";
import { EditionForm, EditionFormValues, editionFormValuesTransformer } from "@/management/edition/form";
import { Actions } from "@/components";
import { Save } from "@material-ui/icons";
import { Cancel } from "mdi-material-ui";
@ -21,11 +20,21 @@ 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 = () => {};
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>
<Page.Header maxWidth="md">

View File

@ -47,7 +47,7 @@ export const ManagementIndex = () => {
{ t("management:type.index.title") }
</ManagementLink>
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:report_fields") }>
{ t("management:edition.report-fields.title") }
{ t("management:report-fields.title") }
</ManagementLink>
<ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }>
{ t("management:page.index.title") }

View File

@ -20,7 +20,7 @@ import { Actions } from "@/components";
import { Refresh } from "mdi-material-ui";
import { useSpacing } from "@/styles";
const title = "edition.report-fields.title";
const title = "report-fields.title";
export const FieldPreview = ({ field }: { field: ReportFieldDefinition }) => {
return <Formik initialValues={{}} onSubmit={() => {}}>

View File

@ -7,11 +7,12 @@ 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 { InternshipManagement } from "@/management/edition/internship/list";
import { InternshipDetails } from "@/management/edition/internship/details";
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 },
@ -19,9 +20,10 @@ export const managementRoutes: Route[] = ([
{ 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_internship", path: "/editions/:edition/internships/:internship", content: InternshipDetails, 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_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 },

View File

@ -12,25 +12,53 @@ import { Alert, AlertTitle } from "@material-ui/lab";
import { ContactButton, Status } from "@/pages/steps/common";
import { useCurrentEdition, useDeadlines } from "@/hooks";
import { useSpacing } from "@/styles";
import { Report } from "@/data/report";
import { sampleReportSchema } from "@/provider/dummy/report";
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";
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 = {
report: Report;
} & DialogProps;
export const ReportPreviewDialog = ({ report, ...props }: ReportPreviewDialogProps) => {
const schema = sampleReportSchema;
const edition = useCurrentEdition() as Edition;
const schema = edition.schema || [];
const { t } = useTranslation();
return <Dialog { ...props } maxWidth="md">
<DialogTitle>{ t("steps.report.preview") }</DialogTitle>
<DialogContent>{ schema.map(field => <>
<Typography variant="subtitle2">{ field.label.pl }</Typography>
{ JSON.stringify(report.fields[`field_${field.id}`]) }
</> )}</DialogContent>
return <Dialog { ...props } maxWidth="md" fullWidth>
<DialogTitle>{ t("steps.report.header") }</DialogTitle>
<DialogContent><ReportPreview schema={ schema } report={ report }/></DialogContent>
</Dialog>
}
@ -40,7 +68,8 @@ const ReportActions = () => {
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 }>
<Button to={ route("internship_report") } variant="contained" color="primary" component={ RouterLink }
startIcon={ <FileUploadOutline/> } { ...props as any }>
{ children }
</Button>
@ -64,7 +93,8 @@ const ReportActions = () => {
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction />
<ReviewAction/>
<FormAction>{ t('send-again') }</FormAction>
</Actions>
case "accepted":
return <Actions>
@ -77,7 +107,7 @@ const ReportActions = () => {
</Actions>
case "draft":
return <Actions>
<FormAction />
<FormAction/>
</Actions>
default:
@ -112,7 +142,7 @@ export const ReportStep = (props: StepProps) => {
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
until={ deadlines.report }
notBefore={ edition?.reportingStart }
state={ <Status submission={ submission } /> }>
state={ <Status submission={ submission }/> }>
<div className={ spacing.vertical }>
<p>{ t(`steps.report.info.${ status }`) }</p>

View File

@ -14,6 +14,9 @@ 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 {
@ -26,6 +29,9 @@ 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
}
},
}

View File

@ -12,17 +12,22 @@ actions:
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
@ -30,6 +35,8 @@ edition:
title: "Edycje praktyk"
reports:
title: "Raporty praktyki"
dean-approvals:
title: "Zgody dziekana"
field:
id: Identyfikator
start: Początek
@ -44,15 +51,19 @@ edition:
deadlines: "Terminy"
program: "Ramowy program praktyk"
types: "Dostępne typy praktyki"
report-fields:
title: "Pola formularza raportu 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