diff --git a/src/api/dto/type.ts b/src/api/dto/type.ts index 213dcb1..611a805 100644 --- a/src/api/dto/type.ts +++ b/src/api/dto/type.ts @@ -19,7 +19,9 @@ export const internshipTypeDtoTransformer: Transformer= 4, } }, reverseTransform(subject: InternshipType, context?: unknown): InternshipTypeDTO { diff --git a/src/components/actions.tsx b/src/components/actions.tsx index 310b241..5259037 100644 --- a/src/components/actions.tsx +++ b/src/components/actions.tsx @@ -1,7 +1,7 @@ import React, { HTMLProps } from "react"; import { useHorizontalSpacing } from "@/styles"; -type ActionsProps = { +export type ActionsProps = { spacing?: number; } & HTMLProps; diff --git a/src/data/internship.ts b/src/data/internship.ts index b819581..e448bc1 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -6,6 +6,8 @@ import { Company, Office } from "@/data/company"; export interface InternshipType extends Identifiable { label: Multilingual, description?: Multilingual, + requiresDeanApproval: boolean, + requiresInsurance: boolean, } export interface InternshipProgramEntry extends Identifiable { diff --git a/src/management/api/index.ts b/src/management/api/index.ts index 5a3e680..e2390f7 100644 --- a/src/management/api/index.ts +++ b/src/management/api/index.ts @@ -1,9 +1,11 @@ import * as edition from "./edition" import * as page from "./page" +import * as type from "./type" export const api = { edition, - page + page, + type } export default api; diff --git a/src/management/api/type.ts b/src/management/api/type.ts new file mode 100644 index 0000000..178f5a1 --- /dev/null +++ b/src/management/api/type.ts @@ -0,0 +1,28 @@ +import { InternshipType } from "@/data"; +import { axios } from "@/api"; +import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type"; +import { encapsulate, OneOrMany } from "@/helpers"; +import { prepare } from "@/routing"; + +const INTERNSHIP_TYPE_INDEX_ENDPOINT = '/internshipTypes' +const INTERNSHIP_TYPE_ENDPOINT = INTERNSHIP_TYPE_INDEX_ENDPOINT + "/:id"; + +export async function all(): Promise { + const response = await axios.get(INTERNSHIP_TYPE_INDEX_ENDPOINT); + return response.data.map(dto => internshipTypeDtoTransformer.transform(dto)) +} + +export async function remove(type: OneOrMany): Promise { + await Promise.all(encapsulate(type).map( + type => axios.delete(prepare(INTERNSHIP_TYPE_ENDPOINT, { id: type.id as string })) + )); +} + +export async function save(type: InternshipType): Promise { + await axios.put( + INTERNSHIP_TYPE_INDEX_ENDPOINT, + internshipTypeDtoTransformer.reverseTransform(type) + ); + + return type; +} diff --git a/src/management/common/BulkActions.tsx b/src/management/common/BulkActions.tsx new file mode 100644 index 0000000..69679ea --- /dev/null +++ b/src/management/common/BulkActions.tsx @@ -0,0 +1,15 @@ +import { Actions, ActionsProps } from "@/components"; +import React from "react"; +import { Typography } from "@material-ui/core"; +import { useTranslation } from "react-i18next"; + +export type BulkActionsProps = ActionsProps; + +export const BulkActions = ({ children, ...props }: BulkActionsProps) => { + const { t } = useTranslation("management"); + + return + { t("actions.bulk") }: + { children } + ; +}; diff --git a/src/management/common/DeleteResourceAction.tsx b/src/management/common/DeleteResourceAction.tsx new file mode 100644 index 0000000..55e3508 --- /dev/null +++ b/src/management/common/DeleteResourceAction.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { OneOrMany } from "@/helpers"; +import useTheme from "@material-ui/core/styles/useTheme"; +import { Trans, useTranslation } from "react-i18next"; +import { Confirm } from "@/components/confirm"; +import { Button, IconButton, Tooltip } from "@material-ui/core"; +import { Delete } from "mdi-material-ui"; +import { createBoundComponent } from "@/management/common/helpers"; + +export type DeleteResourceActionProps = { + onDelete: (resource: OneOrMany) => void; + resource: OneOrMany; + label: (resource: T) => string; + children?: (action: any) => React.ReactNode; +}; + +export function DeleteResourceAction({ onDelete, resource, children, label }: DeleteResourceActionProps) { + const theme = useTheme(); + const { t } = useTranslation("management"); + + const confirmation = <> + { !Array.isArray(resource) + ? + Czy na pewno chcesz usunąć { label(resource) }? + + : <> + { t("confirm.bulk-delete") } +
    + { resource.map(current =>
  • { label(current) }
  • ) } +
+ + } + ; + + return onDelete(resource) } + content={ confirmation } + confirm={ props => + + } + > + { action => children ? children(action) : } + ; +} + +export function createDeleteAction(props: Pick, 'label' | 'onDelete'>) { + return createBoundComponent(DeleteResourceAction, props); +} + diff --git a/src/management/common/MaterialTableTitle.tsx b/src/management/common/MaterialTableTitle.tsx new file mode 100644 index 0000000..ab9dcd8 --- /dev/null +++ b/src/management/common/MaterialTableTitle.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { AsyncResult } from "@/hooks"; +import { CircularProgress } from "@material-ui/core"; + +export type MaterialTableTitleProps = { result: AsyncResult, label: React.ReactNode } & React.HTMLProps; + +export const MaterialTableTitle = ({ label, result, style, ...props }: MaterialTableTitleProps) => +
+ { label } + { result.isLoading && } +
diff --git a/src/management/common/helpers.tsx b/src/management/common/helpers.tsx new file mode 100644 index 0000000..f8dfcfe --- /dev/null +++ b/src/management/common/helpers.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Column } from "material-table"; +import { Actions } from "@/components"; +import { Trans } from "react-i18next"; + +export function actionsColumn(render: (value: T) => React.ReactNode): Column { + return { + title: , + render: value => { render(value) }, + sorting: false, + width: 0, + resizable: false, + removable: false, + searchable: false, + } +} + +export function createBoundComponent(Component: React.ComponentType, bound: Pick) { + return (props: Omit) => ; +} diff --git a/src/management/main.tsx b/src/management/main.tsx index fc91e9d..4a990ce 100644 --- a/src/management/main.tsx +++ b/src/management/main.tsx @@ -4,7 +4,7 @@ import React from "react"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; -import { CalendarClock, FileDocumentMultipleOutline } from "mdi-material-ui"; +import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline } from "mdi-material-ui"; export const Management = { Breadcrumbs: ({ children, ...props }: BreadcrumbsProps) => { @@ -41,6 +41,9 @@ export const ManagementIndex = () => { } route={ route("management:editions") }> { t("management:edition.index.title") } + } route={ route("management:types") }> + { t("management:type.index.title") } + } route={ route("management:static_pages") }> { t("management:page.index.title") } diff --git a/src/management/page/list.tsx b/src/management/page/list.tsx index 15376f3..095e295 100644 --- a/src/management/page/list.tsx +++ b/src/management/page/list.tsx @@ -18,6 +18,12 @@ import { createPortal } from "react-dom"; import { EditStaticPageDialog } from "@/management/page/edit"; import { Confirm } from "@/components/confirm"; import useTheme from "@material-ui/core/styles/useTheme"; +import { BulkActions } from "@/management/common/BulkActions"; +import { MaterialTableTitle } from "@/management/common/MaterialTableTitle"; +import { actionsColumn } from "@/management/common/helpers"; +import { createDeleteAction } from "@/management/common/DeleteResourceAction"; + +const label = (page: StaticPage) => page.title.pl; export const StaticPageManagement = () => { const { t } = useTranslation("management"); @@ -69,46 +75,13 @@ export const StaticPageManagement = () => { } - const DeleteStaticPageAction = ({ page, children }: { page: OneOrMany, children?: (action: any) => React.ReactNode }) => { - const theme = useTheme(); - - const handlePageDeletion = async () => { - await api.page.remove(page); - updatePageList(); - } - - const confirmation = <> - { !Array.isArray(page) - ? - Czy na pewno chcesz usunąć stronę { page.title.pl }? - - : <> - { t("page.confirm.bulk-delete") } -
    - { page.map(page =>
  • { page.title.pl }
  • ) } -
- - } - ; - - return - - } - > - { action => children ? children(action) : } - ; + const handlePageDeletion = async (page: OneOrMany) => { + await api.page.remove(page); + updatePageList(); } + const DeleteStaticPageAction = createDeleteAction({ label, onDelete: handlePageDeletion }) + const PreviewStaticPageAction = ({ page }: { page: StaticPage }) => { const history = useHistory(); const handlePagePreview = async () => history.push(`/${page.slug}`); @@ -127,19 +100,11 @@ export const StaticPageManagement = () => { field: "slug", title: t("page.field.slug"), }, - { - title: t("actions.label"), - render: page => - - - - , - sorting: false, - width: 0, - resizable: false, - removable: false, - searchable: false, - }, + actionsColumn(page => <> + + + + ) ]; const PagePreview = ({ page }: { page: StaticPage }) => @@ -168,15 +133,14 @@ export const StaticPageManagement = () => { - { selected.length > 0 && - { t("actions.bulk") }: - + { selected.length > 0 && + { action => } - } + } { pages => { t("page.index.title") } { result.isLoading && } } + title={ } columns={ columns } data={ pages } detailPanel={ page => } diff --git a/src/management/routing.tsx b/src/management/routing.tsx index 6c02c77..a994e67 100644 --- a/src/management/routing.tsx +++ b/src/management/routing.tsx @@ -4,11 +4,13 @@ import { EditionsManagement } from "@/management/edition/list"; import React from "react"; import { ManagementIndex } from "@/management/main"; import StaticPageManagement from "@/management/page/list"; +import { InternshipTypeManagement } from "@/management/type/list"; export const managementRoutes: Route[] = ([ { name: "index", path: "/", content: ManagementIndex, exact: true }, { name: "editions", path: "/editions", content: EditionsManagement }, + { name: "types", path: "/types", content: InternshipTypeManagement }, { name: "static_pages", path: "/static-pages", content: StaticPageManagement } ] as Route[]).map( ({ name, path, middlewares = [], ...route }): Route => ({ diff --git a/src/management/type/list.tsx b/src/management/type/list.tsx new file mode 100644 index 0000000..7855fab --- /dev/null +++ b/src/management/type/list.tsx @@ -0,0 +1,110 @@ +import { Page } from "@/pages/base"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAsyncState } from "@/hooks"; +import { InternshipType, Multilingual } from "@/data"; +import api from "@/management/api"; +import { Management } from "@/management/main"; +import { Button, Chip, Container, Tooltip, Typography } from "@material-ui/core"; +import { Async } from "@/components/async"; +import MaterialTable, { Column } from "material-table"; +import { MaterialTableTitle } from "@/management/common/MaterialTableTitle"; +import { actionsColumn } from "@/management/common/helpers"; +import { AccountCheck, Delete, Refresh, ShieldCheck } from "mdi-material-ui"; +import { OneOrMany } from "@/helpers"; +import { createDeleteAction } from "@/management/common/DeleteResourceAction"; +import { BulkActions } from "@/management/common/BulkActions"; +import { useSpacing } from "@/styles"; +import { Actions } from "@/components"; + +const title = "type.index.title"; + +const MultilingualCell = ({ value }: { value: Multilingual }) => { + return <> + { Object.keys(value).map(language =>
+ + { value[language as keyof Multilingual] } +
) } + +} + +const label = (type: InternshipType) => type?.label?.pl; + +export const InternshipTypeManagement = () => { + const { t } = useTranslation("management"); + const [result, setTypesPromise] = useAsyncState(); + const [selected, setSelected] = useState([]); + const spacing = useSpacing(2); + + const updateTypeList = () => { + setTypesPromise(api.type.all()); + } + + const handleTypeDelete = async (type: OneOrMany) => { + await api.type.remove(type); + updateTypeList(); + } + + useEffect(updateTypeList, []); + + const DeleteTypeAction = createDeleteAction({ label, onDelete: handleTypeDelete }); + + const columns: Column[] = [ + { + field: "id", + title: "ID", + width: 0, + defaultSort: "asc", + filtering: false, + }, + { + title: t("type.field.label"), + render: type => , + }, + { + title: t("type.field.description"), + render: type => type.description && , + }, + { + title: t("type.field.flags"), + render: type => <> + { type.requiresDeanApproval && } + { type.requiresInsurance && } + , + width: 0, + filtering: true, + sorting: false, + }, + actionsColumn(type => <> + + ) + ]; + + return + + + { t(title) } + + { t(title) } + + + + + + { selected.length > 0 && + + { action => } + + } + { + pages => } + columns={ columns } + data={ pages } + onSelectionChange={ pages => setSelected(pages) } + options={ { selection: true, pageSize: 10 } } + /> + } + + +} diff --git a/translations/management.pl.yaml b/translations/management.pl.yaml index 96f8166..02a238b 100644 --- a/translations/management.pl.yaml +++ b/translations/management.pl.yaml @@ -21,6 +21,17 @@ edition: end: Koniec course: Kierunek +type: + index: + title: "Rodzeje praktyki" + field: + label: "Rodzaj praktyki" + description: "Opis" + flags: "Flagi" + flag: + dean-approval: "Wymaga zgody dziekana" + insurance: "Wymaga ubezpieczenia" + page: index: title: Strony statyczne