diff --git a/src/components/contact.tsx b/src/components/contact.tsx new file mode 100644 index 0000000..03b5cea --- /dev/null +++ b/src/components/contact.tsx @@ -0,0 +1,74 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSpacing } from "@/styles"; +import { Field, Form, Formik } from "formik"; +import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, Typography } from "@material-ui/core"; +import { CKEditorField } from "@/field/ckeditor"; +import { Actions } from "@/components/actions"; +import { Cancel, Send } from "mdi-material-ui"; +import { createPortal } from "react-dom"; +import { capitalize } from "@/helpers"; + +export type ContactFormValues = { + content: string; +} + +const initialContactFormValues: ContactFormValues = { + content: "", +} + +export type ContactDialogProps = { + onSend: (values: ContactFormValues) => void; +} & DialogProps; + +export function ContactForm() { + const { t } = useTranslation(); + const spacing = useSpacing(2); + + return <div className={ spacing.vertical } style={{ overflow: 'hidden' }}> + <Field label={ t("forms.contact.field.content") } name="content" component={ CKEditorField }/> + </div> +} + +export function ContactDialog({ onSend, ...props }: ContactDialogProps) { + const spacing = useSpacing(2); + const { t } = useTranslation(); + + return <Dialog { ...props } maxWidth="lg"> + <Formik initialValues={ initialContactFormValues } onSubmit={ onSend }> + <Form className={ spacing.vertical }> + <DialogTitle>{ capitalize(t("forms.contact.title")) }</DialogTitle> + <DialogContent> + <ContactForm /> + </DialogContent> + <DialogActions> + <Actions> + <Button variant="contained" color="primary" startIcon={ <Send /> } type="submit">{ t("send") }</Button> + <Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button> + </Actions> + </DialogActions> + </Form> + </Formik> + </Dialog> +} + +export type ContactActionProps = { + children: (props: { action: () => void }) => React.ReactNode +}; + +export function ContactAction({ children }: ContactActionProps) { + const [open, setOpen] = useState<boolean>(false); + + const handleClose = () => { setOpen(false) }; + const handleSubmit = (values: ContactFormValues) => { + setOpen(false); + } + + return <> + { children({ action: () => setOpen(true) }) } + { createPortal( + <ContactDialog open={ open } onSend={ handleSubmit } onClose={ handleClose }/>, + document.getElementById("modals") as HTMLElement + ) } + </> +} diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index c5d83f0..e8dd4c9 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -112,7 +112,7 @@ const InternshipProgramForm = () => { if (ev.target.checked) { setSelectedProgramEntries([ ...selectedProgramEntries, entry ]); } else { - setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur != entry)); + setSelectedProgramEntries(selectedProgramEntries.filter(cur => cur.id != entry.id)); } } @@ -133,6 +133,7 @@ const InternshipProgramForm = () => { onBlur={ handleBlur } /> </Grid> + { values.kind?.requiresDeanApproval && <Grid item xs={ 12 }><Alert severity="warning">{ t("internship.kind-requires-dean-approval") }</Alert></Grid> } {/*<Grid item md={ 8 }>*/} {/* {*/} {/* values.kind === InternshipType.Other &&*/} @@ -159,6 +160,7 @@ const InternshipProgramForm = () => { const InternshipDurationForm = () => { const { t } = useTranslation(); + const edition = useCurrentEdition(); const { values: { startDate, endDate, workingHours }, errors, @@ -174,6 +176,8 @@ const InternshipDurationForm = () => { const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]); const weeks = useMemo(() => hours !== null ? Math.floor(hours / workingHours) : null, [ hours ]); + const requiresDeanApproval = useMemo(() => edition?.startDate?.isAfter(startDate) || edition?.endDate?.isBefore(endDate), [ startDate, endDate ]) + useUpdateEffect(() => { setFieldTouched("hours", true); setFieldValue("hours", hours, true); @@ -200,6 +204,9 @@ const InternshipDurationForm = () => { minDate={ startDate } /> </Grid> + { requiresDeanApproval && <Grid item xs={ 12 }> + <Alert severity="warning">{ t("internship.duration-requires-dean-approval") }</Alert> + </Grid> } <Grid item md={ 4 }> <Field component={ TextFieldFormik } name="workingHours" diff --git a/src/forms/plan.tsx b/src/forms/plan.tsx index 57e560d..08ddcc0 100644 --- a/src/forms/plan.tsx +++ b/src/forms/plan.tsx @@ -32,9 +32,10 @@ export const PlanForm = () => { if (!destination) { destination = await api.upload.create(UploadType.Ipp); - dispatch({ type: InternshipPlanActions.Send, document: destination }); } + dispatch({ type: InternshipPlanActions.Send, document: destination }); + await api.upload.upload(destination, file); history.push("/"); diff --git a/src/forms/student.tsx b/src/forms/student.tsx index 3d3cead..422e9fc 100644 --- a/src/forms/student.tsx +++ b/src/forms/student.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { useFormikContext } from "formik"; import { InternshipFormValues } from "@/forms/internship"; import { useCurrentEdition } from "@/hooks"; +import { ContactAction } from "@/components/contact"; export const StudentForm = () => { const { t } = useTranslation(); @@ -36,8 +37,10 @@ export const StudentForm = () => { <TextField label={ t("forms.internship.fields.semester") } value={ student.semester || "" } disabled fullWidth/> </Grid> <Grid item xs={12}> - <Alert severity="warning" action={ <Button color="inherit" size="small">skontaktuj się z opiekunem</Button> }> - Powyższe dane nie są poprawne? + <Alert severity="warning" action={ <ContactAction>{ + ({ action }) => <Button color="inherit" size="small" onClick={ action }>{ t("contact") }</Button> + }</ContactAction> }> + { t("incorrect-data-question") } </Alert> </Grid> </Grid> diff --git a/src/helpers.ts b/src/helpers.ts index 1d66767..cfb9ee9 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -43,3 +43,7 @@ export function one<T>(value: OneOrMany<T>): T { return value; } + +export function capitalize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} diff --git a/src/pages/steps/common.tsx b/src/pages/steps/common.tsx index 58fdb8e..6a7d61f 100644 --- a/src/pages/steps/common.tsx +++ b/src/pages/steps/common.tsx @@ -4,6 +4,7 @@ import { createStyles, makeStyles } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import React from "react"; import { CommentQuestion } from "mdi-material-ui/index"; +import { ContactAction } from "@/components/contact"; export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => { switch (status) { @@ -46,8 +47,12 @@ export const Status = ({ submission } : SubmissionStatusProps) => { return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>; } -export const ContactAction = (props: ButtonProps) => { +export const ContactButton = (props: ButtonProps) => { const { t } = useTranslation(); - return <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button> + return <ContactAction>{ + ({ action }) => <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props } onClick={ action}> + { t('contact') } + </Button> } + </ContactAction> } diff --git a/src/pages/steps/insurance.tsx b/src/pages/steps/insurance.tsx index b005389..252902d 100644 --- a/src/pages/steps/insurance.tsx +++ b/src/pages/steps/insurance.tsx @@ -4,7 +4,7 @@ import { InsuranceState } from "@/state/reducer/insurance"; import { Actions, Step } from "@/components"; import { useTranslation } from "react-i18next"; import React from "react"; -import { ContactAction } from "@/pages/steps/common"; +import { ContactButton } from "@/pages/steps/common"; import { useDeadlines } from "@/hooks"; import { StepProps } from "@material-ui/core"; @@ -17,7 +17,7 @@ export const InsuranceStep = (props: StepProps) => { return <Step { ...props } label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }> <p>{ t(`steps.insurance.instructions`) }</p> <Actions> - <ContactAction /> + <ContactButton /> </Actions> </Step> } diff --git a/src/pages/steps/plan.tsx b/src/pages/steps/plan.tsx index 2e15ec3..7d4d523 100644 --- a/src/pages/steps/plan.tsx +++ b/src/pages/steps/plan.tsx @@ -9,7 +9,7 @@ import { Link as RouterLink, useHistory } from "react-router-dom"; import { Actions, Step } from "@/components"; import React, { HTMLProps } from "react"; import { Alert, AlertTitle } from "@material-ui/lab"; -import { ContactAction, Status } from "@/pages/steps/common"; +import { ContactButton, Status } from "@/pages/steps/common"; import { Description as DescriptionIcon } from "@material-ui/icons"; import { useDeadlines } from "@/hooks"; import { InternshipDocument } from "@/api/dto/internship-registration"; @@ -56,9 +56,9 @@ const PlanActions = () => { </Actions> case "declined": return <Actions> - <FormAction>{ t('fix-errors') }</FormAction> + <FormAction>{ t('send-again') }</FormAction> <TemplateAction /> - <ContactAction/> + <ContactButton/> </Actions> case "draft": return <Actions> diff --git a/src/pages/steps/proposal.tsx b/src/pages/steps/proposal.tsx index 9996ba4..697e904 100644 --- a/src/pages/steps/proposal.tsx +++ b/src/pages/steps/proposal.tsx @@ -10,7 +10,7 @@ import { Actions, Step } from "@/components"; import { route } from "@/routing"; import { Link as RouterLink } from "react-router-dom"; import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index"; -import { ContactAction, Status } from "@/pages/steps/common"; +import { ContactButton, Status } from "@/pages/steps/common"; import { useDeadlines } from "@/hooks"; const ProposalActions = () => { @@ -43,7 +43,7 @@ const ProposalActions = () => { case "declined": return <Actions> <FormAction>{ t('fix-errors') }</FormAction> - <ContactAction /> + <ContactButton /> </Actions> case "draft": return <Actions> diff --git a/translations/pl.yaml b/translations/pl.yaml index 294a9ff..d5d2c18 100644 --- a/translations/pl.yaml +++ b/translations/pl.yaml @@ -26,12 +26,15 @@ contact: skontaktuj się z pełnomocnikiem comments: Zgłoszone uwagi send-again: wyślij ponownie cancel: anuluj +send: wyślij accept: zaakceptuj accept-with-comments: zaakceptuj z uwagami accept-without-comments: zaakceptuj bez uwag discard: zgłoś uwagi +incorrect-data-question: "Powyższe dane nie są poprawne?" + dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać" pages: @@ -63,6 +66,10 @@ forms: sections: personal: "Dane osobowe" studies: "Dane kierunkowe" + contact: + title: $t(contact) + field: + content: "Treść" internship: fields: start-date: Data rozpoczęcia praktyki @@ -128,6 +135,8 @@ internship: intern: semester: semestr {{ semester, roman }} album: "numer albumu {{ album }}" + kind-requires-dean-approval: "Ten rodzaj praktyki/umowy wymaga akceptacji przez dziekana!" + duration-requires-dean-approval: "Taki okres trwania praktyki wymaga akceptacji przez dziekana!" date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}" duration_2: "{{ duration, weeks }} tygodni" duration_0: "{{ duration, weeks }} tydzień" @@ -195,7 +204,7 @@ steps: draft: > W porozumieniu z firmą w której odbywają się praktyki należy sporządzić Indywidualny Plan Praktyk zgodnie z załączonym szablonem a następnie wysłać go do weryfikacji. Indywidualny Plan Praktyk musi zostać zatwierdzony - oraz podpisany przez Twojego zakłądowego opiekuna praktyki. + oraz podpisany przez Twojego zakładowego opiekuna praktyki. awaiting: > Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i zatwierdzony. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian.