Intership listening

This commit is contained in:
Kacper Donat 2021-01-09 22:47:39 +01:00
parent a38409e2d0
commit e31d89b688
11 changed files with 359 additions and 52 deletions

View File

@ -1,10 +1,79 @@
import React, { useState } from "react";
import { Button, ButtonGroup, Dialog, DialogActions, DialogContent, DialogTitle, Menu, MenuItem, TextField, Typography } from "@material-ui/core";
import {
Button,
ButtonGroup,
ButtonProps,
Dialog,
DialogActions,
DialogContent,
DialogProps,
DialogTitle,
Menu,
MenuItem,
TextField,
Typography
} from "@material-ui/core";
import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
import { useTranslation } from "react-i18next";
import { useVerticalSpacing } from "@/styles";
import { createPortal } from "react-dom";
type AcceptSubmissionDialogProps = {
onAccept: (comment?: string) => void;
label: string;
} & DialogProps;
export function AcceptSubmissionDialog({ onAccept, label, ...props }: AcceptSubmissionDialogProps) {
const { t } = useTranslation();
const [comment, setComment] = useState<string>("");
const classes = useVerticalSpacing(3);
return <Dialog maxWidth="md" { ...props }>
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
<DialogContent className={ classes.root }>
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
<DialogActions>
<Button onClick={ ev => props.onClose?.(ev, "backdropClick") }>
{ t('cancel') }
</Button>
<Button onClick={ () => onAccept?.(comment) } color="primary" variant="contained">
{ t('confirm') }
</Button>
</DialogActions>
</DialogContent>
</Dialog>
}
type DiscardSubmissionDialogProps = {
onDiscard: (comment: string) => void;
label: string;
} & DialogProps;
export function DiscardSubmissionDialog({ onDiscard, label, ...props }: DiscardSubmissionDialogProps) {
const { t } = useTranslation();
const [comment, setComment] = useState<string>("");
const classes = useVerticalSpacing(3);
return <Dialog maxWidth="md" { ...props }>
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
<DialogContent className={ classes.root }>
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
<DialogActions>
<Button onClick={ ev => props.onClose?.(ev, "backdropClick") }>
{ t('cancel') }
</Button>
<Button onClick={ () => onDiscard?.(comment) } color="primary" variant="contained">
{ t('confirm') }
</Button>
</DialogActions>
</DialogContent>
</Dialog>
}
type AcceptanceActionsProps = {
onAccept: (comment?: string) => void;
onDiscard: (comment: string) => void;
@ -17,19 +86,8 @@ export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActi
const [isDiscardModalOpen, setDiscardModelOpen] = useState<boolean>(false);
const [isAcceptModalOpen, setAcceptModelOpen] = useState<boolean>(false);
const [comment, setComment] = useState<string>("");
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
const classes = useVerticalSpacing(3);
const handleAccept = () => {
onAccept(comment);
}
const handleDiscard = () => {
onDiscard(comment);
}
const handleAcceptModalClose = () => {
setAcceptModelOpen(false);
}
@ -77,39 +135,8 @@ export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActi
</Button>
{ createPortal(<>
<Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md">
<DialogTitle>{ t(label + ".discard.title") }</DialogTitle>
<DialogContent className={ classes.root }>
<Typography variant="body1">{ t(label + ".discard.info") }</Typography>
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
<DialogActions>
<Button onClick={ handleDiscardModalClose }>
{ t('cancel') }
</Button>
<Button onClick={ handleDiscard } color="primary" variant="contained">
{ t('confirm') }
</Button>
</DialogActions>
</DialogContent>
</Dialog>
<Dialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md">
<DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
<DialogContent className={ classes.root }>
<Typography variant="body1">{ t(label + ".accept.info") }</Typography>
<TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
<DialogActions>
<Button onClick={ handleAcceptModalClose }>
{ t('cancel') }
</Button>
<Button onClick={ handleAccept } color="primary" variant="contained">
{ t('confirm') }
</Button>
</DialogActions>
</DialogContent>
</Dialog>
<DiscardSubmissionDialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md" onDiscard={ onDiscard } label={ label }/>
<AcceptSubmissionDialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md" onAccept={ onAccept } label={ label }/>
</>, document.getElementById("modals") as Element) }
</>
}

View File

@ -2,12 +2,14 @@ import * as edition from "./edition"
import * as page from "./page"
import * as type from "./type"
import * as course from "./course"
import * as internship from "./internship"
export const api = {
edition,
page,
type,
course,
internship
}
export default api;

View File

@ -0,0 +1,58 @@
import { Identifiable, Identifier, Internship } from "@/data";
import { sampleCompanies, sampleStudent } from "@/provider/dummy";
import moment, { Moment } from "moment-timezone";
import { OneOrMany } from "@/helpers";
import { SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
export type InternshipSubmission = Internship & {
state: SubmissionStatus,
changed: Moment
}
const sampleInternship: InternshipSubmission = {
id: "test",
company: sampleCompanies[0],
startDate: moment('2020-07-01'),
endDate: moment('2020-08-31'),
hours: 180,
intern: sampleStudent,
isAccepted: false,
lengthInWeeks: 0,
mentor: {
email: "test@test.com",
name: "Test",
surname: "Testowy",
phone: "+48 123 456 789"
},
office: sampleCompanies[0].offices[0],
program: [
{ description: "Test" },
],
type: {
description: {
pl: "Przykładowy typ",
en: "Example type"
},
label: {
pl: "Przykładowy",
en: "Example"
},
requiresDeanApproval: true,
requiresInsurance: false
},
state: "declined",
changed: moment(),
}
export async function all(edition: Identifiable): Promise<InternshipSubmission[]> {
return [
sampleInternship,
]
}
export async function get(id: Identifier): Promise<InternshipSubmission> {
return sampleInternship;
}
export async function approve(internship: OneOrMany<Internship>, comment?: string): Promise<void> {}
export async function decline(internship: OneOrMany<Internship>, comment: string): Promise<void> {}

View File

@ -0,0 +1,47 @@
import { SubmissionStatus } from "@/state/reducer/submission";
import React from "react";
import { ClockOutline, NotebookCheckOutline, NotebookEditOutline, NotebookRemoveOutline } from "mdi-material-ui";
import { useTranslation } from "react-i18next";
import { Chip } from "@material-ui/core";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import { green, orange, red } from "@material-ui/core/colors";
import { InternshipSubmission } from "@/management/api/internship";
const useStateLabelStyles = makeStyles((theme: Theme) => createStyles<SubmissionStatus, {}>({
awaiting: {
borderColor: orange["800"],
color: orange["800"],
},
declined: {
borderColor: red["600"],
color: red["600"],
},
draft: {},
accepted: {
borderColor: green["600"],
color: green["600"]
}
}))
export type StateLabelProps = {
state: SubmissionStatus;
};
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 <Chip icon={ icons[state] } label={ t(`translation:submission.status.${ state }`) } variant="outlined" className={ classes[state] }/>
}
export const canEdit = (internship: InternshipSubmission) => internship.state != "draft";
export const canAccept = (internship: InternshipSubmission) => ["declined", "awaiting"].includes(internship.state);
export const canDiscard = (internship: InternshipSubmission) => ["accepted", "awaiting"].includes(internship.state);

View File

@ -0,0 +1,39 @@
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,119 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncState } from "@/hooks";
import { useSpacing } from "@/styles";
import api from "@/management/api";
import { Box, Button, Container, IconButton, 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 { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import { AcceptanceActions, 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";
const title = "edition.internships.title";
export const InternshipManagement = ({ edition }: EditionManagementProps) => {
const { t } = useTranslation("management");
const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
const [selected, setSelected] = useState<InternshipSubmission[]>([]);
const spacing = useSpacing(2);
const updateInternshipList = () => {
setInternshipsPromise(api.internship.all(edition));
}
useEffect(updateInternshipList, []);
const handleSubmissionAccept = api.internship.approve
const handleSubmissionDiscard = api.internship.decline
const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
const [open, setOpen] = useState(false);
return <>
<IconButton onClick={ () => setOpen(true) }><StickerCheckOutline /></IconButton>
{ createPortal(
<AcceptSubmissionDialog onAccept={ comment => handleSubmissionAccept(internship, comment) } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
document.getElementById("modals") as Element,
) }
</>;
}
const DiscardAction = ({ internship }: { internship: InternshipSubmission }) => {
const [open, setOpen] = useState(false);
return <>
<IconButton onClick={ () => setOpen(true) }><StickerRemoveOutline /></IconButton>
{ createPortal(
<DiscardSubmissionDialog onDiscard={ comment => handleSubmissionDiscard(internship, comment) } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
document.getElementById("modals") as Element,
) }
</>;
}
const columns: Column<InternshipSubmission>[] = [
{
title: t("internship.column.student"),
render: internship => <>{internship.intern.name} {internship.intern.surname}</>,
},
{
title: t("internship.column.album"),
field: "intern.albumNumber",
},
{
title: t("internship.column.type"),
field: "type.label.pl",
},
{
title: t("internship.column.changed"),
render: summary => summary.changed.format("yyyy-MM-DD HH:mm")
},
{
title: t("internship.column.status"),
render: summary => <StateLabel state={ summary.state } />
},
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>
</>)
];
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>
<AcceptanceActions label="internship" onAccept={ comment => handleSubmissionAccept(selected, comment) } onDiscard={ comment => handleSubmissionDiscard(selected, comment) } />
</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={ internship => <Box m={ 3 }><ProposalPreview proposal={ internship } /></Box> }
/>
}</Async>
</Container>
</Page>
}

View File

@ -12,6 +12,7 @@ import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import { useAsync } from "@/hooks";
import api from "@/management/api";
import { Async } from "@/components/async";
import { OneOrMany } from "@/helpers";
const useSectionStyles = makeStyles((theme: Theme) => createStyles({
header: {
@ -49,7 +50,7 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
<ManagementLink icon={ <AccountMultiple/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
{ t("management:edition.students.title") }
</ManagementLink>
<ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
<ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_internships", { edition: edition.id || "" }) }>
{ t("management:edition.internships.title") }
</ManagementLink>
<ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
@ -78,7 +79,7 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
</Page>
}
EditionManagement.Breadcrumbs = ({ children }: { children: React.ReactChild }) => {
EditionManagement.Breadcrumbs = ({ children }: { children: OneOrMany<React.ReactChild> }) => {
const edition = useContext<Edition | null>(EditionContext);
return <Management.Breadcrumbs>

View File

@ -8,6 +8,8 @@ import { InternshipTypeManagement } from "@/management/type/list";
import { EditionRouter, EditionManagement } from "@/management/edition/manage";
import { EditionReportFields } from "@/management/edition/report/fields/list";
import { EditionSettings } from "@/management/edition/settings";
import { InternshipManagement } from "@/management/edition/internship/list";
import { InternshipDetails } from "@/management/edition/internship/details";
export const managementRoutes: Route[] = ([
{ name: "index", path: "/", content: ManagementIndex, exact: true },
@ -15,7 +17,9 @@ export const managementRoutes: Route[] = ([
{ name: "edition_router", path: "/editions/:edition", content: EditionRouter },
{ name: "edition_report_form", path: "/editions/:edition/report", content: EditionReportFields, tags: ["edition"] },
{ name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] },
{ name: "edition_manage", path: "/editions/:edition", content: EditionManagement, 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: "editions", path: "/editions", content: EditionsManagement },
{ name: "types", path: "/types", content: InternshipTypeManagement },

View File

@ -8,7 +8,7 @@ export type PageProps = {
} & BoxProps;
export type PageHeaderProps = {
maxWidth?: "sm" | "md" | "lg" | false
maxWidth?: "sm" | "md" | "lg" | "xl" | false
} & HTMLProps<HTMLDivElement>
export const Page = ({ title, children, ...props }: PageProps) => {

View File

@ -13,7 +13,17 @@ actions:
edit: Edytuj
add: Dodaj
internship:
column:
student: Imię i Nazwisko
album: Numer Albumu
type: Rodzaj praktyki
status: Status
changed: Data aktualizacji
edition:
internships:
title: Zgłoszenia praktyk
index:
title: "Edycje praktyk"
field:

View File

@ -6842,9 +6842,9 @@ md5.js@^1.3.4:
safe-buffer "^5.1.2"
mdi-material-ui@^6.17.0:
version "6.17.0"
resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.17.0.tgz#da69f0b7d7c6fc2255e6007ed8b8ca858c1aede7"
integrity sha512-eOprRu31lklPIS1WGe3cM0G/8glKl1WKRvewxjDrgXH2Ryxxg7uQ+uwDUwUEONtLku0p2ZOLzgXUIy2uRy5rLg==
version "6.21.0"
resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.21.0.tgz#e052215b0534e6c20abeb7e89c3fd8a421a519fd"
integrity sha512-rcO7KmaOhZq4H7vHYpwnjMqHfuJh0PmpEJNssEofWaqoSEABmIwRHUNmdJDPrjrBCTUm4m7tpYexqPOYzkb1Eg==
mdn-data@2.0.4:
version "2.0.4"