From 67fe75958e026347a23047a7e56e7c4798f40aef Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 16:54:57 +0200 Subject: [PATCH 1/8] feat(users): add users edit action with tooltip --- src/components/users/data-table/index.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/users/data-table/index.tsx b/src/components/users/data-table/index.tsx index b0b6da0..ce75a51 100644 --- a/src/components/users/data-table/index.tsx +++ b/src/components/users/data-table/index.tsx @@ -222,6 +222,16 @@ export const UsersDataTable = (): JSX.Element => { }) } />, + + toast('To edit user, double click on the row', { + position: 'top-right', + }) + } + />, Date: Sat, 21 Oct 2023 16:55:17 +0200 Subject: [PATCH 2/8] feat: add filter button in the toolbar for all data grids --- src/components/data-grid/toolbar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/data-grid/toolbar.tsx b/src/components/data-grid/toolbar.tsx index fec9671..80d3e0d 100644 --- a/src/components/data-grid/toolbar.tsx +++ b/src/components/data-grid/toolbar.tsx @@ -2,6 +2,7 @@ import { GridRowId, GridToolbarColumnsButton, GridToolbarContainer, + GridToolbarFilterButton, } from '@mui/x-data-grid'; import { JSX } from 'react'; import Box from '@mui/material/Box'; @@ -23,6 +24,7 @@ export const DataGridToolbar = ({ {prependButtons} + {appendButtons} {!!selection.length && endButtons} From 8aebc4c2534baba414ab1141beb054c61f5b19ca Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 17:37:15 +0200 Subject: [PATCH 3/8] refactor: remove useless console.log --- src/utils/modal/modals/manage-roles.modal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/modal/modals/manage-roles.modal.tsx b/src/utils/modal/modals/manage-roles.modal.tsx index 4685fb2..2ba2df3 100644 --- a/src/utils/modal/modals/manage-roles.modal.tsx +++ b/src/utils/modal/modals/manage-roles.modal.tsx @@ -49,7 +49,6 @@ export const ManageRolesModal = ({ useEffect(() => { if (query?.data && userRoles === null) { - console.log(query.data); const { [E_USER_ENTITY_KEYS.ROLES]: roles } = query.data; setUserRoles(map(roles, (role) => role[E_ROLE_ENTITY_KEYS.NAME])); From 50d60b41aeaa12fee79e034b8b7d49555b99a582 Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 17:49:47 +0200 Subject: [PATCH 4/8] feat(courses): add course modal --- src/api/courses/types.ts | 19 ++ src/store/modals/reducer.ts | 15 + src/utils/modal/base-modal.tsx | 5 + src/utils/modal/modals/course-form.modal.tsx | 296 +++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 src/api/courses/types.ts create mode 100644 src/utils/modal/modals/course-form.modal.tsx diff --git a/src/api/courses/types.ts b/src/api/courses/types.ts new file mode 100644 index 0000000..06bfdfa --- /dev/null +++ b/src/api/courses/types.ts @@ -0,0 +1,19 @@ +import { TApiUser } from '../user/types'; + +export enum E_COURSE_ENTITY_KEYS { + ABBR = 'abbr', + NAME = 'name', + CREDITS = 'credits', + ANNOTATION = 'annotation', + GUARANTOR = 'guarantor', + TEACHERS = 'teachers', +} + +export type TPureCourse = { + [E_COURSE_ENTITY_KEYS.ABBR]: string; + [E_COURSE_ENTITY_KEYS.NAME]: string; + [E_COURSE_ENTITY_KEYS.CREDITS]: number; + [E_COURSE_ENTITY_KEYS.ANNOTATION]: string; + [E_COURSE_ENTITY_KEYS.GUARANTOR]: TApiUser; + [E_COURSE_ENTITY_KEYS.TEACHERS]: Array; +}; diff --git a/src/store/modals/reducer.ts b/src/store/modals/reducer.ts index 5034a3a..d5449c0 100644 --- a/src/store/modals/reducer.ts +++ b/src/store/modals/reducer.ts @@ -3,10 +3,14 @@ import { closeModal, openModal } from './actions'; import { findLastIndex } from 'lodash'; import { TUserCreateData } from '../../api/user/user.service'; import { E_ROLE } from '../../api/user/types'; +import { E_MODAL_MODE } from '../../utils/modal/base-modal'; +import { TPureCourse } from '../../api/courses/types'; +import { TCreateCourseData } from '../../api/courses/course.service'; export enum E_MODALS { MANAGE_ROLES = 'manage-roles.modal', ADD_NEW_USER = 'add-new-user.modal', + COURSE_FORM = 'course-form.modal', } export type TModalMapItem = { @@ -23,6 +27,17 @@ export type TModalMetaMap = { userID: string; onSuccess(userID: string, roles: Array): void; }; + [E_MODALS.COURSE_FORM]: { initialData?: Partial } & ( + | { + mode: E_MODAL_MODE.CREATE; + onSuccess(data: TCreateCourseData): void; + } + | { + mode: E_MODAL_MODE.UPDATE; + abbr: string; + onSuccess(abbr: string, data: TCreateCourseData): void; + } + ); }; export type TModalState = { diff --git a/src/utils/modal/base-modal.tsx b/src/utils/modal/base-modal.tsx index 1c4b6bf..52dc873 100644 --- a/src/utils/modal/base-modal.tsx +++ b/src/utils/modal/base-modal.tsx @@ -7,6 +7,11 @@ import { DialogTitle, } from '@mui/material'; +export enum E_MODAL_MODE { + CREATE = 'create', + UPDATE = 'update', +} + export type TCommonModalProps = { isOpen: boolean; onClose(): void; diff --git a/src/utils/modal/modals/course-form.modal.tsx b/src/utils/modal/modals/course-form.modal.tsx new file mode 100644 index 0000000..235cb73 --- /dev/null +++ b/src/utils/modal/modals/course-form.modal.tsx @@ -0,0 +1,296 @@ +import { + Autocomplete, + Button, + Chip, + FormControl, + FormGroup, + InputLabel, + MenuItem, + Select, + Stack, + TextField, +} from '@mui/material'; +import { E_USER_ENTITY_KEYS, TApiUser } from '../../../api/user/types'; +import { + FormEvent, + JSX, + SyntheticEvent, + useEffect, + useMemo, + useState, +} from 'react'; +import { every, filter, map, mapValues, omit, toString } from 'lodash'; +import { BaseModal, E_MODAL_MODE, TCommonModalProps } from '../base-modal'; +import Box from '@mui/material/Box'; +import { E_MODALS, TDynModalMeta } from '../../../store/modals'; +import { E_COURSE_ENTITY_KEYS } from '../../../api/courses/types'; +import { TCreateCourseData } from '../../../api/courses/course.service'; +import { useQuery } from '@tanstack/react-query'; +import UserService from '../../../api/user/user.service'; +import { SelectChangeEvent } from '@mui/material/Select/SelectInput'; +import { AutocompleteValue } from '@mui/base/useAutocomplete/useAutocomplete'; + +export type TCourseFormModalProps = TCommonModalProps & + TDynModalMeta; + +export type TCourseFormModalData = Omit< + TCreateCourseData, + E_COURSE_ENTITY_KEYS.CREDITS | E_COURSE_ENTITY_KEYS.TEACHERS +> & { + [E_COURSE_ENTITY_KEYS.CREDITS]: string; + [E_COURSE_ENTITY_KEYS.TEACHERS]: Array; +}; + +const CourseFormModal = ({ + onClose, + onSuccess, + mode, + initialData, + ...rest +}: TCourseFormModalProps) => { + const { data: usersData } = useQuery({ + queryKey: ['getUsers'], + queryFn: UserService.getUsers.bind(UserService), + }); + + const [data, setData] = useState({ + [E_COURSE_ENTITY_KEYS.ABBR]: '', + [E_COURSE_ENTITY_KEYS.NAME]: '', + [E_COURSE_ENTITY_KEYS.ANNOTATION]: '', + [E_COURSE_ENTITY_KEYS.CREDITS]: '', + [E_COURSE_ENTITY_KEYS.GUARANTOR]: '', + [E_COURSE_ENTITY_KEYS.TEACHERS]: [], + }); + + useEffect(() => { + if (initialData) + setData((prev) => ({ + ...prev, + ...mapValues( + omit(initialData, [ + E_COURSE_ENTITY_KEYS.CREDITS, + E_COURSE_ENTITY_KEYS.GUARANTOR, + E_COURSE_ENTITY_KEYS.TEACHERS, + ]), + toString, + ), + ...(initialData[E_COURSE_ENTITY_KEYS.CREDITS] && { + [E_COURSE_ENTITY_KEYS.CREDITS]: toString( + initialData[E_COURSE_ENTITY_KEYS.CREDITS], + ), + }), + ...(initialData[E_COURSE_ENTITY_KEYS.GUARANTOR] && { + [E_COURSE_ENTITY_KEYS.GUARANTOR]: + initialData[E_COURSE_ENTITY_KEYS.GUARANTOR][E_USER_ENTITY_KEYS.ID], + }), + ...(initialData[E_COURSE_ENTITY_KEYS.TEACHERS] && { + [E_COURSE_ENTITY_KEYS.TEACHERS]: + initialData[E_COURSE_ENTITY_KEYS.TEACHERS], + }), + })); + }, [initialData]); + + const isSaveDisabled = useMemo(() => { + const { + [E_COURSE_ENTITY_KEYS.ABBR]: abbreviation, + [E_COURSE_ENTITY_KEYS.NAME]: name, + [E_COURSE_ENTITY_KEYS.CREDITS]: credits, + } = data; + + return !abbreviation || !name || !credits; + }, [data]); + + const handleFieldChange = (event: FormEvent) => { + const { name, value } = event.target as HTMLInputElement; + + setData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleGuarantorChange = (event: SelectChangeEvent) => { + setData((prev) => ({ + ...prev, + [E_COURSE_ENTITY_KEYS.GUARANTOR]: event.target.value, + })); + }; + + const handleTeachersChange = ( + _: SyntheticEvent, + newValue: AutocompleteValue, + ) => { + setData((prev) => ({ + ...prev, + [E_COURSE_ENTITY_KEYS.TEACHERS]: newValue, + })); + }; + + const filterFunc = (options: Array): Array => + filter(options, (option) => + every( + data[E_COURSE_ENTITY_KEYS.TEACHERS], + (teacher) => + teacher[E_USER_ENTITY_KEYS.ID] !== option[E_USER_ENTITY_KEYS.ID], + ), + ); + + const handleModalClose = ( + _: Record, + reason: 'backdropClick' | 'escapeKeyDown', + ) => { + if (reason === 'backdropClick') return; + + onClose(); + }; + + const handleClose = () => { + onClose(); + }; + + const handleSave = (event: FormEvent) => { + event.preventDefault(); + + if (isSaveDisabled) return; + + if (mode === E_MODAL_MODE.CREATE) { + onSuccess({ + ...data, + [E_COURSE_ENTITY_KEYS.CREDITS]: parseInt( + data[E_COURSE_ENTITY_KEYS.CREDITS], + ), + [E_COURSE_ENTITY_KEYS.TEACHERS]: map( + data[E_COURSE_ENTITY_KEYS.TEACHERS], + E_USER_ENTITY_KEYS.ID, + ), + }); + } + + if (mode === E_MODAL_MODE.UPDATE) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const abbr = rest.abbr; + + onSuccess(abbr, { + ...data, + [E_COURSE_ENTITY_KEYS.CREDITS]: parseInt( + data[E_COURSE_ENTITY_KEYS.CREDITS], + ), + [E_COURSE_ENTITY_KEYS.TEACHERS]: map( + data[E_COURSE_ENTITY_KEYS.TEACHERS], + E_USER_ENTITY_KEYS.ID, + ), + }); + } + }; + + const footer = (): JSX.Element => ( + + + + + ); + + return ( + + + + + + + + + + Guarantor + + + ( + + )} + value={data[E_COURSE_ENTITY_KEYS.TEACHERS]} + onChange={handleTeachersChange} + renderTags={(tags, getTagProps) => + tags.map((tag, index) => ( + // eslint-disable-next-line react/jsx-key + + )) + } + filterOptions={filterFunc} + options={usersData ?? []} + getOptionLabel={(option) => + `${option[E_USER_ENTITY_KEYS.FIRST_NAME]} ${ + option[E_USER_ENTITY_KEYS.LAST_NAME] + }` + } + /> + + + ); +}; + +export default CourseFormModal; From bcbed4162107c80c732cec6944209e212ad03b46 Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 17:49:52 +0200 Subject: [PATCH 5/8] feat(courses): add course service --- src/api/courses/course.service.ts | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/api/courses/course.service.ts diff --git a/src/api/courses/course.service.ts b/src/api/courses/course.service.ts new file mode 100644 index 0000000..f360d15 --- /dev/null +++ b/src/api/courses/course.service.ts @@ -0,0 +1,64 @@ +import { BaseService } from '../base/service'; +import Api from '../base/api'; +import { E_COURSE_ENTITY_KEYS, TPureCourse } from './types'; + +export type TCreateCourseData = Omit< + TPureCourse, + | E_COURSE_ENTITY_KEYS.ANNOTATION + | E_COURSE_ENTITY_KEYS.CREDITS + | E_COURSE_ENTITY_KEYS.GUARANTOR + | E_COURSE_ENTITY_KEYS.TEACHERS +> & { + [E_COURSE_ENTITY_KEYS.CREDITS]: number; + [E_COURSE_ENTITY_KEYS.ANNOTATION]?: string; + [E_COURSE_ENTITY_KEYS.GUARANTOR]: string; + [E_COURSE_ENTITY_KEYS.TEACHERS]?: Array; +}; + +export type TUpdateCourseData = Partial; + +export type TCourseCreateMutationVariables = { + data: TCreateCourseData; +}; + +export type TCourseUpdateMutationVariables = { + [E_COURSE_ENTITY_KEYS.ABBR]: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR]; + data: TUpdateCourseData; +}; + +export type TCourseDeleteMutationVariables = { + [E_COURSE_ENTITY_KEYS.ABBR]: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR]; +}; + +export default class CourseService extends BaseService { + protected static readonly endpoint = '/courses'; + + public static async getCourses(): Promise> { + return await Api.instance.get>(this.endpoint); + } + + public static async createCourse( + data: TCreateCourseData, + ): Promise { + return await Api.instance.post( + this.endpoint, + data, + ); + } + + public static async updateCourse( + abbr: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR], + data: TUpdateCourseData, + ): Promise { + return await Api.instance.put( + `${this.endpoint}/${abbr}`, + data, + ); + } + + public static async deleteCourse( + abbr: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR], + ): Promise { + await Api.instance.delete(`${this.endpoint}/${abbr}`); + } +} From 2df35e2ad3723089748a7db248927cab9793edfb Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 17:49:59 +0200 Subject: [PATCH 6/8] feat(courses): add course data table --- src/components/courses/data-table/error.tsx | 44 +++ src/components/courses/data-table/index.tsx | 364 ++++++++++++++++++ src/components/courses/data-table/toolbar.tsx | 51 +++ 3 files changed, 459 insertions(+) create mode 100644 src/components/courses/data-table/error.tsx create mode 100644 src/components/courses/data-table/index.tsx create mode 100644 src/components/courses/data-table/toolbar.tsx diff --git a/src/components/courses/data-table/error.tsx b/src/components/courses/data-table/error.tsx new file mode 100644 index 0000000..7847fde --- /dev/null +++ b/src/components/courses/data-table/error.tsx @@ -0,0 +1,44 @@ +import { + Button, + Card, + CardContent, + CircularProgress, + Stack, + Typography, +} from '@mui/material'; +import { TApiError } from '../../../api/base/types'; +import { JSX } from 'react'; + +export type TCourseDataTableErrorProps = { + isLoading: boolean; + error: TApiError | null; + refetch(): void; +}; + +export const CourseDataTableError = ({ + isLoading, + error, + refetch, +}: TCourseDataTableErrorProps): JSX.Element => ( + + + + {isLoading && } + {error && ( + <> + + Error: {error?.message ?? 'Unknown error'} + + + + )} + + + +); diff --git a/src/components/courses/data-table/index.tsx b/src/components/courses/data-table/index.tsx new file mode 100644 index 0000000..df2c318 --- /dev/null +++ b/src/components/courses/data-table/index.tsx @@ -0,0 +1,364 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import CourseService, { + TCourseCreateMutationVariables, + TCourseDeleteMutationVariables, + TCourseUpdateMutationVariables, + TCreateCourseData, + TUpdateCourseData, +} from '../../../api/courses/course.service'; +import { JSX, useEffect, useState } from 'react'; +import { E_COURSE_ENTITY_KEYS, TPureCourse } from '../../../api/courses/types'; +import { + DataGrid, + GridActionsCellItem, + GridColDef, + GridRowId, +} from '@mui/x-data-grid'; +import { E_USER_ENTITY_KEYS } from '../../../api/user/types'; +import { chipSelectColDef } from '../../data-grid/chip-select'; +import { + differenceWith, + forEach, + isEmpty, + isEqual, + omit, + pick, + toString, + unionBy, +} from 'lodash'; +import { + GridRenderCellParams, + GridValueFormatterParams, +} from '@mui/x-data-grid/models/params/gridCellParams'; +import { LinearProgress } from '@mui/material'; +import { TApiError } from '../../../api/base/types'; +import { CourseDataTableError } from './error'; +import { CourseDataTableToolbar } from './toolbar'; +import { OpenInNew } from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; +import { useModal } from '../../../utils/hooks/useModal'; +import { E_MODALS } from '../../../store/modals'; +import { E_MODAL_MODE } from '../../../utils/modal/base-modal'; +import { toast } from 'react-hot-toast'; + +export const CoursesDataTable = (): JSX.Element => { + const navigate = useNavigate(); + + const { onOpen: openCourseFormModal, onClose: closeCourseFormModal } = + useModal(E_MODALS.COURSE_FORM); + + const { data, isLoading, isFetching, error, refetch } = useQuery< + Array, + TApiError + >({ + queryKey: ['getCourses'], + queryFn: CourseService.getCourses.bind(CourseService), + }); + + const createMutation = useMutation< + TPureCourse, + TApiError, + TCourseCreateMutationVariables + >({ + mutationFn: async ({ data: createData }: TCourseCreateMutationVariables) => + CourseService.createCourse(createData), + onSuccess: async () => { + await refetch(); + closeCourseFormModal(); + + toast.success('Course created successfully'); + }, + onError: async () => { + await refetch(); + + toast.error('Failed to create course'); + }, + }); + + const updateMutation = useMutation< + TPureCourse, + TApiError, + TCourseUpdateMutationVariables + >({ + mutationFn: async ({ + [E_COURSE_ENTITY_KEYS.ABBR]: abbr, + data: updateData, + }: TCourseUpdateMutationVariables) => + CourseService.updateCourse(abbr, updateData), + onSuccess: async () => { + await refetch(); + closeCourseFormModal(); + + toast.success('Course updated successfully!'); + }, + onError: async () => { + await refetch(); + + toast.error('Failed to update course'); + }, + }); + + const deleteMutation = useMutation< + void, + TApiError, + TCourseDeleteMutationVariables + >({ + mutationFn: async ({ + [E_COURSE_ENTITY_KEYS.ABBR]: abbr, + }: TCourseDeleteMutationVariables) => CourseService.deleteCourse(abbr), + onSuccess: async () => { + await refetch(); + + toast.success('Course deleted successfully'); + }, + onError: async () => { + await refetch(); + + toast.error('Failed to delete course'); + }, + }); + + const [rows, setRows] = useState>([]); + const [rowSelection, setRowSelection] = useState>([]); + + useEffect(() => { + if (data && !isFetching) setRows(data); + }, [data, isFetching]); + + const handleCreateSuccess = (createData: TCreateCourseData) => { + const pureData = pick(createData, [ + E_COURSE_ENTITY_KEYS.ABBR, + E_COURSE_ENTITY_KEYS.NAME, + E_COURSE_ENTITY_KEYS.CREDITS, + E_COURSE_ENTITY_KEYS.GUARANTOR, + ]); + createMutation.mutate({ + data: { + ...pureData, + ...(createData[E_COURSE_ENTITY_KEYS.ANNOTATION] && { + [E_COURSE_ENTITY_KEYS.ANNOTATION]: + createData[E_COURSE_ENTITY_KEYS.ANNOTATION], + }), + ...(createData[E_COURSE_ENTITY_KEYS.TEACHERS] && { + [E_COURSE_ENTITY_KEYS.TEACHERS]: + createData[E_COURSE_ENTITY_KEYS.TEACHERS], + }), + }, + }); + }; + + const handleUpdateSuccess = ( + abbr: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR], + updateData: TUpdateCourseData, + ) => { + const pureData = pick(updateData, [ + E_COURSE_ENTITY_KEYS.ABBR, + E_COURSE_ENTITY_KEYS.NAME, + E_COURSE_ENTITY_KEYS.CREDITS, + E_COURSE_ENTITY_KEYS.GUARANTOR, + ]); + updateMutation.mutate({ + abbr, + data: { + ...pureData, + ...(updateData[E_COURSE_ENTITY_KEYS.ANNOTATION] && { + [E_COURSE_ENTITY_KEYS.ANNOTATION]: + updateData[E_COURSE_ENTITY_KEYS.ANNOTATION], + }), + ...(updateData[E_COURSE_ENTITY_KEYS.TEACHERS] && { + [E_COURSE_ENTITY_KEYS.TEACHERS]: + updateData[E_COURSE_ENTITY_KEYS.TEACHERS], + }), + }, + }); + }; + + const handleDeleteSelected = () => { + forEach(rowSelection, (id) => { + deleteMutation.mutate({ + [E_COURSE_ENTITY_KEYS.ABBR]: toString(id), + }); + }); + }; + + const handleRowSelection = (newSelection: Array) => { + setRowSelection(newSelection); + }; + + const handleRowUpdate = async (newRow: TPureCourse, oldRow: TPureCourse) => { + const diff = differenceWith([oldRow], [newRow], isEqual); + + if (diff.length === 0) return oldRow; + + setRows((prevRows) => + unionBy([newRow], prevRows, E_COURSE_ENTITY_KEYS.ABBR), + ); + + updateMutation.mutate({ + [E_COURSE_ENTITY_KEYS.ABBR]: newRow[E_COURSE_ENTITY_KEYS.ABBR], + data: omit(newRow, [ + E_COURSE_ENTITY_KEYS.ABBR, + E_COURSE_ENTITY_KEYS.GUARANTOR, + E_COURSE_ENTITY_KEYS.TEACHERS, + ]), + }); + + return newRow; + }; + + const handleDuplicateAction = (duplicateData: TPureCourse) => { + openCourseFormModal({ + mode: E_MODAL_MODE.CREATE, + initialData: duplicateData, + onSuccess: handleCreateSuccess, + }); + }; + + const handleEditAction = (editData: TPureCourse) => { + openCourseFormModal({ + abbr: editData[E_COURSE_ENTITY_KEYS.ABBR], + mode: E_MODAL_MODE.UPDATE, + initialData: editData, + onSuccess: handleUpdateSuccess, + }); + }; + + const handleDeleteAction = (abbr: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR]) => { + deleteMutation.mutate({ + abbr, + }); + }; + + const gridColumns: Array> = [ + { + field: E_COURSE_ENTITY_KEYS.ABBR, + headerName: 'Abbreviation', + hideable: false, + }, + { + field: E_COURSE_ENTITY_KEYS.NAME, + headerName: 'Name', + editable: true, + flex: 1, + hideable: false, + }, + { + field: E_COURSE_ENTITY_KEYS.ANNOTATION, + headerName: 'Annotation', + editable: true, + flex: 1, + renderCell: ({ + value, + }: GridRenderCellParams< + TPureCourse, + TPureCourse[E_COURSE_ENTITY_KEYS.ANNOTATION] + >) => (isEmpty(value) ? None : value), + }, + { + field: E_COURSE_ENTITY_KEYS.CREDITS, + type: 'number', + headerName: 'Credits', + editable: true, + }, + { + field: E_COURSE_ENTITY_KEYS.GUARANTOR, + headerName: 'Guarantor', + flex: 1, + valueFormatter: ({ + value, + }: GridValueFormatterParams< + TPureCourse[E_COURSE_ENTITY_KEYS.GUARANTOR] + >) => + `${value[E_USER_ENTITY_KEYS.FIRST_NAME]} ${ + value[E_USER_ENTITY_KEYS.LAST_NAME] + }`, + }, + { + ...chipSelectColDef(E_USER_ENTITY_KEYS.USERNAME, []), + field: E_COURSE_ENTITY_KEYS.TEACHERS, + headerName: 'Teachers', + width: 200, + }, + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + hideable: false, + flex: 1, + align: 'right', + getActions: (params) => [ + } + onClick={() => + navigate(`/courses/${params.row[E_COURSE_ENTITY_KEYS.ABBR]}`) + } + />, + handleDuplicateAction(params.row)} + />, + handleEditAction(params.row)} + />, + + handleDeleteAction(params.row[E_COURSE_ENTITY_KEYS.ABBR]) + } + />, + ], + }, + ]; + + if (isLoading || error) { + return ( + + ); + } + + return ( + <> + row[E_COURSE_ENTITY_KEYS.ABBR]} + rowSelectionModel={rowSelection} + onRowSelectionModelChange={handleRowSelection} + sortModel={[ + { + field: E_COURSE_ENTITY_KEYS.ABBR, + sort: 'asc', + }, + ]} + processRowUpdate={handleRowUpdate} + slots={{ + loadingOverlay: LinearProgress, + toolbar: () => ( + + ), + }} + /> + + ); +}; diff --git a/src/components/courses/data-table/toolbar.tsx b/src/components/courses/data-table/toolbar.tsx new file mode 100644 index 0000000..519249a --- /dev/null +++ b/src/components/courses/data-table/toolbar.tsx @@ -0,0 +1,51 @@ +import { JSX } from 'react'; +import { DataGridToolbar } from '../../data-grid/toolbar'; +import { Button } from '@mui/material'; +import { E_MODALS, TDynModalMeta } from '../../../store/modals'; +import { Add, Delete } from '@mui/icons-material'; +import { GridRowId } from '@mui/x-data-grid'; +import { E_MODAL_MODE } from '../../../utils/modal/base-modal'; +import { TCreateCourseData } from '../../../api/courses/course.service'; + +export type TCourseDataTableToolbarProps = { + rowSelection: Array; + handleCreateSuccess(data: TCreateCourseData): void; + openCreateModal(meta: TDynModalMeta): void; + handleDeleteSelected(): void; +}; + +export const CourseDataTableToolbar = ({ + rowSelection, + openCreateModal, + handleCreateSuccess, + handleDeleteSelected, +}: TCourseDataTableToolbarProps): JSX.Element => ( + } + onClick={() => + openCreateModal({ + mode: E_MODAL_MODE.CREATE, + onSuccess: handleCreateSuccess, + }) + } + > + Add new course + , + ]} + endButtons={[ + , + ]} + /> +); From 2984e6e01ce752ca282646de6f3cc7f11beb36a8 Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 17:50:07 +0200 Subject: [PATCH 7/8] feat(courses): add course page route --- src/utils/router/routes.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/router/routes.tsx b/src/utils/router/routes.tsx index 61a8fb7..1899bd8 100644 --- a/src/utils/router/routes.tsx +++ b/src/utils/router/routes.tsx @@ -4,6 +4,7 @@ import LogInPage from '../../pages/login'; import SignUpPage from '../../pages/register'; import { UsersDataTable } from '../../components/users/data-table'; import { E_ROLE } from '../../api/user/types'; +import { CoursesDataTable } from '../../components/courses/data-table'; export type TAppRoute = RouteObject & { path: string; @@ -33,4 +34,10 @@ export const appRoutes: Array = [ roles: [E_ROLE.ADMIN], element: , }, + { + path: '/courses', + label: 'Courses', + roles: [E_ROLE.ADMIN, E_ROLE.GUARANTOR, E_ROLE.TEACHER], + element: , + }, ]; From 0e04c8c1a94228c393da63030fd0c154ccbe2fac Mon Sep 17 00:00:00 2001 From: NickSettler Date: Sat, 21 Oct 2023 17:50:16 +0200 Subject: [PATCH 8/8] fix: update main layout --- src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4acc879..36f1147 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -111,9 +111,9 @@ const App = (): ReactElement => { - + - + {appRoutes.map(({ path, roles, element }) => (