diff --git a/src/components/acceptance-action.tsx b/src/components/acceptance-action.tsx index 670e2b9..b613db5 100644 --- a/src/components/acceptance-action.tsx +++ b/src/components/acceptance-action.tsx @@ -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(""); + const classes = useVerticalSpacing(3); + + return + { t(label + ".accept.title") } + + { t(label + ".accept.info") } + setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> + + + + + + + +} + +type DiscardSubmissionDialogProps = { + onDiscard: (comment: string) => void; + label: string; +} & DialogProps; + +export function DiscardSubmissionDialog({ onDiscard, label, ...props }: DiscardSubmissionDialogProps) { + const { t } = useTranslation(); + const [comment, setComment] = useState(""); + const classes = useVerticalSpacing(3); + + return + { t(label + ".accept.title") } + + { t(label + ".accept.info") } + setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> + + + + + + + +} + 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(false); const [isAcceptModalOpen, setAcceptModelOpen] = useState(false); - const [comment, setComment] = useState(""); const [menuAnchor, setMenuAnchor] = useState(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 { createPortal(<> - - { t(label + ".discard.title") } - - { t(label + ".discard.info") } - setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> - - - - - - - - - - { t(label + ".accept.title") } - - { t(label + ".accept.info") } - setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/> - - - - - - - + + , document.getElementById("modals") as Element) } } diff --git a/src/management/api/index.ts b/src/management/api/index.ts index 62e75d7..4215e92 100644 --- a/src/management/api/index.ts +++ b/src/management/api/index.ts @@ -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; diff --git a/src/management/api/internship.ts b/src/management/api/internship.ts new file mode 100644 index 0000000..3f626d9 --- /dev/null +++ b/src/management/api/internship.ts @@ -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 { + return [ + sampleInternship, + ] +} + +export async function get(id: Identifier): Promise { + return sampleInternship; +} + +export async function approve(internship: OneOrMany, comment?: string): Promise {} +export async function decline(internship: OneOrMany, comment: string): Promise {} diff --git a/src/management/edition/internship/common.tsx b/src/management/edition/internship/common.tsx new file mode 100644 index 0000000..e289b3b --- /dev/null +++ b/src/management/edition/internship/common.tsx @@ -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({ + 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: , + awaiting: , + declined: , + draft: + } + + const classes = useStateLabelStyles(); + + const { t } = useTranslation(); + + return +} + +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); diff --git a/src/management/edition/internship/details.tsx b/src/management/edition/internship/details.tsx new file mode 100644 index 0000000..fa65b4a --- /dev/null +++ b/src/management/edition/internship/details.tsx @@ -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 + { internship => + + + { t("edition.internships.title") } + { fullname(internship.intern) } + + { fullname(internship.intern) } + + + + {} } onDiscard={ () => {} } label="internship" /> + + } + +} diff --git a/src/management/edition/internship/list.tsx b/src/management/edition/internship/list.tsx new file mode 100644 index 0000000..389f9c3 --- /dev/null +++ b/src/management/edition/internship/list.tsx @@ -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(); + const [selected, setSelected] = useState([]); + 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 <> + setOpen(true) }> + { createPortal( + 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 <> + setOpen(true) }> + { createPortal( + handleSubmissionDiscard(internship, comment) } label="internship" open={ open } onClose={ () => setOpen(false) }/>, + document.getElementById("modals") as Element, + ) } + ; + } + + const columns: Column[] = [ + { + 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 => + }, + actionsColumn(internship => <> + { canAccept(internship) && } + { canDiscard(internship) && } + + ) + ]; + + return + + + { t(title) } + + { t(title) } + + + + + + { selected.length > 0 && + handleSubmissionAccept(selected, comment) } onDiscard={ comment => handleSubmissionDiscard(selected, comment) } /> + } + { + internships => } + columns={ columns } + data={ internships } + onSelectionChange={ internships => setSelected(internships) } + options={ { selection: true, pageSize: 10 } } + detailPanel={ internship => } + /> + } + + +} diff --git a/src/management/edition/manage.tsx b/src/management/edition/manage.tsx index d1a8b0f..fefbf77 100644 --- a/src/management/edition/manage.tsx +++ b/src/management/edition/manage.tsx @@ -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) => { } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> { t("management:edition.students.title") } - } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + } route={ route("management:edition_internships", { edition: edition.id || "" }) }> { t("management:edition.internships.title") } } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> @@ -78,7 +79,7 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => { } -EditionManagement.Breadcrumbs = ({ children }: { children: React.ReactChild }) => { +EditionManagement.Breadcrumbs = ({ children }: { children: OneOrMany }) => { const edition = useContext(EditionContext); return diff --git a/src/management/routing.tsx b/src/management/routing.tsx index 525e944..86e8505 100644 --- a/src/management/routing.tsx +++ b/src/management/routing.tsx @@ -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 }, diff --git a/src/pages/base.tsx b/src/pages/base.tsx index bc6e5cc..ab07980 100644 --- a/src/pages/base.tsx +++ b/src/pages/base.tsx @@ -8,7 +8,7 @@ export type PageProps = { } & BoxProps; export type PageHeaderProps = { - maxWidth?: "sm" | "md" | "lg" | false + maxWidth?: "sm" | "md" | "lg" | "xl" | false } & HTMLProps export const Page = ({ title, children, ...props }: PageProps) => { diff --git a/translations/management.pl.yaml b/translations/management.pl.yaml index 7b63159..f026967 100644 --- a/translations/management.pl.yaml +++ b/translations/management.pl.yaml @@ -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: diff --git a/yarn.lock b/yarn.lock index 1ee59b7..6246448 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"