Finish up reporting
This commit is contained in:
parent
6be3fd12f9
commit
3711761671
@ -8,6 +8,7 @@ export enum UploadType {
|
||||
Ipp = "IppScan",
|
||||
DeanConsent = "DeanConsent",
|
||||
Insurance = "NnwInsurance",
|
||||
InternshipEvaluation = "InternshipEvaluation"
|
||||
}
|
||||
|
||||
export interface DocumentFileInfo extends Identifiable {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { emptyReport, sampleReportSchema } from "@/provider/dummy/report";
|
||||
import {
|
||||
Button,
|
||||
@ -12,7 +12,7 @@ import {
|
||||
Radio,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem
|
||||
MenuItem, FormHelperText
|
||||
} from "@material-ui/core";
|
||||
import { Actions } from "@/components";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
@ -26,6 +26,13 @@ import { Transformer } from "@/serialization";
|
||||
import api from "@/api";
|
||||
import { useCurrentEdition } from "@/hooks";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { Description as DescriptionIcon } from "@material-ui/icons";
|
||||
import { DropzoneArea } from "material-ui-dropzone";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
import { UploadType } from "@/api/upload";
|
||||
import { InternshipPlanActions, InternshipReportActions, useDispatch } from "@/state/actions";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "@/state/reducer";
|
||||
|
||||
export type ReportFieldProps<TField = ReportFieldDefinition> = {
|
||||
field: TField;
|
||||
@ -120,11 +127,26 @@ export default function ReportForm() {
|
||||
const edition = useCurrentEdition() as Edition;
|
||||
const report = emptyReport;
|
||||
const schema = edition.schema;
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [file, setFile] = useState<File>();
|
||||
const document = useSelector<AppState>(state => state.report.evaluation);
|
||||
|
||||
const handleSubmit = async (values: ReportFormValues) => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = reportFormValuesTransformer.reverseTransform(values, { report });
|
||||
await api.report.save(result);
|
||||
|
||||
let destination: InternshipDocument = document as any;
|
||||
|
||||
if (!destination) {
|
||||
destination = await api.upload.create(UploadType.InternshipEvaluation);
|
||||
}
|
||||
|
||||
await api.upload.upload(destination, file);
|
||||
};
|
||||
|
||||
return <Formik initialValues={ reportFormValuesTransformer.transform(report) } onSubmit={ handleSubmit }>
|
||||
@ -133,6 +155,18 @@ export default function ReportForm() {
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" component="p">{ t('forms.report.instructions') }</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/karta%20oceny%20praktyki" startIcon={ <DescriptionIcon /> }>
|
||||
{ t('steps.report.template') }
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") } onChange={ files => setFile(files[0]) }/>
|
||||
<FormHelperText>{ t('forms.report.dropzone-help') }</FormHelperText>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h3">{ t('forms.report.report') }</Typography>
|
||||
</Grid>
|
||||
{ schema.map(field => <Grid item xs={12}><CustomField field={ field }/></Grid>) }
|
||||
<Grid item xs={12}>
|
||||
<Actions>
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
InternshipInfoDTO, internshipReportDtoTransformer,
|
||||
submissionStateDtoTransformer
|
||||
} from "@/api/dto/internship-registration";
|
||||
import { Transformer } from "@/serialization";
|
||||
import { OneWayTransformer, Transformer } from "@/serialization";
|
||||
import { mentorDtoTransformer } from "@/api/dto/mentor";
|
||||
import { internshipTypeDtoTransformer } from "@/api/dto/type";
|
||||
import { studentDtoTransfer } from "@/api/dto/student";
|
||||
@ -24,6 +24,7 @@ export type InternshipSubmission = Nullable<Internship> & {
|
||||
changed: Moment | null,
|
||||
ipp: InternshipDocument | null,
|
||||
report: Report | null,
|
||||
evaluation: InternshipDocument,
|
||||
grade: number | null,
|
||||
approvals: InternshipDocument[],
|
||||
}
|
||||
@ -34,10 +35,12 @@ const INTERNSHIP_GRADE_ENDPOINT = "/management/internship/:id/grade";
|
||||
const INTERNSHIP_ACCEPT_ENDPOINT = "/management/internship/:id/registration/accept";
|
||||
const INTERNSHIP_REJECT_ENDPOINT = "/management/internship/:id/registration/reject";
|
||||
|
||||
const internshipInfoDtoTransformer: Transformer<InternshipInfoDTO, InternshipSubmission> = {
|
||||
const internshipInfoDtoTransformer: OneWayTransformer<InternshipInfoDTO, InternshipSubmission> = {
|
||||
transform(subject: InternshipInfoDTO, context?: never): InternshipSubmission {
|
||||
// @ts-ignore
|
||||
const ipp = subject.documentation.find<InternshipDocumentDTO>(doc => doc.type === UploadType.Ipp);
|
||||
// @ts-ignore
|
||||
const evaluation = subject.documentation.find<InternshipDocumentDTO>(doc => doc.type === UploadType.InternshipEvaluation);
|
||||
const report = subject.report;
|
||||
|
||||
return {
|
||||
@ -58,12 +61,10 @@ const internshipInfoDtoTransformer: Transformer<InternshipInfoDTO, InternshipSub
|
||||
type: subject.internshipRegistration.type && internshipTypeDtoTransformer.transform(subject.internshipRegistration.type),
|
||||
ipp: ipp && internshipDocumentDtoTransformer.transform(ipp),
|
||||
report: report && internshipReportDtoTransformer.transform(report),
|
||||
approvals: (subject.documentation.filter(doc => doc.type === UploadType.DeanConsent).map(subject => internshipDocumentDtoTransformer.transform(subject)))
|
||||
approvals: (subject.documentation.filter(doc => doc.type === UploadType.DeanConsent).map(subject => internshipDocumentDtoTransformer.transform(subject))),
|
||||
evaluation: evaluation && internshipDocumentDtoTransformer.transform(evaluation),
|
||||
};
|
||||
},
|
||||
reverseTransform(subject: InternshipSubmission, context: undefined): InternshipInfoDTO {
|
||||
return {} as any;
|
||||
},
|
||||
}
|
||||
|
||||
export async function all(edition: Identifiable): Promise<InternshipSubmission[]> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncState } from "@/hooks";
|
||||
import { useSpacing } from "@/styles";
|
||||
@ -22,7 +22,7 @@ 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 { EditionContext, EditionManagement, EditionManagementProps } from "@/management/edition/manage";
|
||||
import { InternshipSubmission } from "@/management/api/internship";
|
||||
import { createPortal } from "react-dom";
|
||||
import { FileInfo } from "@/components/fileinfo";
|
||||
@ -34,6 +34,8 @@ import { AcceptanceActions } from "@/components/acceptance-action";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
import { StudentPreview } from "@/pages/user/profile";
|
||||
import { SubmissionStatus } from "@/state/reducer/submission";
|
||||
import { ReportPreview } from "@/pages/steps/report";
|
||||
import { Report, ReportSchema } from "@/data/report";
|
||||
|
||||
const title = "edition.internships.title";
|
||||
|
||||
@ -54,7 +56,9 @@ const ProposalAction = ({ internship, children }: { internship: InternshipSubmis
|
||||
}
|
||||
|
||||
return <>
|
||||
<div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children || <FileFind /> }</div>
|
||||
{ internship.state
|
||||
? <div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children }</div>
|
||||
: children }
|
||||
{ createPortal(
|
||||
<InternshipDetailsDialog onDiscard={ handleDiscard } onAccept={ handleAccept } internship={ internship } open={ open } onClose={ () => setOpen(false) }/>,
|
||||
document.getElementById("modals") as Element,
|
||||
@ -95,6 +99,41 @@ const IPPAction = ({ internship, children }: { internship: InternshipSubmission,
|
||||
</>;
|
||||
}
|
||||
|
||||
const ReportAction = ({ internship, children }: { internship: InternshipSubmission, children: React.ReactChild }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const edition = useContext(EditionContext);
|
||||
|
||||
const handleDiscard = (comment: string) => {
|
||||
setOpen(false);
|
||||
api.document.discard(internship.evaluation as InternshipDocument, comment);
|
||||
}
|
||||
|
||||
const handleAccept = (comment?: string) => {
|
||||
setOpen(false);
|
||||
api.document.accept(internship.evaluation as InternshipDocument, comment);
|
||||
}
|
||||
|
||||
return <>
|
||||
{ internship.report
|
||||
? <div onClick={ () => setOpen(true) } style={{ display: "inline-block", cursor: "pointer" }}>{ children }</div>
|
||||
: children }
|
||||
{ createPortal(
|
||||
<Dialog maxWidth="md" fullWidth open={ open } onClose={ () => setOpen(false) }>
|
||||
<DialogTitle>{ fullname(internship.intern as Student) }</DialogTitle>
|
||||
<DialogContent>
|
||||
{ internship.evaluation && <FileInfo document={ internship.evaluation } /> }
|
||||
<ReportPreview schema={ edition?.schema as ReportSchema } report={ internship.report as Report } />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<AcceptanceActions onAccept={ handleAccept } onDiscard={ handleDiscard } label="internship"/>
|
||||
</DialogActions>
|
||||
</Dialog>,
|
||||
document.getElementById("modals") as Element,
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
const StudentAction = ({ internship, children }: { internship: InternshipSubmission, children: React.ReactChild }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -117,7 +156,7 @@ export const InternshipState = ({ internship }: { internship: InternshipSubmissi
|
||||
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 reportState = internship.evaluation?.state || null;
|
||||
const gradeState = internship.grade ? "accepted" : null;
|
||||
const approvalState = internship.approvals.reduce<SubmissionStatus | null>((status, document) => {
|
||||
switch (status) {
|
||||
@ -145,7 +184,9 @@ export const InternshipState = ({ internship }: { internship: InternshipSubmissi
|
||||
<IPPAction internship={ internship }>
|
||||
<StepState state={ ippState } label={ t("steps.plan.header") } icon={ <FormatPageBreak /> } />
|
||||
</IPPAction>
|
||||
<StepState state={ reportState } label={ t("steps.report.header") } icon={ <FileChartOutline /> } />
|
||||
<ReportAction internship={ internship }>
|
||||
<StepState state={ reportState } label={ t("steps.report.header") } icon={ <FileChartOutline /> } />
|
||||
</ReportAction>
|
||||
<StepState state={ gradeState } label={ t("steps.grade.header") } icon={ <Star /> } />
|
||||
<StepState state={ approvalState } label={ t("steps.approvals.header") } icon={ <CertificateOutline/> }
|
||||
style={ approvalState ? {} : { opacity: 0.2 } }
|
||||
|
@ -33,7 +33,7 @@ export function EditionSettings() {
|
||||
|
||||
await api.edition.save(result);
|
||||
|
||||
history.push("management:edition_manage", { edition: edition.id as string })
|
||||
history.push("management:edition_manage", { edition: edition.value.id as string })
|
||||
};
|
||||
|
||||
return <Page>
|
||||
|
@ -15,9 +15,15 @@ import { InsuranceStep } from "@/pages/steps/insurance";
|
||||
import { StudentStep } from "@/pages/steps/student";
|
||||
import api from "@/api";
|
||||
import { AppDispatch, InternshipPlanActions, InternshipProposalActions, InternshipReportActions, useDispatch } from "@/state/actions";
|
||||
import { internshipDocumentDtoTransformer, internshipRegistrationDtoTransformer, internshipReportDtoTransformer } from "@/api/dto/internship-registration";
|
||||
import {
|
||||
internshipDocumentDtoTransformer,
|
||||
internshipRegistrationDtoTransformer,
|
||||
internshipReportDtoTransformer, SubmissionState,
|
||||
submissionStateDtoTransformer
|
||||
} from "@/api/dto/internship-registration";
|
||||
import { UploadType } from "@/api/upload";
|
||||
import { ReportStep } from "@/pages/steps/report";
|
||||
import { GradeStep } from "@/pages/steps/grade";
|
||||
|
||||
export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
||||
const internship = await api.internship.get();
|
||||
@ -27,9 +33,11 @@ export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
||||
state: internship.internshipRegistration.state,
|
||||
comment: internship.internshipRegistration.changeStateComment,
|
||||
internship: internshipRegistrationDtoTransformer.transform(internship.internshipRegistration),
|
||||
grade: internship.grade,
|
||||
})
|
||||
|
||||
const plan = internship.documentation.find(doc => doc.type === UploadType.Ipp);
|
||||
const evaluation = internship.documentation.find(doc => doc.type === UploadType.InternshipEvaluation);
|
||||
const report = internship.report;
|
||||
|
||||
if (plan) {
|
||||
@ -49,8 +57,9 @@ export const updateInternshipInfo = async (dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: InternshipReportActions.Receive,
|
||||
report: internshipReportDtoTransformer.transform(report),
|
||||
state: report.state,
|
||||
comment: report.changeStateComment,
|
||||
state: evaluation?.state || SubmissionState.Draft,
|
||||
comment: evaluation?.changeStateComment || "",
|
||||
evaluation: evaluation,
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
@ -84,7 +93,7 @@ export const MainPage = () => {
|
||||
yield <InsuranceStep key="insurance"/>;
|
||||
|
||||
yield <ReportStep key="report"/>;
|
||||
yield <Step label={ t('steps.grade.header') } key="grade"/>
|
||||
yield <GradeStep key="grade"/>;
|
||||
}
|
||||
|
||||
return <Page>
|
||||
|
22
src/pages/steps/grade.tsx
Normal file
22
src/pages/steps/grade.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { StepProps, Typography } from "@material-ui/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "@/state/reducer";
|
||||
import { Actions, Step } from "@/components";
|
||||
import { ContactButton } from "@/pages/steps/common";
|
||||
|
||||
|
||||
export const GradeStep = (props: StepProps) => {
|
||||
const { t } = useTranslation();
|
||||
const grade = useSelector<AppState, number | null>(state => state.proposal.grade);
|
||||
|
||||
return <Step { ...props } label={ t('steps.grade.header') } completed={ !!grade } active={ true }>
|
||||
{ grade ? <>
|
||||
<Typography variant="h1">{ grade }</Typography>
|
||||
<Actions>
|
||||
<ContactButton />
|
||||
</Actions>
|
||||
</> : <>{ t("steps.grade.wait") }</> }
|
||||
</Step>
|
||||
}
|
@ -16,6 +16,8 @@ import { MultiChoiceValue, Report, ReportSchema, SingleChoiceValue, TextFieldVal
|
||||
import { createPortal } from "react-dom";
|
||||
import { getInternshipReport } from "@/state/reducer/report";
|
||||
import { Edition } from "@/data/edition";
|
||||
import { FileInfo } from "@/components/fileinfo";
|
||||
import { InternshipDocument } from "@/api/dto/internship-registration";
|
||||
|
||||
export type ReportPreviewProps = {
|
||||
schema: ReportSchema,
|
||||
@ -36,7 +38,7 @@ export const ReportPreview = ({ schema, report }: ReportPreviewProps) => {
|
||||
return <div>{ (value as SingleChoiceValue).pl }</div>
|
||||
case "long-text":
|
||||
case "short-text":
|
||||
return <p>{ value as TextFieldValue }</p>
|
||||
return <p style={ { marginTop: "0" } }>{ value as TextFieldValue }</p>
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,13 +117,13 @@ const ReportActions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const PlanComment = (props: HTMLProps<HTMLDivElement>) => {
|
||||
const { comment, declined } = useSelector<AppState, SubmissionState>(state => state.plan);
|
||||
export const ReportComment = (props: HTMLProps<HTMLDivElement>) => {
|
||||
const { comment, declined } = useSelector<AppState, SubmissionState>(state => state.report);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
|
||||
<AlertTitle>{ t('comments') }</AlertTitle>
|
||||
{ comment }
|
||||
<div dangerouslySetInnerHTML={{ __html: comment }}/>
|
||||
</Alert> : null
|
||||
}
|
||||
|
||||
@ -129,6 +131,7 @@ export const ReportStep = (props: StepProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submission = useSelector<AppState, SubmissionState>(state => state.report);
|
||||
const evaluation = useSelector<AppState, InternshipDocument | null>(state => state.report.evaluation);
|
||||
const spacing = useSpacing(2);
|
||||
const edition = useCurrentEdition();
|
||||
|
||||
@ -146,8 +149,8 @@ export const ReportStep = (props: StepProps) => {
|
||||
<div className={ spacing.vertical }>
|
||||
<p>{ t(`steps.report.info.${ status }`) }</p>
|
||||
|
||||
{ comment && <Box pb={ 2 }><PlanComment/></Box> }
|
||||
|
||||
<ReportComment />
|
||||
{ evaluation && <FileInfo document={ evaluation } /> }
|
||||
<ReportActions/>
|
||||
</div>
|
||||
</Step>;
|
||||
|
@ -29,6 +29,7 @@ export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateActi
|
||||
internship: Internship;
|
||||
state: SubmissionState;
|
||||
comment?: string;
|
||||
grade?: number;
|
||||
}
|
||||
|
||||
export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> {
|
||||
|
@ -15,12 +15,14 @@ import { SubmissionState as ApiSubmissionState } from "@/api/dto/internship-regi
|
||||
|
||||
export type InternshipProposalState = SubmissionState & MayRequireDeanApproval & {
|
||||
proposal: Serializable<Internship> | null;
|
||||
grade: number | null;
|
||||
}
|
||||
|
||||
const defaultInternshipProposalState: InternshipProposalState = {
|
||||
...defaultDeanApprovalsState,
|
||||
...defaultSubmissionState,
|
||||
proposal: null,
|
||||
grade: null,
|
||||
}
|
||||
|
||||
export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
|
||||
@ -59,6 +61,7 @@ const internshipProposalReducer = (state: InternshipProposalState = defaultInter
|
||||
].includes(action.state),
|
||||
proposal: internshipSerializationTransformer.transform(action.internship),
|
||||
comment: action.comment || "",
|
||||
grade: action.grade || null,
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
|
@ -8,18 +8,20 @@ import {
|
||||
} from "@/state/reducer/submission";
|
||||
import { Reducer } from "react";
|
||||
import { SubmissionAction } from "@/state/actions/submission";
|
||||
import { SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
|
||||
import { InternshipDocument, SubmissionState as ApiSubmissionState } from "@/api/dto/internship-registration";
|
||||
import { Report } from "@/data/report";
|
||||
import { reportSerializationTransformer } from "@/serialization/report";
|
||||
|
||||
export type InternshipReportState = SubmissionState & {
|
||||
report: Serializable<Report> | null;
|
||||
evaluation: InternshipDocument | null;
|
||||
}
|
||||
|
||||
const defaultInternshipReportState: InternshipReportState = {
|
||||
...defaultDeanApprovalsState,
|
||||
...defaultSubmissionState,
|
||||
report: null,
|
||||
evaluation: null,
|
||||
}
|
||||
|
||||
export const getInternshipReport = ({ report }: InternshipReportState): Report | null =>
|
||||
@ -60,6 +62,7 @@ const internshipReportReducer = (state: InternshipReportState = defaultInternshi
|
||||
].includes(action.state),
|
||||
report: reportSerializationTransformer.transform(action.report),
|
||||
comment: action.comment,
|
||||
evaluation: action.evaluation,
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
|
@ -112,8 +112,11 @@ forms:
|
||||
key: Klucz dostępu do edycji
|
||||
|
||||
report:
|
||||
report: Raport
|
||||
dropzone-help: Skan karty oceny w formacie PDF
|
||||
instructions: >
|
||||
Wypełnij wszystkie pola formularza w celu sfinalizowania praktyki.
|
||||
Poproś swojego opiekuna o wypełnienie karty oceny praktyki - następnie zeskanuj ją i zamieść wynikowy plik poniże.
|
||||
Dodatkowo wypełnij wszystkie pola formularza raportu praktyki w celu sfinalizowania praktyki.
|
||||
|
||||
student:
|
||||
name: imię
|
||||
@ -235,8 +238,10 @@ steps:
|
||||
Twój raport został zweryfikowany i odrzucony. Popraw zgłoszone uwagi i wyślij raport ponownie. W razie
|
||||
pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku.
|
||||
submit: Uzupełnij raport
|
||||
template: "Szablon karty oceny prakyki"
|
||||
grade:
|
||||
header: "Ocena z praktyki"
|
||||
wait: "W tym miejscu pojawi się ocena z praktyki, po wystawieniu jej przez pełnomocnika praktyk ds. Twojego kierunku"
|
||||
insurance:
|
||||
header: "Ubezpieczenie NNW"
|
||||
instructions: >
|
||||
|
Loading…
Reference in New Issue
Block a user