From d847ef03a32b26494d37ad6af998f308b3cf5591 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 14 Jun 2023 22:37:00 +0700 Subject: [PATCH 01/18] crud admin --- src/components/Status/Status.component.tsx | 50 ++- src/config/admin_create_template.tsx | 59 ++++ src/constants/routes.ts | 17 + src/hooks/useLockers.tsx | 27 ++ src/hooks/usePagination.tsx | 6 +- src/hooks/useRooms.tsx | 27 ++ src/index.css | 4 +- src/pages/Guards/RoleMapper.tsx | 108 +++++- .../AdminDashboardPage/AdminDashboardPage.tsx | 181 ++++++++++ .../AdminDocumentDetailPage.tsx | 278 +++++++++++++++ .../AdminDocumentPage/AdminDocumentPage.tsx | 70 ++++ .../AdminEmployeeCreatePage.tsx | 192 +++++++++++ .../AdminEmployeeDetailPage.tsx | 259 ++++++++++++++ .../AdminEmployeePage/AdminEmployeePage.tsx | 76 +++++ .../AdminFolderCreatePage.tsx | 153 +++++++++ .../AdminFolderDetailPage.tsx | 306 +++++++++++++++++ .../admin/AdminFolderPage/AdminFolderPage.tsx | 69 ++++ .../AdminLockerCreatePage.tsx | 153 +++++++++ .../AdminLockerDetailPage.tsx | 320 ++++++++++++++++++ .../admin/AdminLockerPage/AdminLockerPage.tsx | 68 ++++ .../AdminRequestDetailPage.tsx | 166 +++++++++ .../AdminRequestPage/AdminRequestPage.tsx | 61 ++++ .../AdminRoomCreatePage.tsx | 153 +++++++++ .../AdminRoomDetailPage.tsx | 300 ++++++++++++++++ .../admin/AdminRoomPage/AdminRoomPage.tsx | 68 ++++ .../AdminStaffCreatePage.tsx | 5 + .../admin/AdminStaffPage/AdminStaffPage.tsx | 75 ++++ src/pages/admin/index.ts | 17 + src/pages/emp/index.ts | 4 + .../StaffDocumentDetailPage.tsx | 7 +- .../StaffLockerDetailPage.tsx | 9 +- .../StaffRequestDetailPage.tsx | 6 +- .../staff/StaffReturnPage/StaffReturnPage.tsx | 4 +- src/pages/staff/index.ts | 2 +- src/types/response.ts | 18 +- src/types/roles.ts | 58 +++- 36 files changed, 3331 insertions(+), 45 deletions(-) create mode 100644 src/config/admin_create_template.tsx create mode 100644 src/hooks/useLockers.tsx create mode 100644 src/hooks/useRooms.tsx create mode 100644 src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx create mode 100644 src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx create mode 100644 src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx create mode 100644 src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx create mode 100644 src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx create mode 100644 src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx create mode 100644 src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx create mode 100644 src/pages/admin/AdminFolderDetailPage/AdminFolderDetailPage.tsx create mode 100644 src/pages/admin/AdminFolderPage/AdminFolderPage.tsx create mode 100644 src/pages/admin/AdminLockerCreatePage/AdminLockerCreatePage.tsx create mode 100644 src/pages/admin/AdminLockerDetailPage/AdminLockerDetailPage.tsx create mode 100644 src/pages/admin/AdminLockerPage/AdminLockerPage.tsx create mode 100644 src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx create mode 100644 src/pages/admin/AdminRequestPage/AdminRequestPage.tsx create mode 100644 src/pages/admin/AdminRoomCreatePage/AdminRoomCreatePage.tsx create mode 100644 src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx create mode 100644 src/pages/admin/AdminRoomPage/AdminRoomPage.tsx create mode 100644 src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx create mode 100644 src/pages/admin/AdminStaffPage/AdminStaffPage.tsx create mode 100644 src/pages/admin/index.ts diff --git a/src/components/Status/Status.component.tsx b/src/components/Status/Status.component.tsx index c75aa97..9aced59 100644 --- a/src/components/Status/Status.component.tsx +++ b/src/components/Status/Status.component.tsx @@ -1,5 +1,5 @@ import { DOCUMENT_STATUS, REQUEST_STATUS } from '@/constants/status'; -import { IBorrowRequest, IDocument, IFolder, ILocker } from '@/types/item'; +import { IBorrowRequest, IDocument, IFolder, ILocker, IRoom, IUser } from '@/types/item'; import clsx from 'clsx'; import { FC, HTMLAttributes } from 'react'; @@ -23,11 +23,29 @@ interface IStatusFolderProps { type: 'folder'; } +interface IStatusRoomProps { + item: IRoom; + type: 'room'; +} + +interface IStatusUserActiveProps { + item: IUser; + type: 'user_active'; +} + +interface IStatusUserActivatedProps { + item: IUser; + type: 'user_activated'; +} + type IStatusProps = ( | IStatusBorrowProps | IStatusDocumentProps | IStatusLockerProps | IStatusFolderProps + | IStatusRoomProps + | IStatusUserActiveProps + | IStatusUserActivatedProps ) & HTMLAttributes; @@ -60,6 +78,36 @@ const Status: FC = ({ item, type, className, ...rest }) => { ); + if (type === 'user_activated') { + return ( + + {item.isActivated ? 'Activated' : 'Not activated'} + + ); + } + + if (type === 'user_active') { + return ( + + {item.isActive ? 'Active' : 'Not active'} + + ); + } + return ( { + const { data } = await axiosClient.get('/departments'); + return data.data.map((department) => ({ + label: department.name, + value: department.id, + })); + }, + }, + ], + }, + ], + }, + rooms: { + left: [], + right: [], + }, +}; + +export type TemplateKey = keyof typeof ADMIN_CREATE_TEMPLATE; + +export default ADMIN_CREATE_TEMPLATE; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 0d0f2ff..faea0ab 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -3,8 +3,10 @@ export const AUTH_ROUTES = { PHYSICAL: '/physical', LOCKERS: '/physical/lockers', LOCKER: '/physical/lockers/:lockerId', + NEW_LOCKER: '/physical/lockers/create', FOLDERS: '/physical/folders', FOLDER: '/physical/folders/:folderId', + NEW_FOLDER: '/physical/folders/create', DOCUMENTS: '/physical/documents', DOCUMENT: '/physical/documents/:documentId', REQUESTS: '/requests', @@ -13,6 +15,21 @@ export const AUTH_ROUTES = { RETURNS: '/returns', IMPORT: '/import', DRIVE: '/digital', + EMPLOYEES: '/employees', + EMPLOYEES_MANAGE: '/employees/manage', + EMPLOYEE: '/employees/manage/:empId', + NEW_EMP: '/employees/create', + LOGS: '/logs', + ROOMS: '/physical/rooms', + ROOM: '/physical/rooms/:roomId', + NEW_ROOM: '/physical/rooms/create', + STAFFS: '/staffs', + STAFFS_MANAGE: '/staffs/manage', + STAFF: '/staffs/manage/:staffId', + NEW_STAFF: '/staffs/create', + DEPARTMENTS: '/departments', + DEPARTMENT: '/departments/:departmentId', + NEW_DEPARTMENT: '/departments/create', }; export const UNAUTH_ROUTES = { diff --git a/src/hooks/useLockers.tsx b/src/hooks/useLockers.tsx new file mode 100644 index 0000000..1dec9b2 --- /dev/null +++ b/src/hooks/useLockers.tsx @@ -0,0 +1,27 @@ +import { DropdownOption } from '@/types/config'; +import { GetLockersResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { useQuery } from 'react-query'; + +const useLockers = () => { + const { data: lockersResult, refetch: lockersRefetch } = useQuery( + ['lockers'], + async () => (await axiosClient.get('/lockers')).data, + { + enabled: false, + } + ); + + const lockers: DropdownOption[] = + lockersResult?.data.items.map((locker) => ({ + name: locker.name, + id: locker.id, + })) || []; + + return { + lockers, + lockersRefetch, + }; +}; + +export default useLockers; diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index aa17487..a93daea 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -1,6 +1,6 @@ import { REFETCH_CONFIG } from '@/constants/config'; import { AuthContext } from '@/context/authContext'; -import { BaseResponse, GetPaginationResponse } from '@/types/response'; +import { BaseResponse, PaginationResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { DataTableProps, DataTableStateEvent, DataTableValueArray } from 'primereact/datatable'; import { useState, useContext } from 'react'; @@ -46,10 +46,10 @@ const usePagination = ({ [key, paginate], async () => ( - await axiosClient.get>(url, { + await axiosClient.get>(url, { params: { searchTerm: query, - roomId: user?.department.roomId, + roomId: user?.role === 'admin' ? undefined : user?.department.roomId, page: paginate.page + 1, // Primereact datatable page start at 0, our api start at 1 size: paginate.rows, sortBy: paginate?.sortField?.slice(0, 1).toUpperCase() + paginate?.sortField?.slice(1), diff --git a/src/hooks/useRooms.tsx b/src/hooks/useRooms.tsx new file mode 100644 index 0000000..9ac3973 --- /dev/null +++ b/src/hooks/useRooms.tsx @@ -0,0 +1,27 @@ +import { DropdownOption } from '@/types/config'; +import { GetRoomsResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { useQuery } from 'react-query'; + +const useRooms = () => { + const { data: roomsResult, refetch: roomsRefetch } = useQuery( + ['rooms'], + async () => (await axiosClient.get('/rooms')).data, + { + enabled: false, + } + ); + + const rooms: DropdownOption[] = + roomsResult?.data.items.map((room) => ({ + name: room.name, + id: room.id, + })) || []; + + return { + rooms, + roomsRefetch, + }; +}; + +export default useRooms; diff --git a/src/index.css b/src/index.css index 028d387..4db2856 100644 --- a/src/index.css +++ b/src/index.css @@ -81,10 +81,10 @@ body { /* Handle */ ::-webkit-scrollbar-thumb { - background: #888; + background: var(--primary); } /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { - background: #555; + background: var(--secondary); } \ No newline at end of file diff --git a/src/pages/Guards/RoleMapper.tsx b/src/pages/Guards/RoleMapper.tsx index c301793..a2afb92 100644 --- a/src/pages/Guards/RoleMapper.tsx +++ b/src/pages/Guards/RoleMapper.tsx @@ -3,10 +3,11 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { lazy } from 'react'; import { Navigate } from 'react-router'; -import { StaffDashboardPage } from '../staff'; -import { EmpDashboardPage } from '../emp'; // Staff imports +const StaffDashboardPage = lazy( + () => import('@/pages/staff/StaffDashboardPage/StaffDashboardPage') +); const StaffLockerPage = lazy(() => import('@/pages/staff/StaffLockerPage/StaffLockerPage')); const StaffDocumentPage = lazy(() => import('@/pages/staff/StaffDocumentPage/StaffDocumentPage')); const StaffImportPage = lazy(() => import('@/pages/staff/StaffImportPage/StaffImportPage')); @@ -27,6 +28,7 @@ const StaffFolderDetailPage = lazy( ); // Employee imports +const EmpDashboardPage = lazy(() => import('@/pages/emp/EmpDashboardPage/EmpDashboardPage')); const EmpDocumentPage = lazy(() => import('@/pages/emp/EmpDocumentPage/EmpDocumentPage')); const EmpRequestPage = lazy(() => import('@/pages/emp/EmpRequestPage/EmpRequestPage')); const EmpRequestDetailPage = lazy( @@ -39,27 +41,69 @@ const EmpDocumentDetailPage = lazy( () => import('@/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage') ); +// Admin imports +const AdminDashboardPage = lazy( + () => import('@/pages/admin/AdminDashboardPage/AdminDashboardPage') +); +const AdminLockerPage = lazy(() => import('@/pages/admin/AdminLockerPage/AdminLockerPage')); +const AdminLockerDetailPage = lazy( + () => import('@/pages/admin/AdminLockerDetailPage/AdminLockerDetailPage') +); +const AdminFolderPage = lazy(() => import('@/pages/admin/AdminFolderPage/AdminFolderPage')); +const AdminFolderDetailPage = lazy( + () => import('@/pages/admin/AdminFolderDetailPage/AdminFolderDetailPage') +); +const AdminDocumentPage = lazy(() => import('@/pages/admin/AdminDocumentPage/AdminDocumentPage')); +const AdminDocumentDetailPage = lazy( + () => import('@/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage') +); +const AdminRequestPage = lazy(() => import('@/pages/admin/AdminRequestPage/AdminRequestPage')); +const AdminRequestDetailPage = lazy( + () => import('@/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage') +); +const AdminRoomPage = lazy(() => import('@/pages/admin/AdminRoomPage/AdminRoomPage')); +const AdminRoomDetailPage = lazy( + () => import('@/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage') +); +const AdminEmployeePage = lazy(() => import('@/pages/admin/AdminEmployeePage/AdminEmployeePage')); +const AdminEmployeeCreatePage = lazy( + () => import('@/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage') +); +const AdminEmployeeDetailPage = lazy( + () => import('@/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage') +); +const AdminStaffPage = lazy(() => import('@/pages/admin/AdminStaffPage/AdminStaffPage')); +const AdminLockerCreatePage = lazy( + () => import('@/pages/admin/AdminLockerCreatePage/AdminLockerCreatePage') +); +const AdminRoomCreatePage = lazy( + () => import('@/pages/admin/AdminRoomCreatePage/AdminRoomCreatePage') +); +const AdminFolderCreatePage = lazy( + () => import('@/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage') +); + export const ROLE_MAPPER = { [AUTH_ROUTES.HOME]: { - admin: () =>
Admins
, + admin: () => , staff: () => , employee: () => , }, [AUTH_ROUTES.PHYSICAL]: { - admin: () =>
Admins
, - staff: () => , - employee: () => , + admin: () => , + staff: () => , + employee: () => , }, [AUTH_ROUTES.LOCKERS]: { - admin: () =>
Admins
, + admin: () => , staff: () => , }, [AUTH_ROUTES.LOCKER]: { - admin: () =>
Admins
, + admin: () => , staff: () => , }, [AUTH_ROUTES.DOCUMENTS]: { - admin: () =>
Admins
, + admin: () => , staff: () => , employee: () => , }, @@ -67,14 +111,17 @@ export const ROLE_MAPPER = { staff: () => , }, [AUTH_ROUTES.DOCUMENT]: { + admin: () => , staff: () => , employee: () => , }, [AUTH_ROUTES.REQUESTS]: { + admin: () => , staff: () => , employee: () => , }, [AUTH_ROUTES.REQUEST]: { + admin: () => , staff: () => , employee: () => , }, @@ -85,11 +132,50 @@ export const ROLE_MAPPER = { staff: () => , }, [AUTH_ROUTES.FOLDERS]: { - admin: () =>
Admins
, + admin: () => , staff: () => , }, [AUTH_ROUTES.FOLDER]: { - admin: () =>
Admins
, + admin: () => , staff: () => , }, + [AUTH_ROUTES.ROOMS]: { + admin: () => , + }, + [AUTH_ROUTES.ROOM]: { + admin: () => , + }, + [AUTH_ROUTES.EMPLOYEES]: { + admin: () => , + }, + [AUTH_ROUTES.EMPLOYEES_MANAGE]: { + admin: () => , + }, + [AUTH_ROUTES.EMPLOYEE]: { + admin: () => , + }, + [AUTH_ROUTES.STAFFS]: { + admin: () => , + }, + [AUTH_ROUTES.STAFFS_MANAGE]: { + admin: () => , + }, + [AUTH_ROUTES.NEW_DEPARTMENT]: { + // admin: () => , + }, + [AUTH_ROUTES.NEW_ROOM]: { + admin: () => , + }, + [AUTH_ROUTES.NEW_LOCKER]: { + admin: () => , + }, + [AUTH_ROUTES.NEW_FOLDER]: { + admin: () => , + }, + [AUTH_ROUTES.NEW_STAFF]: { + // admin: () => , + }, + [AUTH_ROUTES.NEW_EMP]: { + admin: () => , + }, }; diff --git a/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx b/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx new file mode 100644 index 0000000..e64a383 --- /dev/null +++ b/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx @@ -0,0 +1,181 @@ +import InfoCard from '@/components/Card/InfoCard.component'; +import Progress from '@/components/Progress/Progress.component'; +import { SkeletonCard } from '@/components/Skeleton'; +import Status from '@/components/Status/Status.component'; +import { REFETCH_CONFIG } from '@/constants/config'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { GetDocumentsResponse, GetFoldersResponse, GetLockersResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import clsx from 'clsx'; +import { useQuery } from 'react-query'; +import { Link } from 'react-router-dom'; + +const AdminDashboardPage = () => { + const { data: lockers, isLoading: isLockerLoading } = useQuery( + ['lockers', 'recent'], + async () => + ( + await axiosClient.get('/lockers', { + params: { + sortBy: 'NumberOfFolders', + sortOrder: 'desc', + size: 3, + page: 1, + }, + }) + ).data, + { + ...REFETCH_CONFIG, + } + ); + + const { data: folders, isLoading: isFolderLoading } = useQuery( + ['folders', 'recent'], + async () => + ( + await axiosClient.get('/folders', { + params: { + sortBy: 'NumberOfDocuments', + sortOrder: 'desc', + size: 3, + page: 1, + }, + }) + ).data, + { + ...REFETCH_CONFIG, + } + ); + + const { data: documents, isLoading: isDocumentLoading } = useQuery( + ['documents', 'recent'], + async () => + ( + await axiosClient.get('/documents', { + params: { + sortBy: '', + sortOrder: 'desc', + size: 8, + page: 1, + }, + }) + ).data, + { + ...REFETCH_CONFIG, + } + ); + + return ( +
+ + Pending request > + +
+
+
+
+
+ + Lockers > + +
+ {isLockerLoading ? ( + [...Array(3)].map((_, index) => ) + ) : lockers ? ( + lockers.data.items.map((locker) => ( + +

+ Status:{' '} + + {locker.isAvailable ? 'Available' : 'Not available'} + +

+

Folder count:

+ +
+ )) + ) : ( +
No lockers
+ )} +
+ + Folders > + +
+ {isFolderLoading ? ( + [...Array(3)].map((_, index) => ) + ) : folders ? ( + folders.data.items.map((folder) => ( + +

+ Status:{' '} + + {folder.isAvailable ? 'Available' : 'Not available'} + +

+

Document count:

+ +
+ )) + ) : ( +
No folders
+ )} +
+ + Documents > + +
+ {isDocumentLoading ? ( + [...Array(3)].map((_, index) => ) + ) : documents ? ( + documents.data.items.map((document) => ( + +
+

+ {document.title} +

+ +
+

{document.folder.name}

+

+ Type: {document.documentType} +

+
+ )) + ) : ( +
No documents
+ )} +
+
+ ); +}; + +export default AdminDashboardPage; diff --git a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx new file mode 100644 index 0000000..0e896bd --- /dev/null +++ b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx @@ -0,0 +1,278 @@ +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { BaseResponse, GetDocumentByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { Button } from 'primereact/button'; +import { useState } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; +import { useParams } from 'react-router'; +import { Link } from 'react-router-dom'; +import QRCode from 'qrcode'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; +import { Formik, FormikHelpers } from 'formik'; +import { SkeletonPage } from '@/components/Skeleton'; +import Status from '@/components/Status/Status.component'; +import { AxiosError } from 'axios'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; + +const AdminDocumentDetailPage = () => { + const { documentId = '' } = useParams<{ documentId: string }>(); + const [qr, setQr] = useState(''); + const [editMode, setEditMode] = useState(false); + const queryClient = useQueryClient(); + + const { data, isLoading, error } = useQuery( + ['documents', documentId], + async () => (await axiosClient.get(`/documents/${documentId}`)).data, + { + onSuccess: async (data) => { + const { id } = data?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }, + } + ); + + if (isLoading) return ; + + if ((error as AxiosError)?.response?.status === 404 || !data) + return ; + + const { + title, + folder: { + id: folderId, + name: folderName, + locker: { + id: lockerId, + name: lockerName, + room: { id: roomId, name: roomName }, + }, + }, + } = data.data; + + const initialValues = data.data; + + type FormValues = typeof initialValues; + + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { + if (JSON.stringify(values) === JSON.stringify(initialValues)) return setEditMode(false); + try { + await axiosClient.put(`/documents/${documentId}`, { + ...values, + documentType: values.documentType.toUpperCase(), + }); + queryClient.invalidateQueries('documents'); + setEditMode(false); + } catch (error) { + const axiosError = error as AxiosError; + setFieldError('title', axiosError?.response?.data?.message || 'Something went wrong'); + } + }; + + const validate = (values: FormValues) => { + const errors: Partial = {}; + + if (!values.title) errors.title = 'Title is required'; + if (!values.documentType) errors.documentType = 'Document type is required'; + + return errors; + }; + + return ( +
+
+

+ / + + {roomName} + + / + + {lockerName} + + / + + {folderName} + + / + {title} +

+
+ + {({ + values, + touched, + errors, + handleChange, + handleBlur, + handleSubmit, + submitForm, + resetForm, + isSubmitting, + isValid, + }) => ( +
+
+ +
+ +
+ + +
+ + } + /> + + + +
+ + +
+
+
+
+ + {qr ? ( + + ) : ( +
+ )} +
+ {editMode ? ( +
+ + + + + +
+ + )} + +
+ ); +}; + +export default AdminDocumentDetailPage; diff --git a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx new file mode 100644 index 0000000..af6b64b --- /dev/null +++ b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx @@ -0,0 +1,70 @@ +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { Link } from 'react-router-dom'; +import { IDocument } from '@/types/item'; +import usePagination from '@/hooks/usePagination'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import Status from '@/components/Status/Status.component'; +import { useRef } from 'react'; + +const AdminDocumentPage = () => { + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['documents', query.current], + url: '/documents', + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + placeholder='document a' + /> + + +
+
+ + + + + + } + /> + + +
+
+
+ ); +}; + +export default AdminDocumentPage; diff --git a/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx b/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx new file mode 100644 index 0000000..f872167 --- /dev/null +++ b/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx @@ -0,0 +1,192 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import useDepartments from '@/hooks/useDepartments'; +import { BaseResponse, GetUserByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Formik, FormikHelpers } from 'formik'; +import { Button } from 'primereact/button'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; + +const initialValues = { + username: '', + email: '', + firstName: '', + lastName: '', + department: '', + role: '', + position: '', +}; + +type FormValues = typeof initialValues; + +const roles = [ + { label: 'Staff', value: 'Staff' }, + { label: 'Employee', value: 'Employee' }, +]; + +const AdminEmployeeCreatePage = () => { + const { departments, departmentsRefetch } = useDepartments(); + const navigate = useNavigate(); + + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { + try { + const { data: user } = await axiosClient.post('/users', { + ...values, + departmentId: values.department, + }); + navigate(`${AUTH_ROUTES.EMPLOYEES_MANAGE}/${user.data.id}`); + } catch (error) { + const axiosError = error as AxiosError; + const status = axiosError.response?.status; + const message = axiosError.response?.data.message; + if (status === 409) { + setFieldError('username', 'Username already exists'); + setFieldError('email', 'Email already exists'); + } + if (status === 404) { + setFieldError('department', 'Department not found'); + } + console.error(message); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (key === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value as string)) { + error[key as keyof FormValues] = 'Invalid email'; + } + if (!value) { + error[key as keyof FormValues] = 'Required'; + } + }); + return error; + }; + + useEffect(() => { + departmentsRefetch(); + }, [departmentsRefetch]); + + return ( + + {({ + values, + touched, + errors, + handleSubmit, + handleChange, + handleBlur, + isValid, + isSubmitting, + }) => ( +
+ + + + + + + + + + +
+ + + + )} + + ); +}; + +export default AdminEmployeeDetailPage; diff --git a/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx b/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx new file mode 100644 index 0000000..d20793c --- /dev/null +++ b/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx @@ -0,0 +1,76 @@ +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import usePagination from '@/hooks/usePagination'; +import { IUser } from '@/types/item'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { useRef } from 'react'; +import { Link } from 'react-router-dom'; + +const AdminEmployeePage = () => { + const query = useRef(''); + const { getPaginatedTableProps, refetch } = usePagination({ + key: 'employees', + url: '/users', + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'EMPLOYEES_MANAGE' }); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + placeholder='locker a' + /> + + +
+
+ + {/* {locker.id}} + /> */} + + + + + + + + + } + /> + } + /> +
+
+
+ ); +}; + +export default AdminEmployeePage; diff --git a/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx b/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx new file mode 100644 index 0000000..dbb88fc --- /dev/null +++ b/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx @@ -0,0 +1,153 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputNumberWithLabel from '@/components/InputWithLabel/InputNumberWithLabel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { BaseResponse, GetFolderByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Formik, FormikHelpers } from 'formik'; +import { Button } from 'primereact/button'; +import { useNavigate } from 'react-router'; +import { useEffect } from 'react'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import useLockers from '@/hooks/useLockers'; + +const initialValues = { + name: '', + description: '', + locker: '', + capacity: 0, +}; + +const NOT_REQUIRED = ['description']; + +type FormValues = typeof initialValues; + +const AdminFolderCreatePage = () => { + const navigate = useNavigate(); + const { lockers, lockersRefetch } = useLockers(); + + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { + try { + const { data: folder } = await axiosClient.post('/folders', { + ...values, + lockerId: values.locker, + }); + navigate(`${AUTH_ROUTES.FOLDERS}/${folder.data.id}`); + } catch (error) { + const axiosError = error as AxiosError; + const status = axiosError.response?.status; + const message = axiosError.response?.data.message; + if (status === 404) { + setFieldError('locker', 'Locker not found'); + } + if (status === 409) { + if (message?.includes('limit')) { + setFieldError('locker', 'Locker is full'); + } else { + setFieldError('name', 'Folder name already exists'); + } + } + console.error(message); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (!value && NOT_REQUIRED.indexOf(key) === -1) { + error[key as keyof FormValues] = 'Required'; + } + if (key === 'capacity' && (value as number) <= 0) { + error[key as keyof FormValues] = 'Capacity must be greater than 0'; + } + }); + return error; + }; + + useEffect(() => { + lockersRefetch(); + }, [lockersRefetch]); + + return ( + + {({ + values, + errors, + touched, + handleBlur, + handleChange, + handleSubmit, + isValid, + setFieldValue, + isSubmitting, + }) => ( +
+ + + setFieldValue('capacity', e.value)} + onBlur={handleBlur} + error={touched.capacity && !!errors.capacity} + small={touched.capacity ? errors.capacity : undefined} + disabled={isSubmitting} + /> + + + + + +
+ + {/* {folder.id}} + /> */} + + + + + + } + /> +
+
+ + ); +}; + +export default AdminFolderPage; diff --git a/src/pages/admin/AdminLockerCreatePage/AdminLockerCreatePage.tsx b/src/pages/admin/AdminLockerCreatePage/AdminLockerCreatePage.tsx new file mode 100644 index 0000000..43e242e --- /dev/null +++ b/src/pages/admin/AdminLockerCreatePage/AdminLockerCreatePage.tsx @@ -0,0 +1,153 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputNumberWithLabel from '@/components/InputWithLabel/InputNumberWithLabel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import useRooms from '@/hooks/useRooms'; +import { BaseResponse, GetLockerByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Formik, FormikHelpers } from 'formik'; +import { Button } from 'primereact/button'; +import { useNavigate } from 'react-router'; +import { useEffect } from 'react'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; + +const initialValues = { + name: '', + description: '', + room: '', + capacity: 0, +}; + +const NOT_REQUIRED = ['description']; + +type FormValues = typeof initialValues; + +const AdminLockerCreatePage = () => { + const { rooms, roomsRefetch } = useRooms(); + const navigate = useNavigate(); + + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { + try { + const { data: locker } = await axiosClient.post('/lockers', { + ...values, + roomId: values.room, + }); + navigate(`${AUTH_ROUTES.LOCKERS}/${locker.data.id}`); + } catch (error) { + const axiosError = error as AxiosError; + const status = axiosError.response?.status; + const message = axiosError.response?.data.message; + if (status === 404) { + setFieldError('room', 'Room not found'); + } + if (status === 409) { + if (message?.includes('limit')) { + setFieldError('room', 'Room is full'); + } else { + setFieldError('name', 'Locker name already exists'); + } + } + console.error(message); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (!value && NOT_REQUIRED.indexOf(key) === -1) { + error[key as keyof FormValues] = 'Required'; + } + if (key === 'capacity' && (value as number) <= 0) { + error[key as keyof FormValues] = 'Capacity must be greater than 0'; + } + }); + return error; + }; + + useEffect(() => { + roomsRefetch(); + }, [roomsRefetch]); + + return ( + + {({ + values, + errors, + touched, + handleBlur, + handleChange, + handleSubmit, + isValid, + setFieldValue, + isSubmitting, + }) => ( +
+ + + setFieldValue('capacity', e.value)} + onBlur={handleBlur} + error={touched.capacity && !!errors.capacity} + small={touched.capacity ? errors.capacity : undefined} + disabled={isSubmitting} + /> + + + + + +
+ + {/* {locker.id}} + /> */} + + + + + } + /> +
+
+ + ); +}; + +export default AdminLockerPage; diff --git a/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx b/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx new file mode 100644 index 0000000..768bfe3 --- /dev/null +++ b/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx @@ -0,0 +1,166 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { SkeletonPage } from '@/components/Skeleton'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { REQUEST_STATUS } from '@/constants/status'; +import { + BaseResponse, + GetDocumentByIdResponse, + GetRequestByIdResponse, + GetUserByIdResponse, +} from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Button } from 'primereact/button'; +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { Navigate, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; + +const NO_ACTIONS = [ + REQUEST_STATUS.Cancelled.status, + REQUEST_STATUS.CheckedOut.status, + REQUEST_STATUS.NotProcessable.status, + REQUEST_STATUS.Returned.status, + REQUEST_STATUS.Lost.status, +]; + +const AdminRequestDetailPage = () => { + const { requestId } = useParams<{ requestId: string }>(); + const [error, setError] = useState(''); + const { data, refetch } = useQuery( + ['requests', requestId], + async () => (await axiosClient.get(`/borrows/${requestId}`)).data, + { + enabled: !!requestId, + } + ); + + const { documentId, borrowerId } = data ? data.data : { documentId: '', borrowerId: '' }; + + const { data: document, isLoading } = useQuery( + [requestId, documentId], + async () => (await axiosClient.get(`/documents/${documentId}`)).data, + { + enabled: !!documentId, + } + ); + + const { data: employee, isLoading: isEmployeeLoading } = useQuery( + ['employee', borrowerId, requestId], + async () => (await axiosClient.get(`/users/${borrowerId}`)).data, + { + enabled: !!requestId && !!borrowerId, + } + ); + + if (!requestId) return ; + + if (!data || !document || !employee || isLoading || isEmployeeLoading) return ; + + const { + title, + documentType, + folder: { + name: folder, + locker: { name: locker }, + }, + } = document.data; + + const { id: employeeId, lastName, firstName } = employee.data; + + const { borrowTime, dueTime, reason, status } = data.data; + + const onApprove = async () => { + try { + await axiosClient.post(`/borrows/approve/${requestId}`); + await refetch(); + } catch (error) { + const axiosError = error as AxiosError; + const message = axiosError.response?.data.message || 'Something went wrong'; + console.log(error); + setError(message); + } + }; + + const onDeny = async () => { + try { + await axiosClient.post(`/borrows/reject/${requestId}`); + await refetch(); + } catch (error) { + const axiosError = error as AxiosError; + const message = axiosError.response?.data.message || 'Something went wrong'; + console.log(error); + setError(message); + } + }; + + const onCheckout = async () => { + try { + await axiosClient.post(`/borrows/checkout/${requestId}`); + await refetch(); + } catch (error) { + const axiosError = error as AxiosError; + const message = axiosError.response?.data.message || 'Something went wrong'; + console.log(error); + setError(message); + } + }; + + return ( +
+
+ + + + +
+ + +
+
+ + + + + +
+
+ + + + + + + +
+ {NO_ACTIONS.indexOf(status) !== -1 ? null : status === + REQUEST_STATUS.Approved.status ? ( +
+ {error &&
{error}
} +
+
+
+ ); +}; + +export default AdminRequestDetailPage; diff --git a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx new file mode 100644 index 0000000..870e807 --- /dev/null +++ b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx @@ -0,0 +1,61 @@ +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import usePagination from '@/hooks/usePagination'; +import { IBorrowRequest } from '@/types/item'; +import { dateFormatter } from '@/utils/formatter'; +import { Column } from 'primereact/column'; + +const AdminRequestPage = () => { + const { getPaginatedTableProps } = usePagination({ + key: 'requests', + url: '/borrows', + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); + + return ( +
+

Pending requests

+
+ + + + } + sortable + /> + + dateFormatter(new Date(request.borrowTime), undefined, { + dateStyle: 'full', + }) + } + header='From' + sortable + /> + + dateFormatter(new Date(request.dueTime), undefined, { + dateStyle: 'full', + }) + } + header='To' + sortable + /> +
+
+
+ ); +}; + +export default AdminRequestPage; diff --git a/src/pages/admin/AdminRoomCreatePage/AdminRoomCreatePage.tsx b/src/pages/admin/AdminRoomCreatePage/AdminRoomCreatePage.tsx new file mode 100644 index 0000000..9719cde --- /dev/null +++ b/src/pages/admin/AdminRoomCreatePage/AdminRoomCreatePage.tsx @@ -0,0 +1,153 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputNumberWithLabel from '@/components/InputWithLabel/InputNumberWithLabel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { BaseResponse, GetRoomByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Formik, FormikHelpers } from 'formik'; +import { Button } from 'primereact/button'; +import { useNavigate } from 'react-router'; +import { useEffect } from 'react'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import useDepartments from '@/hooks/useDepartments'; + +const initialValues = { + name: '', + description: '', + department: '', + capacity: 0, +}; + +const NOT_REQUIRED = ['description']; + +type FormValues = typeof initialValues; + +const AdminRoomCreatePage = () => { + const { departments, departmentsRefetch } = useDepartments(); + const navigate = useNavigate(); + + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { + try { + const { data: room } = await axiosClient.post('/rooms', { + ...values, + departmentId: values.department, + }); + navigate(`${AUTH_ROUTES.ROOMS}/${room.data.id}`); + } catch (error) { + const axiosError = error as AxiosError; + const status = axiosError.response?.status; + const message = axiosError.response?.data.message; + if (status === 404) { + setFieldError('department', 'Department not found'); + } + if (status === 409) { + if (message?.includes('limit')) { + setFieldError('department', 'Department is full'); + } else { + setFieldError('name', 'Locker name already exists'); + } + } + console.error(message); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (!value && NOT_REQUIRED.indexOf(key) === -1) { + error[key as keyof FormValues] = 'Required'; + } + if (key === 'capacity' && (value as number) <= 0) { + error[key as keyof FormValues] = 'Capacity must be greater than 0'; + } + }); + return error; + }; + + useEffect(() => { + departmentsRefetch(); + }, [departmentsRefetch]); + + return ( + + {({ + values, + errors, + touched, + handleBlur, + handleChange, + handleSubmit, + isValid, + setFieldValue, + isSubmitting, + }) => ( +
+ + + setFieldValue('capacity', e.value)} + onBlur={handleBlur} + error={touched.capacity && !!errors.capacity} + small={touched.capacity ? errors.capacity : undefined} + disabled={isSubmitting} + /> + + + + + +
+ + {/* {locker.id}} + /> */} + + + + + } + /> +
+
+ + ); +}; + +export default AdminRoomPage; diff --git a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx new file mode 100644 index 0000000..27220d0 --- /dev/null +++ b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx @@ -0,0 +1,5 @@ +const AdminStaffCreatePage = () => { + return
; +}; + +export default AdminStaffCreatePage; diff --git a/src/pages/admin/AdminStaffPage/AdminStaffPage.tsx b/src/pages/admin/AdminStaffPage/AdminStaffPage.tsx new file mode 100644 index 0000000..1ef1434 --- /dev/null +++ b/src/pages/admin/AdminStaffPage/AdminStaffPage.tsx @@ -0,0 +1,75 @@ +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import usePagination from '@/hooks/usePagination'; +import { IUser } from '@/types/item'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { useRef } from 'react'; +import { Link } from 'react-router-dom'; + +const AdminEmployeePage = () => { + const query = useRef(''); + const { getPaginatedTableProps, refetch } = usePagination({ + key: 'staffs', + url: '/staffs', + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'STAFFS_MANAGE' }); + + console.log(getPaginatedTableProps()); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + placeholder='locker a' + /> + + +
+
+ + {/* {locker.id}} + /> */} + + + + + } + /> + } + /> +
+
+
+ ); +}; + +export default AdminEmployeePage; diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts new file mode 100644 index 0000000..0a1af1d --- /dev/null +++ b/src/pages/admin/index.ts @@ -0,0 +1,17 @@ +export { default as AdminDashboardPage } from './AdminDashboardPage/AdminDashboardPage'; +export { default as AdminDocumentDetailPage } from './AdminDocumentDetailPage/AdminDocumentDetailPage'; +export { default as AdminDocumentPage } from './AdminDocumentPage/AdminDocumentPage'; +export { default as AdminFolderDetailPage } from './AdminFolderDetailPage/AdminFolderDetailPage'; +export { default as AdminFolderPage } from './AdminFolderPage/AdminFolderPage'; +export { default as AdminLockerDetailPage } from './AdminLockerDetailPage/AdminLockerDetailPage'; +export { default as AdminLockerPage } from './AdminLockerPage/AdminLockerPage'; +export { default as AdminRequestDetailPage } from './AdminRequestDetailPage/AdminRequestDetailPage'; +export { default as AdminRequestPage } from './AdminRequestPage/AdminRequestPage'; +export { default as AdminEmployeePage } from './AdminEmployeePage/AdminEmployeePage'; +export { default as AdminEmployeeCreatePage } from './AdminEmployeeCreatePage/AdminEmployeeCreatePage'; +export { default as AdminEmployeeDetailPage } from './AdminEmployeeDetailPage/AdminEmployeeDetailPage'; +export { default as AdminStaffPage } from './AdminStaffPage/AdminStaffPage'; +export { default as AdminLockerCreatePage } from './AdminLockerCreatePage/AdminLockerCreatePage'; +export { default as AdminRoomCreatePage } from './AdminRoomCreatePage/AdminRoomCreatePage'; +export { default as AdminFolderCreatePage } from './AdminFolderCreatePage/AdminFolderCreatePage'; +export { default as AdminRoomPage } from './AdminRoomPage/AdminRoomPage'; diff --git a/src/pages/emp/index.ts b/src/pages/emp/index.ts index 2180e62..cab9749 100644 --- a/src/pages/emp/index.ts +++ b/src/pages/emp/index.ts @@ -1,2 +1,6 @@ export { default as EmpDashboardPage } from './EmpDashboardPage/EmpDashboardPage'; export { default as EmpDocumentPage } from './EmpDocumentPage/EmpDocumentPage'; +export { default as EmpDocumentDetailPage } from './EmpDocumentDetailPage/EmpDocumentDetailPage'; +export { default as EmpRequestPage } from './EmpRequestPage/EmpRequestPage'; +export { default as EmpRequestDetailPage } from './EmpRequestDetailPage/EmpRequestDetailPage'; +export { default as EmpRequestCreatePage } from './EmpRequestCreatePage/EmpRequestCreatePage'; diff --git a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx index 119563e..6679001 100644 --- a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx +++ b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx @@ -1,7 +1,7 @@ import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; import { AUTH_ROUTES } from '@/constants/routes'; -import { GetDocumentByIdResponse } from '@/types/response'; +import { BaseResponse, GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; import { useState } from 'react'; @@ -54,7 +54,7 @@ const StaffDocumentDetailPage = () => { type FormValues = typeof initialValues; - const onSubmit = async (values: FormValues, { setValues }: FormikHelpers) => { + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { if (JSON.stringify(values) === JSON.stringify(initialValues)) return setEditMode(false); try { await axiosClient.put(`/documents/${documentId}`, { @@ -64,7 +64,8 @@ const StaffDocumentDetailPage = () => { queryClient.invalidateQueries('documents'); setEditMode(false); } catch (error) { - setValues(initialValues); + const axiosError = error as AxiosError; + setFieldError('title', axiosError?.response?.data?.message || 'Something went wrong'); } }; diff --git a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx index f9688d1..70af808 100644 --- a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx +++ b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx @@ -38,14 +38,7 @@ const StaffLockerDetailPage = () => { error: axiosError, } = useQuery( ['lockers', lockerId], - async () => - ( - await axiosClient.get(`/lockers/${lockerId}`, { - params: { - roomId, - }, - }) - ).data + async () => (await axiosClient.get(`/lockers/${lockerId}`)).data ); const { data: folders, isLoading: isFoldersLoading } = useQuery( diff --git a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx index 9a08e45..dc774be 100644 --- a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx +++ b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx @@ -111,7 +111,7 @@ const StaffRequestDetailPage = () => { return (
- + @@ -120,14 +120,14 @@ const StaffRequestDetailPage = () => {
- +
- + diff --git a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx index dc81e0b..5f33a3b 100644 --- a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx +++ b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx @@ -104,7 +104,7 @@ const StaffReturnsPage = () => { return (
- + @@ -113,7 +113,7 @@ const StaffReturnsPage = () => {
- + diff --git a/src/pages/staff/index.ts b/src/pages/staff/index.ts index 075c0bd..bd5c6fa 100644 --- a/src/pages/staff/index.ts +++ b/src/pages/staff/index.ts @@ -3,7 +3,7 @@ export { default as StaffLockerPage } from './StaffLockerPage/StaffLockerPage'; export { default as StaffDocumentPage } from './StaffDocumentPage/StaffDocumentPage'; export { default as StaffImportPage } from './StaffImportPage/StaffImportPage'; export { default as StaffDocumentDetailPage } from './StaffDocumentDetailPage/StaffDocumentDetailPage'; -export { default as StaffReturnsPage } from './StaffReturnPage/StaffReturnPage'; +export { default as StaffReturnPage } from './StaffReturnPage/StaffReturnPage'; export { default as StaffRequestDetailPage } from './StaffRequestDetailPage/StaffRequestDetailPage'; export { default as StaffRequestPage } from './StaffRequestPage/StaffRequestPage'; export { default as StaffLockerDetailPage } from './StaffLockerDetailPage/StaffLockerDetailPage'; diff --git a/src/types/response.ts b/src/types/response.ts index c81c9c8..41c834c 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -1,4 +1,4 @@ -import { IBorrowRequest, IDepartment, IDocument, IFolder, ILocker, IUser } from './item'; +import { IBorrowRequest, IDepartment, IDocument, IFolder, ILocker, IRoom, IUser } from './item'; export type BaseResponse = { data: T; @@ -6,7 +6,7 @@ export type BaseResponse = { message: string | null; }; -export type GetPaginationResponse = { +export type PaginationResponse = { pageNumber: number; totalPages: number; totalCount: number; @@ -35,10 +35,10 @@ export type GetEmptyContainersResponse = BaseResponse< slot: number; }[]; }[]; - } & GetPaginationResponse + } & PaginationResponse >; -export type GetDocumentsResponse = BaseResponse<{ items: IDocument[] } & GetPaginationResponse>; +export type GetDocumentsResponse = BaseResponse<{ items: IDocument[] } & PaginationResponse>; export type GetDocumentByIdResponse = BaseResponse; @@ -49,17 +49,21 @@ export type PostRequestResponse = BaseResponse; export type GetRequestsResponse = BaseResponse< { items: IBorrowRequest[]; - } & GetPaginationResponse + } & PaginationResponse >; export type GetRequestByIdResponse = BaseResponse; export type GetUserByIdResponse = BaseResponse; -export type GetLockersResponse = BaseResponse<{ items: ILocker[] } & GetPaginationResponse>; +export type GetLockersResponse = BaseResponse<{ items: ILocker[] } & PaginationResponse>; export type GetLockerByIdResponse = BaseResponse; -export type GetFoldersResponse = BaseResponse<{ items: IFolder[] } & GetPaginationResponse>; +export type GetFoldersResponse = BaseResponse<{ items: IFolder[] } & PaginationResponse>; export type GetFolderByIdResponse = BaseResponse; + +export type GetRoomsResponse = BaseResponse<{ items: IRoom[] } & PaginationResponse>; + +export type GetRoomByIdResponse = BaseResponse; diff --git a/src/types/roles.ts b/src/types/roles.ts index 0a21789..5cc6f75 100644 --- a/src/types/roles.ts +++ b/src/types/roles.ts @@ -11,6 +11,45 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { { label: 'Home', path: AUTH_ROUTES.HOME, + icon: PrimeIcons.HOME, + }, + { + type: 'group', + label: 'Users', + }, + { + label: 'Employee', + path: AUTH_ROUTES.EMPLOYEES, + icon: PrimeIcons.USERS, + items: [ + { + label: 'Manage', + path: AUTH_ROUTES.EMPLOYEES_MANAGE, + icon: PrimeIcons.USERS, + }, + { + label: 'Create', + path: AUTH_ROUTES.NEW_EMP, + icon: PrimeIcons.USER_PLUS, + }, + ], + }, + { + label: 'Staffs', + path: AUTH_ROUTES.STAFFS_MANAGE, + icon: PrimeIcons.USER, + // items: [ + // { + // label: 'Manage', + // path: AUTH_ROUTES.STAFFS_MANAGE, + // icon: PrimeIcons.USER, + // }, + // // { + // // label: 'Create', + // // path: AUTH_ROUTES.NEW_STAFF, + // // icon: PrimeIcons.USER_PLUS, + // // }, + // ], }, { type: 'group', @@ -18,22 +57,36 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { }, { label: 'Physical', + icon: PrimeIcons.FOLDER_OPEN, items: [ + { + label: 'Rooms', + path: AUTH_ROUTES.ROOMS, + icon: PrimeIcons.BUILDING, + }, { label: 'Lockers', path: AUTH_ROUTES.LOCKERS, + icon: PrimeIcons.LOCK, }, { label: 'Folders', path: AUTH_ROUTES.FOLDERS, + icon: PrimeIcons.FOLDER, }, { label: 'Documents', path: AUTH_ROUTES.DOCUMENTS, + icon: PrimeIcons.FILE, }, ], path: AUTH_ROUTES.PHYSICAL, }, + { + label: 'Digital', + path: AUTH_ROUTES.DRIVE, + icon: PrimeIcons.CLOUD, + }, { type: 'group', label: 'Borrowed docs', @@ -41,10 +94,7 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { { label: 'Requests', path: AUTH_ROUTES.REQUESTS, - }, - { - label: 'Returns', - path: AUTH_ROUTES.RETURNS, + icon: PrimeIcons.BELL, }, ], // Staff sidebar From f63da65e7803820bfdb7c34949c6c2ae208e29a8 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Thu, 15 Jun 2023 08:50:03 +0700 Subject: [PATCH 02/18] Change placeholder --- src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx | 3 ++- src/pages/admin/AdminStaffPage/AdminStaffPage.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx b/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx index d20793c..3580892 100644 --- a/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx +++ b/src/pages/admin/AdminEmployeePage/AdminEmployeePage.tsx @@ -15,6 +15,7 @@ const AdminEmployeePage = () => { const { getPaginatedTableProps, refetch } = usePagination({ key: 'employees', url: '/users', + query: query.current, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'EMPLOYEES_MANAGE' }); @@ -32,7 +33,7 @@ const AdminEmployeePage = () => { (query.current = e.target.value)} - placeholder='locker a' + placeholder='employee a' />
+
+ ); +}; + +export default AdminDepartmentDetailPage; diff --git a/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx b/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx new file mode 100644 index 0000000..9f64c3d --- /dev/null +++ b/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx @@ -0,0 +1,65 @@ +import Table from '@/components/Table/Table.component'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { IDepartment } from '@/types/item'; +import usePagination from '@/hooks/usePagination'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import Status from '@/components/Status/Status.component'; +import { useRef } from 'react'; + +const AdminDepartmentPage = () => { + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['departments', query.current], + url: '/departments', + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DEPARTMENTS' }); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + placeholder='department a' + /> +
+
+ + + + + + } + /> + + +
+
+
+ ); +}; + +export default AdminDepartmentPage; diff --git a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx index 0e896bd..7b3471d 100644 --- a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx +++ b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx @@ -4,7 +4,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { BaseResponse, GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -25,17 +25,19 @@ const AdminDocumentDetailPage = () => { const { data, isLoading, error } = useQuery( ['documents', documentId], - async () => (await axiosClient.get(`/documents/${documentId}`)).data, - { - onSuccess: async (data) => { - const { id } = data?.data || { id: '' }; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, - } + async () => (await axiosClient.get(`/documents/${documentId}`)).data ); + useEffect(() => { + const renderQr = async () => { + const { id } = data?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }; + renderQr(); + }, [data]); + if (isLoading) return ; if ((error as AxiosError)?.response?.status === 404 || !data) @@ -43,18 +45,33 @@ const AdminDocumentDetailPage = () => { const { title, - folder: { - id: folderId, - name: folderName, - locker: { - id: lockerId, - name: lockerName, - room: { id: roomId, name: roomName }, + // folder: { + // id: folderId, + // name: folderName, + // locker: { id: lockerId, name: lockerName }, + // }, + } = data.data; + + const folder = data.data.folder || { + id: '', + name: '', + locker: { + id: '', + name: '', + room: { + id: '', + name: '', }, }, - } = data.data; + }; + + const { + id: folderId, + name: folderName, + locker: { id: lockerId, name: lockerName, room: { id: roomId, name: roomName } } = { room: {} }, + } = folder; - const initialValues = data.data; + const initialValues = { ...data.data, folder }; type FormValues = typeof initialValues; @@ -85,7 +102,7 @@ const AdminDocumentDetailPage = () => { return (
-

+

/ {roomName} diff --git a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx index af6b64b..b274871 100644 --- a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx +++ b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx @@ -1,9 +1,7 @@ import Table from '@/components/Table/Table.component'; -import { AUTH_ROUTES } from '@/constants/routes'; import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; import { InputText } from 'primereact/inputtext'; -import { Link } from 'react-router-dom'; import { IDocument } from '@/types/item'; import usePagination from '@/hooks/usePagination'; import useNavigateSelect from '@/hooks/useNavigateSelect'; @@ -38,9 +36,6 @@ const AdminDocumentPage = () => { /> -

diff --git a/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx b/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx index dbb88fc..ed712bf 100644 --- a/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx +++ b/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx @@ -26,7 +26,7 @@ type FormValues = typeof initialValues; const AdminFolderCreatePage = () => { const navigate = useNavigate(); - const { lockers, lockersRefetch } = useLockers(); + const { lockers, lockersRefetch } = useLockers(true); const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { try { @@ -123,6 +123,13 @@ const AdminFolderCreatePage = () => { error={touched.locker && !!errors.locker} small={touched.locker ? errors.locker : undefined} disabled={isSubmitting} + optionGroupLabel='room' + optionGroupChildren='lockers' + optionGroupTemplate={(option) => ( +
+ {option.room} +
+ )} /> { const queryClient = useQueryClient(); const [editMode, setEditMode] = useState(false); const [error, setError] = useState(''); + const navigate = useNavigate(); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); @@ -133,10 +134,21 @@ const AdminFolderDetailPage = () => { } }; + const onDelete = async () => { + try { + await axiosClient.delete(`/folders/${folderId}`); + queryClient.invalidateQueries('folders'); + navigate(AUTH_ROUTES.FOLDERS); + } catch (error) { + const axiosError = error as AxiosError; + setError(axiosError.response?.data.message || 'Something went wrong'); + } + }; + return (
-

+

/ {roomName} @@ -266,16 +278,23 @@ const AdminFolderDetailPage = () => { disabled={editMode || isSubmitting || !isValid} /> )} - -

+ +
+ +
+
{ const queryClient = useQueryClient(); const [editMode, setEditMode] = useState(false); const [error, setError] = useState(''); + const navigate = useNavigate(); const { data: room, @@ -62,7 +63,7 @@ const AdminRoomDetailPage = () => { if (isLoading) return ; if ((axiosError as AxiosError)?.response?.status === 404 || !room) - return ; + return ; const { name: roomName, capacity, numberOfLockers, description, isAvailable } = room.data; @@ -80,11 +81,12 @@ const AdminRoomDetailPage = () => { const onToggleAvailability = async () => { try { - if (isAvailable) { - await axiosClient.put(`/rooms/disable/${roomId}`); - } else { - await axiosClient.put(`/rooms/enable/${roomId}`); - } + await axiosClient.put(`/rooms/${roomId}`, { + name: roomName, + description, + capacity, + isAvailable: !isAvailable, + }); queryClient.invalidateQueries('rooms'); } catch (error) { const axiosError = error as AxiosError; @@ -124,10 +126,21 @@ const AdminRoomDetailPage = () => { } }; + const onDelete = async () => { + try { + await axiosClient.delete(`/rooms/${roomId}`); + queryClient.invalidateQueries('rooms'); + navigate(AUTH_ROUTES.ROOMS); + } catch (error) { + const axiosError = error as AxiosError; + setError(axiosError.response?.data.message || 'Something went wrong'); + } + }; + return (
-

+

/ {roomName}

@@ -150,7 +163,7 @@ const AdminRoomDetailPage = () => {
{' '}
- + {
+ +
{ } + body={(item: ILocker) => } />
diff --git a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx index 27220d0..b478fbc 100644 --- a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx +++ b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx @@ -1,5 +1,117 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import usePagination from '@/hooks/usePagination'; +import { IRoom, IStaff } from '@/types/item'; +import { BaseResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Formik } from 'formik'; +import { Button } from 'primereact/button'; +import { useState } from 'react'; +import { useNavigate } from 'react-router'; + const AdminStaffCreatePage = () => { - return
; + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const params = new URLSearchParams(window.location.search); + + const initialValues = { + staffId: params.get('staffId') || '', + roomId: '', + }; + + type FormValues = typeof initialValues; + + const { data: staffs } = usePagination({ + key: ['staffs', 'unassigned'], + url: '/staffs', + }); + + const { data: rooms } = usePagination({ + key: ['rooms', 'unassigned'], + url: '/rooms', + }); + + const staffOptions = staffs?.data.items + // .filter((staff) => staff.room === null) + .map((staff) => ({ + label: `${staff.user.firstName} ${staff.user.lastName}`, + value: staff.id, + room: staff.room, + })); + + const roomOptions = rooms?.data.items + .filter((room) => room.staffId === null && room.isAvailable) + .map((room) => ({ + label: room.name, + value: room.id, + })); + + const onSubmit = async (values: FormValues) => { + try { + await axiosClient.post('/staffs', values); + navigate(AUTH_ROUTES.STAFFS_MANAGE); + } catch (error) { + const axiosError = error as AxiosError; + console.error(error); + setError(axiosError.response?.data.message || 'Something went wrong'); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (!value) { + error[key as keyof FormValues] = 'Required'; + } + }); + return error; + }; + + return ( + + {({ handleSubmit, handleBlur, handleChange, errors, touched, values }) => ( + + + ( +
+
{option.label}
+
{option.room?.name || 'Unassigned'}
+
+ )} + /> + + {error &&
{error}
} + +
- +
+ navigate(`${AUTH_ROUTES.NEW_STAFF}?staffId=${(e.value as { id: string }).id}`) + } + {...getPaginatedTableProps()} + > {/* { /> */} - - + <>{staff.room?.department?.name || 'N/A'}} + /> + <>{staff.room?.name || 'N/A'}} + /> { ); }; -export default AdminEmployeePage; +export default AdminStaffPage; diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 0a1af1d..4c4af9b 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -15,3 +15,8 @@ export { default as AdminLockerCreatePage } from './AdminLockerCreatePage/AdminL export { default as AdminRoomCreatePage } from './AdminRoomCreatePage/AdminRoomCreatePage'; export { default as AdminFolderCreatePage } from './AdminFolderCreatePage/AdminFolderCreatePage'; export { default as AdminRoomPage } from './AdminRoomPage/AdminRoomPage'; +export { default as AdminRoomDetailPage } from './AdminRoomDetailPage/AdminRoomDetailPage'; +export { default as AdminStaffCreatePage } from './AdminStaffCreatePage/AdminStaffCreatePage'; +export { default as AdminDepartmentPage } from './AdminDepartmentPage/AdminDepartmentPage'; +export { default as AdminDepartmentCreatePage } from './AdminDepartmentCreatePage/AdminDepartmentCreatePage'; +export { default as AdminDepartmentDetailPage } from './AdminDepartmentDetailPage/AdminDepartmentDetailPage'; \ No newline at end of file diff --git a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx index 4f3020b..7069b51 100644 --- a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx +++ b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx @@ -1,7 +1,5 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { Link } from 'react-router-dom'; -import { useContext } from 'react'; -import { AuthContext } from '@/context/authContext'; import axiosClient from '@/utils/axiosClient'; import { GetDocumentByIdResponse, GetRequestsResponse } from '@/types/response'; import { REFETCH_CONFIG } from '@/constants/config'; @@ -12,17 +10,12 @@ import Status from '@/components/Status/Status.component'; import { dateFormatter } from '@/utils/formatter'; const EmpDashboardPage = () => { - const { user } = useContext(AuthContext); - - const roomId = user?.department.roomId; - const { data: requests, isLoading: isRequestsLoading } = useQuery( ['requests', 'recent'], async () => ( - await axiosClient.get('/borrows/employees', { + await axiosClient.get('/documents/borrows', { params: { - roomId, sortOrder: 'desc', size: 4, page: 1, diff --git a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx index a824c11..28da291 100644 --- a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx +++ b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx @@ -4,7 +4,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -13,25 +13,36 @@ import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.com import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; import { SkeletonPage } from '@/components/Skeleton'; import Status from '@/components/Status/Status.component'; +import { AxiosError } from 'axios'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; const EmpDocumentDetailPage = () => { const { documentId = '' } = useParams<{ documentId: string }>(); const [qr, setQr] = useState(''); - const { data: doc, isLoading } = useQuery( + const { + data: doc, + isLoading, + error: axiosError, + } = useQuery( ['documents', documentId], - async () => (await axiosClient.get(`/documents/${documentId}`)).data, - { - onSuccess: async (data) => { - const { id } = data?.data || { id: '' }; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, - } + async () => (await axiosClient.get(`/documents/${documentId}`)).data ); - if (isLoading || !doc) return ; + useEffect(() => { + const renderQr = async () => { + const { id } = doc?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }; + renderQr(); + }, [doc]); + + if (isLoading) return ; + + if ((axiosError as AxiosError)?.response?.status === 404 || !doc) + return ; const { title, @@ -48,7 +59,7 @@ const EmpDocumentDetailPage = () => { return (
-

+

/ {lockerName} diff --git a/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx b/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx index 15663db..be1b644 100644 --- a/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx +++ b/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx @@ -15,7 +15,7 @@ const EmpDocumentPage = () => { const { getPaginatedTableProps, refetch } = usePagination({ key: ['documents', query.current], - url: '/documents', + url: '/documents/employees', query: query.current, }); @@ -38,7 +38,7 @@ const EmpDocumentPage = () => { />

diff --git a/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx new file mode 100644 index 0000000..e09f127 --- /dev/null +++ b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx @@ -0,0 +1,228 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import axiosClient from '@/utils/axiosClient'; +import { Formik, FormikHelpers } from 'formik'; +import { Button } from 'primereact/button'; +import { useNavigate } from 'react-router'; +import { useContext, useEffect } from 'react'; +import { AuthContext } from '@/context/authContext'; +import { AxiosError } from 'axios'; +import useDocumentTypes from '@/hooks/useDocumentTypes'; +import useRooms from '@/hooks/useRooms'; +import { BaseResponse } from '@/types/response'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import { InputSwitch } from 'primereact/inputswitch'; + +const RequiredValues = { + title: '', + documentType: '', + description: '', + roomId: '', + importReason: '', + isPrivate: true, +}; + +const NOT_REQUIRED = ['description']; + +type FormValues = typeof RequiredValues; + +const EmpImportPage = () => { + const navigate = useNavigate(); + const { user } = useContext(AuthContext); + + const { documentTypes, typesRefetch } = useDocumentTypes(); + + const departmentId = user?.department.id || ''; + + const { rooms, roomsRefetch } = useRooms(false, departmentId); + + const onSubmit = async ( + values: FormValues, + { setSubmitting, setFieldError }: FormikHelpers + ) => { + try { + const result = await axiosClient.post('/documents/import-requests', values); + navigate(`${AUTH_ROUTES.IMPORT_MANAGE}/${result.data.data.id}`); + } catch (error) { + console.log(error); + const axiosError = error as AxiosError; + const msg = axiosError.response?.data.message || 'Incorrect format'; + const status = axiosError.response?.status; + + if (status === 409) { + setFieldError('title', msg); + } + if (status === 400) { + setFieldError('id', msg); + } + } finally { + setSubmitting(false); + } + }; + + const onValidate = (values: FormValues) => { + const errors: { [key: string]: string } = {}; + Object.entries(values).forEach(([key, value]) => { + if (!value && NOT_REQUIRED.indexOf(key) === -1) { + errors[key] = 'This field is required'; + } + }); + return errors; + }; + + useEffect(() => { + const getConfigs = async () => { + await typesRefetch(); + await roomsRefetch(); + }; + getConfigs(); + }, [typesRefetch, roomsRefetch]); + + return ( + + {({ + values, + errors, + touched, + handleBlur, + handleChange, + handleSubmit, + setFieldValue, + setFieldTouched, + isSubmitting, + isValid, + }) => { + return ( + <> +
+
+ + + ( +
+ {value === '' ? ( + 'No item selected' + ) : options?.some( + (option) => option.id.toUpperCase() === value.toUpperCase() + ) ? ( + <> + {value.toUpperCase()} selected + + ) : ( + <> + {value.toUpperCase()} will be added + + )} +
+ )} + /> +
+ + setFieldValue('isPrivate', e.value)} + /> +
+ + +
+ + { + setFieldValue('roomId', ''); + setFieldTouched('roomId', false); + handleChange(e); + }} + onBlur={handleBlur} + value={values.roomId} + error={touched.roomId && !!errors.roomId} + small={touched.roomId ? errors.roomId : undefined} + disabled={isSubmitting} + itemTemplate={(option) => ( +
+
+ {option.name} - Free: {option.capacity - option.numberOfLockers}/ + {option.capacity} +
+
{option.description}
+
+ )} + /> +
+ + + ); + }} +
+ ); +}; + +export default EmpImportPage; diff --git a/src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx b/src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx new file mode 100644 index 0000000..4342c42 --- /dev/null +++ b/src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx @@ -0,0 +1,65 @@ +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import { SkeletonPage } from '@/components/Skeleton'; +import Status from '@/components/Status/Status.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { GetImportByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; +import { useParams } from 'react-router'; + +const ImportDetailPagePage = () => { + const { importId } = useParams<{ importId: string }>(); + + const { + data: importRequest, + isLoading, + error: axiosError, + } = useQuery( + ['import', importId], + async () => + (await axiosClient.get(`/documents/import-requests/${importId}`)).data, + { + enabled: !!importId, + } + ); + + if (isLoading) return ; + + if ((axiosError as AxiosError)?.response?.status === 404 || !importRequest) + return ; + + const { + document: { title, documentType, description, isPrivate }, + importReason, + staffReason, + status: importStatus, + room: { name }, + } = importRequest.data; + + return ( +
+ + } + /> + + + + + + + + + + +
+ ); +}; + +export default ImportDetailPagePage; diff --git a/src/pages/emp/EmpImportPage/EmpImportPage.tsx b/src/pages/emp/EmpImportPage/EmpImportPage.tsx new file mode 100644 index 0000000..f0b3281 --- /dev/null +++ b/src/pages/emp/EmpImportPage/EmpImportPage.tsx @@ -0,0 +1,70 @@ +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import usePagination from '@/hooks/usePagination'; +import { IDocument, IImportRequest } from '@/types/item'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { Link } from 'react-router-dom'; +import { useRef } from 'react'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; + +const EmpImportPage = () => { + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['imports', query.current], + url: '/documents/import-requests', + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'IMPORT_MANAGE' }); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> + + +
+
+
+ + + + + } + /> + + +
+
+
+ ); +}; + +export default EmpImportPage; diff --git a/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx b/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx index 046da1a..54dda30 100644 --- a/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx +++ b/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx @@ -5,13 +5,14 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { GetDocumentByIdResponse, GetRequestByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery } from 'react-query'; import { Link, Navigate, useParams } from 'react-router-dom'; import QRCode from 'qrcode'; import { SkeletonPage } from '@/components/Skeleton'; import CustomCalendar from '@/components/Calendar/Calendar.component'; import { REQUEST_STATUS } from '@/constants/status'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; const NO_ACTIONS = [ REQUEST_STATUS.Cancelled.status, @@ -22,22 +23,49 @@ const NO_ACTIONS = [ REQUEST_STATUS.NotProcessable.status, ]; +type Values = { + borrowReason: string; + borrowTime?: string | null | Date; + dueTime?: string | null | Date; +}; + const EmpRequestDetailPage = () => { const { requestId } = useParams<{ requestId: string }>(); const [qr, setQr] = useState(''); const { data, refetch: refetchRequest } = useQuery( ['requests', requestId], - async () => (await axiosClient.get(`/borrows/${requestId}`)).data, + async () => + (await axiosClient.get(`/documents/borrows/${requestId}`)).data, { enabled: !!requestId, - onSuccess: async (data) => { - const id = data?.data?.documentId || ''; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, } ); + const [editMode, setEditMode] = useState(false); + const [values, setValues] = useState({ + borrowReason: '', + borrowTime: '', + dueTime: '', + }); + + useEffect(() => { + const renderQr = async () => { + const { documentId } = data?.data || { documentId: '' }; + if (!documentId) return; + const qrCode = await QRCode.toDataURL(documentId); + setQr(qrCode); + }; + + const updateValues = () => { + const { borrowReason, borrowTime, dueTime } = data?.data || { + borrowReason: 'This is a reason', + borrowTime: new Date(), + dueTime: new Date(new Date().setDate(new Date().getDate() + 7)), + }; + setValues({ borrowReason, borrowTime, dueTime }); + }; + updateValues(); + renderQr(); + }, [data]); const { documentId } = data ? data.data : { documentId: '' }; @@ -51,28 +79,69 @@ const EmpRequestDetailPage = () => { if (!requestId) return ; - if (!data || !document || isLoading) return ; + if (isLoading) return ; + // if (!document || !data) + // return ( + // + // ); + + // const { + // title, + // documentType, + // // folder: { + // // name: folder, + // // locker: { name: locker }, + // // }, + // } = document.data; + + const folder = '', + locker = '', + title = '', + documentType = ''; - const { - title, - documentType, - folder: { - name: folder, - locker: { name: locker }, - }, - } = document.data; + const status = 'Pending'; - const { status, reason, borrowTime, dueTime } = data.data; + // const { status, borrowReason, borrowTime, dueTime } = data.data; const onCancel = async () => { try { - await axiosClient.post(`/borrows/cancel/${requestId}`); + await axiosClient.post(`/documents/borrows/cancel/${requestId}`); + await refetchRequest(); + } catch (error) { + console.log(error); + } + }; + + const onUpdate = async () => { + if (!values.borrowReason || !values.borrowTime || !values.dueTime) return; + if (JSON.stringify(values) === JSON.stringify(data?.data)) return; + try { + await axiosClient.put(`/documents/borrows/${requestId}`, { + reason: values.borrowReason, + borrowFrom: values.borrowTime, + borrowTo: values.dueTime, + }); await refetchRequest(); + setEditMode(false); } catch (error) { console.log(error); } }; + const onEditCancel = () => { + setEditMode(false); + setValues({ + borrowReason: data?.data.borrowReason || 'This is a reason', + borrowTime: data?.data.borrowTime || new Date(), + dueTime: data?.data.dueTime || new Date(new Date().setDate(new Date().getDate() + 7)), + }); + }; + return (
@@ -86,21 +155,32 @@ const EmpRequestDetailPage = () => { - + setValues((prev) => ({ ...prev, borrowReason: e.target.value }))} + error={values.borrowReason === ''} + small={values.borrowReason === '' ? 'Please enter a reason' : ''} + />
- { + if (e.value === null) { + setValues((prev) => ({ ...prev, borrowTime: null, dueTime: null })); + return; + } + const [borrowTime, dueTime] = e.value as Date[]; + setValues((prev) => ({ ...prev, borrowTime, dueTime })); + }} + error={!values.borrowTime || !values.dueTime} + small={!values.borrowTime || !values.dueTime ? 'Please select a date' : ''} />
@@ -112,12 +192,26 @@ const EmpRequestDetailPage = () => { ) : (
)} + {status === REQUEST_STATUS.Pending.status && ( +
diff --git a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx index 6679001..393c2c9 100644 --- a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx +++ b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx @@ -4,7 +4,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { BaseResponse, GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -25,17 +25,19 @@ const StaffDocumentDetailPage = () => { const { data, isLoading, error } = useQuery( ['documents', documentId], - async () => (await axiosClient.get(`/documents/${documentId}`)).data, - { - onSuccess: async (data) => { - const { id } = data?.data || { id: '' }; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, - } + async () => (await axiosClient.get(`/documents/${documentId}`)).data ); + useEffect(() => { + const renderQr = async () => { + const { id } = data?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }; + renderQr(); + }, [data]); + if (isLoading) return ; if ((error as AxiosError)?.response?.status === 404 || !data) @@ -43,14 +45,31 @@ const StaffDocumentDetailPage = () => { const { title, - folder: { - id: folderId, - name: folderName, - locker: { id: lockerId, name: lockerName }, - }, + // folder: { + // id: folderId, + // name: folderName, + // locker: { id: lockerId, name: lockerName }, + // }, } = data.data; - const initialValues = data.data; + console.log(data.data.folder); + + const folder = data.data.folder || { + id: '', + name: '', + locker: { + id: '', + name: '', + }, + }; + + const { + id: folderId, + name: folderName, + locker: { id: lockerId, name: lockerName } = {}, + } = folder; + + const initialValues = { ...data.data, folder }; type FormValues = typeof initialValues; @@ -81,15 +100,23 @@ const StaffDocumentDetailPage = () => { return (
-

- / - - {lockerName} - - / - - {folderName} - +

+ {lockerId && ( + <> + / + + {lockerName} + + + )} + {folderId && ( + <> + / + + {folderName} + + + )} / {title}

diff --git a/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx b/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx index 9f11b2a..ed960c5 100644 --- a/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx +++ b/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx @@ -38,7 +38,7 @@ const StaffDocumentPage = () => { />
diff --git a/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx b/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx index eebc71a..b5030a6 100644 --- a/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx +++ b/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx @@ -31,7 +31,7 @@ const StaffFolderDetailPage = () => { const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); - const roomId = user?.department.roomId; + const roomId = user?.roomId; const { data: folder, @@ -138,7 +138,7 @@ const StaffFolderDetailPage = () => { return (
-

+

/ {lockerName} diff --git a/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx b/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx index 175e26f..d146efd 100644 --- a/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx +++ b/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx @@ -38,7 +38,7 @@ const StaffFolderPage = () => { />

diff --git a/src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx b/src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx new file mode 100644 index 0000000..8072f20 --- /dev/null +++ b/src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx @@ -0,0 +1,14 @@ +import ImportDocumentContainer from '@/containers/ImportDocumentContainer/ImportDocumentContainer'; + +const StaffImportCreatePage = () => { + return ( +
+
+

Importing documents

+
+ +
+ ); +}; + +export default StaffImportCreatePage; diff --git a/src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx b/src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx new file mode 100644 index 0000000..d3a793d --- /dev/null +++ b/src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx @@ -0,0 +1,299 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import Overlay from '@/components/Overlay/Overlay.component'; +import Progress from '@/components/Progress/Progress.component'; +import { SkeletonPage } from '@/components/Skeleton'; +import Status from '@/components/Status/Status.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { AuthContext } from '@/context/authContext'; +import useEmptyContainers from '@/hooks/useEmptyContainers'; +import { GetImportByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { Button } from 'primereact/button'; +import { useState, useContext, useEffect } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; +import { useNavigate, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; + +const StaffImportDetailPage = () => { + const { importId } = useParams<{ importId: string }>(); + const [showModal, setShowModal] = useState(''); + const [reason, setReason] = useState(''); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const { user } = useContext(AuthContext); + const [selected, setSelected] = useState({ + folder: '', + locker: '', + }); + + const { + data: importRequest, + isLoading, + error, + } = useQuery( + ['imports', importId], + async () => + (await axiosClient.get(`/documents/import-requests/${importId}`)).data, + { + enabled: !!importId, + } + ); + + const { availableFolders, availableLockers, containerRefetch } = useEmptyContainers({ + roomId: user?.roomId || '', + }); + + useEffect(() => { + containerRefetch(); + }, [containerRefetch]); + + if (isLoading) return ; + if (error || !importRequest) + return ; + + const { + document: { title, documentType, isPrivate, description, id: documentId, folder }, + room: { name: roomName }, + importReason, + staffReason, + status: importStatus, + } = importRequest.data; + + const onApprove = async () => { + if (!reason) return; + try { + await axiosClient.put(`/documents/import-requests/${importId}`, { + decision: 'Approve', + staffReason: reason, + }); + queryClient.invalidateQueries('imports'); + setShowModal(''); + } catch (error) { + console.log(error); + } + }; + + const onReject = async () => { + if (!reason) return; + try { + await axiosClient.put(`/documents/import-requests/${importId}`, { + decision: 'Reject', + staffReason: reason, + }); + setShowModal(''); + queryClient.invalidateQueries('imports'); + } catch (error) { + console.log(error); + } + }; + + const onAssign = async () => { + if (!selected.folder || !selected.locker) return; + try { + await axiosClient.put(`/documents/import-requests/assign/${importId}`, { + folderId: selected.folder, + }); + setShowModal(''); + queryClient.invalidateQueries('imports'); + } catch (error) { + console.log(error); + } + }; + + const onCheckIn = async () => { + try { + await axiosClient.put(`/documents/import-requests/checkin/${documentId}`); + navigate(`${AUTH_ROUTES.DOCUMENTS}/${documentId}`); + } catch (error) { + console.log(error); + } + }; + + return ( +
+ + } + /> + + + + {folder && ( +
+ + +
+ )} +
+
+ + {importStatus === 'Pending' && ( +
+
+ )} +
+ {importStatus === 'Approved' ? ( +
+
+ + + + + + +
+ {showModal && ( + setShowModal('')} className='flex items-center justify-center'> +
e.stopPropagation()}> +
+
Confirmation
+ setShowModal('')} + /> +
+
+ {showModal === 'approve' + ? 'Are you sure you want to approve this request?' + : showModal === 'reject' + ? 'Are you sure you want to reject this request?' + : 'Choose the folder to assign this document to'} +
+ {showModal === 'assign' ? ( +
+ ( +
+
+ {option.name} - Free: {option.free}/{option.max} +
+
{option.description}
+
+ )} + onChange={(e) => { + setSelected((prev) => ({ ...prev, locker: e.value })); + }} + value={selected.locker} + /> + ( +
+ {option.name} - Free: {option.free}/{option.max} +
+ )} + onChange={(e) => { + setSelected((prev) => ({ ...prev, folder: e.value })); + }} + value={selected.folder} + /> + {selected.folder && availableFolders && ( + value.id === selected.folder + )?.free || 0 + } + max={ + availableFolders[selected.locker].find( + (value) => value.id === selected.folder + )?.max || 0 + } + /> + )} +
+ ) : ( + setReason(e.target.value)} + placeholder='Enter your reason here' + /> + )} +
+
+
+
+ )} +
+ ); +}; + +export default StaffImportDetailPage; diff --git a/src/pages/staff/StaffImportPage/StaffImportPage.tsx b/src/pages/staff/StaffImportPage/StaffImportPage.tsx index e182f8f..f29586f 100644 --- a/src/pages/staff/StaffImportPage/StaffImportPage.tsx +++ b/src/pages/staff/StaffImportPage/StaffImportPage.tsx @@ -1,12 +1,72 @@ -import ImportDocumentContainer from '@/containers/ImportDocumentContainer/ImportDocumentContainer'; +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import usePagination from '@/hooks/usePagination'; +import { IImportRequest } from '@/types/item'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { Link } from 'react-router-dom'; +import { useContext, useRef } from 'react'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import { AuthContext } from '@/context/authContext'; const StaffImportPage = () => { + const { user } = useContext(AuthContext); + const query = useRef(''); + + const roomId = user?.roomId || ''; + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['imports', query.current, roomId], + url: `/documents/import-requests?roomId=${roomId}`, + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'IMPORT_MANAGE' }); + return (
-
-

Importing documents

+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> + + +
+
+ + + + + + } + /> + + +
-
); }; diff --git a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx index 70af808..c51fff0 100644 --- a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx +++ b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx @@ -30,7 +30,7 @@ const StaffLockerDetailPage = () => { const [editMode, setEditMode] = useState(false); const [error, setError] = useState(''); - const roomId = user?.department.roomId || ''; + const roomId = user?.roomId || ''; const { data: locker, @@ -136,7 +136,7 @@ const StaffLockerDetailPage = () => { return (
-

+

/ {lockerName}

diff --git a/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx b/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx index b8b8653..0d652f9 100644 --- a/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx +++ b/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx @@ -38,7 +38,7 @@ const StaffLockerPage = () => { />
diff --git a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx index dc774be..d01b9c3 100644 --- a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx +++ b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx @@ -1,6 +1,8 @@ /* eslint-disable no-mixed-spaces-and-tabs */ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import Overlay from '@/components/Overlay/Overlay.component'; import { SkeletonPage } from '@/components/Skeleton'; import { AUTH_ROUTES } from '@/constants/routes'; import { REQUEST_STATUS } from '@/constants/status'; @@ -12,6 +14,8 @@ import { } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { AxiosError } from 'axios'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; import { Button } from 'primereact/button'; import { useState } from 'react'; import { useQuery } from 'react-query'; @@ -29,9 +33,12 @@ const NO_ACTIONS = [ const StaffRequestDetailPage = () => { const { requestId } = useParams<{ requestId: string }>(); const [error, setError] = useState(''); + const [showModal, setShowModal] = useState(''); + const [reason, setReason] = useState(''); const { data, refetch } = useQuery( ['requests', requestId], - async () => (await axiosClient.get(`/borrows/${requestId}`)).data, + async () => + (await axiosClient.get(`/documents/borrows/${requestId}`)).data, { enabled: !!requestId, } @@ -62,19 +69,25 @@ const StaffRequestDetailPage = () => { const { title, documentType, - folder: { - name: folder, - locker: { name: locker }, - }, + // folder: { + // name: folder, + // locker: { name: locker }, + // }, } = document.data; + const folder = '', + locker = ''; + const { id: employeeId, lastName, firstName } = employee.data; - const { borrowTime, dueTime, reason, status } = data.data; + const { borrowTime, dueTime, borrowReason, status } = data.data; const onApprove = async () => { try { - await axiosClient.post(`/borrows/approve/${requestId}`); + await axiosClient.put(`/documents/borrows/staffs/${requestId}`, { + staffReason: reason, + decision: 'approve', + }); await refetch(); } catch (error) { const axiosError = error as AxiosError; @@ -84,9 +97,12 @@ const StaffRequestDetailPage = () => { } }; - const onDeny = async () => { + const onReject = async () => { try { - await axiosClient.post(`/borrows/reject/${requestId}`); + await axiosClient.put(`/documents/borrows/staffs/${requestId}`, { + staffReason: reason, + decision: 'reject', + }); await refetch(); } catch (error) { const axiosError = error as AxiosError; @@ -98,7 +114,7 @@ const StaffRequestDetailPage = () => { const onCheckout = async () => { try { - await axiosClient.post(`/borrows/checkout/${requestId}`); + await axiosClient.post(`/documents/borrows/checkout/${requestId}`); await refetch(); } catch (error) { const axiosError = error as AxiosError; @@ -131,7 +147,7 @@ const StaffRequestDetailPage = () => { - +
@@ -140,13 +156,21 @@ const StaffRequestDetailPage = () => {
+ {showModal && ( + setShowModal('')} className='flex items-center justify-center'> +
e.stopPropagation()}> +
+
Confirmation
+ setShowModal('')} + /> +
+
+ {showModal === 'approve' + ? 'Are you sure you want to approve this request?' + : showModal === 'reject' + ? 'Are you sure you want to reject this request?' + : 'Choose the folder to assign this document to'} +
+ setReason(e.target.value)} + placeholder='Enter your reason here' + /> +
+
+
+
+ )}
); }; diff --git a/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx b/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx index 3db9d3e..19e305e 100644 --- a/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx +++ b/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx @@ -1,15 +1,19 @@ import Status from '@/components/Status/Status.component'; import Table from '@/components/Table/Table.component'; +import { AuthContext } from '@/context/authContext'; import useNavigateSelect from '@/hooks/useNavigateSelect'; import usePagination from '@/hooks/usePagination'; import { IBorrowRequest } from '@/types/item'; import { dateFormatter } from '@/utils/formatter'; import { Column } from 'primereact/column'; +import { useContext } from 'react'; const StaffRequestPage = () => { + const { user } = useContext(AuthContext); + const { getPaginatedTableProps } = usePagination({ key: 'requests', - url: '/borrows/staffs', + url: `/documents/borrows?roomId=${user?.roomId}`, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); diff --git a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx index 5f33a3b..42c84e2 100644 --- a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx +++ b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx @@ -16,7 +16,7 @@ import { OnResultFunction } from 'react-qr-reader'; import { useNavigate } from 'react-router'; const initialValues = { - id: 'N/A', + documentId: 'N/A', types: 'N/A', title: 'N/A', locker: 'N/A', @@ -32,28 +32,34 @@ const StaffReturnsPage = () => { const [values, setValues] = useState(initialValues); const [error, setError] = useState(''); - const { borrowerDepartment, borrowerId, borrowerName, folder, id, locker, title, types } = values; + const { borrowerDepartment, borrowerId, borrowerName, folder, documentId, locker, title, types } = + values; - const getDocumentsById = async (id: string) => { - if (!id) return; + const getDocumentsById = async (documentId: string) => { + if (!documentId) return; try { - const { data } = await axiosClient.get(`/borrows/documents/${id}`, { - params: { - status: 'checkedout,overdue', - page: 1, - size: 1, - sortBy: 'BorrowTime', - sortDirection: 'desc', - }, - }); + const { data } = await axiosClient.get( + `/documents/borrows/${documentId}`, + { + params: { + status: 'checkedout,overdue', + page: 1, + size: 1, + sortBy: 'BorrowTime', + sortDirection: 'desc', + }, + } + ); const request = data.data.items[0]; - const { data: document } = await axiosClient.get(`/documents/${id}`); + const { data: document } = await axiosClient.get( + `/documents/${documentId}` + ); const { data: employee } = await axiosClient.get( `/users/${request.borrowerId}` ); setValues((prev) => ({ ...prev, - id: document.data.id, + documentId: document.data.id, types: document.data.documentType, title: document.data.title, locker: document.data.folder.locker.name, @@ -72,7 +78,7 @@ const StaffReturnsPage = () => { const onScan: OnResultFunction = async (e) => { const id = e?.getText(); - if (!id || id === values.id) return; + if (!id || id === values.documentId) return; try { await getDocumentsById(id); setOpenScan(false); @@ -83,7 +89,7 @@ const StaffReturnsPage = () => { const onApprove = async () => { try { - await axiosClient.post(`/borrows/return/${id}`); + await axiosClient.post(`/documents/borrows/return/${documentId}`); navigate(AUTH_ROUTES.REQUESTS); } catch (error) { const axiosError = error as AxiosError; @@ -105,7 +111,7 @@ const StaffReturnsPage = () => {
- +
@@ -133,19 +139,19 @@ const StaffReturnsPage = () => { className='h-11 rounded-lg' onClick={() => setOpenScan((prev) => !prev)} /> - {id !== 'N/A' && ( + {documentId !== 'N/A' && (
diff --git a/src/pages/staff/index.ts b/src/pages/staff/index.ts index bd5c6fa..a038019 100644 --- a/src/pages/staff/index.ts +++ b/src/pages/staff/index.ts @@ -1,11 +1,13 @@ export { default as StaffDashboardPage } from './StaffDashboardPage/StaffDashboardPage'; export { default as StaffLockerPage } from './StaffLockerPage/StaffLockerPage'; export { default as StaffDocumentPage } from './StaffDocumentPage/StaffDocumentPage'; -export { default as StaffImportPage } from './StaffImportPage/StaffImportPage'; +export { default as StaffImportCreatePage } from './StaffImportCreatePage/StaffImportCreatePage'; export { default as StaffDocumentDetailPage } from './StaffDocumentDetailPage/StaffDocumentDetailPage'; export { default as StaffReturnPage } from './StaffReturnPage/StaffReturnPage'; export { default as StaffRequestDetailPage } from './StaffRequestDetailPage/StaffRequestDetailPage'; export { default as StaffRequestPage } from './StaffRequestPage/StaffRequestPage'; export { default as StaffLockerDetailPage } from './StaffLockerDetailPage/StaffLockerDetailPage'; export { default as StaffFolderPage } from './StaffFolderPage/StaffFolderPage'; -export { default as StaffFolderDetailPage } from './StaffFolderDetailPage/StaffFolderDetailPage'; \ No newline at end of file +export { default as StaffFolderDetailPage } from './StaffFolderDetailPage/StaffFolderDetailPage'; +export { default as StaffImportDetailPage } from './StaffImportDetailPage/StaffImportDetailPage'; +export { default as StaffImportPage } from './StaffImportPage/StaffImportPage'; \ No newline at end of file diff --git a/src/types/item.ts b/src/types/item.ts index 0c4198b..0992888 100644 --- a/src/types/item.ts +++ b/src/types/item.ts @@ -11,7 +11,6 @@ export interface IItem { export interface IDepartment { id: string; name: string; - roomId: string; } export interface IDocument { @@ -23,6 +22,7 @@ export interface IDocument { importer: IUser; folder: IFolder; status: DOCUMENT_STATUS_KEY; + isPrivate: boolean; } export interface IRoom { @@ -71,6 +71,13 @@ export interface IUser { createdBy: string; lastModified: string; lastModifiedBy: string; + roomId?: string; +} + +export interface IStaff { + user: IUser; + room: IRoom; + id: string; } export interface IBorrowRequest { @@ -80,6 +87,16 @@ export interface IBorrowRequest { borrowTime: string; dueTime: string; actualReturnTime: string; - reason: string; + borrowReason: string; + staffReason: string; status: REQUEST_STATUS_KEY; } + +export interface IImportRequest { + room: IRoom; + document: IDocument; + importReason: string; + staffReason: string; + status: string; + id: string; +} diff --git a/src/types/response.ts b/src/types/response.ts index 41c834c..2df90af 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -1,4 +1,13 @@ -import { IBorrowRequest, IDepartment, IDocument, IFolder, ILocker, IRoom, IUser } from './item'; +import { + IBorrowRequest, + IDepartment, + IDocument, + IFolder, + IImportRequest, + ILocker, + IRoom, + IUser, +} from './item'; export type BaseResponse = { data: T; @@ -67,3 +76,7 @@ export type GetFolderByIdResponse = BaseResponse; export type GetRoomsResponse = BaseResponse<{ items: IRoom[] } & PaginationResponse>; export type GetRoomByIdResponse = BaseResponse; + +export type GetDepartmentByIdResponse = BaseResponse; + +export type GetImportByIdResponse = BaseResponse; diff --git a/src/types/roles.ts b/src/types/roles.ts index 5cc6f75..714bbb8 100644 --- a/src/types/roles.ts +++ b/src/types/roles.ts @@ -13,6 +13,20 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.HOME, icon: PrimeIcons.HOME, }, + { + type: 'group', + label: 'Departments', + }, + { + label: 'Manage', + path: AUTH_ROUTES.DEPARTMENTS_MANAGE, + icon: PrimeIcons.BUILDING, + }, + { + label: 'Create', + path: AUTH_ROUTES.NEW_DEPARTMENT, + icon: PrimeIcons.PLUS, + }, { type: 'group', label: 'Users', @@ -36,20 +50,20 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { }, { label: 'Staffs', - path: AUTH_ROUTES.STAFFS_MANAGE, + path: AUTH_ROUTES.STAFFS, icon: PrimeIcons.USER, - // items: [ - // { - // label: 'Manage', - // path: AUTH_ROUTES.STAFFS_MANAGE, - // icon: PrimeIcons.USER, - // }, - // // { - // // label: 'Create', - // // path: AUTH_ROUTES.NEW_STAFF, - // // icon: PrimeIcons.USER_PLUS, - // // }, - // ], + items: [ + { + label: 'Manage', + path: AUTH_ROUTES.STAFFS_MANAGE, + icon: PrimeIcons.USER, + }, + { + label: 'Assign', + path: AUTH_ROUTES.NEW_STAFF, + icon: PrimeIcons.USER_PLUS, + }, + ], }, { type: 'group', @@ -130,17 +144,12 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { ], path: AUTH_ROUTES.PHYSICAL, }, - { - label: 'Import', - path: AUTH_ROUTES.IMPORT, - icon: PrimeIcons.UPLOAD, - }, { type: 'group', - label: 'Borrowed docs', + label: 'Requests', }, { - label: 'Requests', + label: 'Borrows', path: AUTH_ROUTES.REQUESTS, icon: PrimeIcons.BELL, }, @@ -149,6 +158,23 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.RETURNS, icon: PrimeIcons.REPLY, }, + { + label: 'Imports', + path: AUTH_ROUTES.IMPORT, + icon: PrimeIcons.UPLOAD, + items: [ + { + label: 'Employee Requests', + path: AUTH_ROUTES.IMPORT_MANAGE, + icon: PrimeIcons.USERS, + }, + { + label: 'Manual', + path: AUTH_ROUTES.NEW_IMPORT, + icon: PrimeIcons.PLUS, + }, + ], + }, ], // Employee sidebar employee: [ @@ -171,14 +197,32 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.DRIVE, icon: PrimeIcons.CLOUD, }, + { type: 'group', - label: 'Borrowed docs', + label: 'Requests', }, { - label: 'Requests', + label: 'Borrow', path: AUTH_ROUTES.REQUESTS, icon: PrimeIcons.BELL, }, + { + label: 'Import', + path: AUTH_ROUTES.IMPORT, + icon: PrimeIcons.UPLOAD, + items: [ + { + label: 'Manage', + path: AUTH_ROUTES.IMPORT_MANAGE, + icon: PrimeIcons.UPLOAD, + }, + { + label: 'Create', + path: AUTH_ROUTES.NEW_IMPORT, + icon: PrimeIcons.PLUS, + }, + ], + }, ], }; From decf4e7c0cdc7a620801718c642b4e9c4393ffbe Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 28 Jun 2023 22:55:09 +0700 Subject: [PATCH 04/18] feat: change API --- src/App.tsx | 10 + src/components/Skeleton/SekeltonPage.tsx | 2 +- src/constants/routes.ts | 9 +- .../ImportDocumentContainer.tsx | 6 +- .../SignInFormContainer.tsx | 28 +- src/hooks/useEmptyContainers.tsx | 2 +- src/hooks/useLockers.tsx | 38 ++- src/hooks/usePagination.tsx | 2 +- src/hooks/useRooms.tsx | 50 ++- src/index.css | 23 +- src/pages/CallbackPage/CallbackPage.tsx | 77 +++++ src/pages/Guards/AuthGuard.tsx | 9 +- src/pages/Guards/RoleMapper.tsx | 61 +++- .../AdminDashboardPage/AdminDashboardPage.tsx | 56 +++- .../AdminDepartmentCreatePage.tsx | 61 ++++ .../AdminDepartmentDetailPage.tsx | 108 +++++++ .../AdminDepartmentPage.tsx | 65 ++++ .../AdminDocumentDetailPage.tsx | 57 ++-- .../AdminDocumentPage/AdminDocumentPage.tsx | 5 - .../AdminFolderCreatePage.tsx | 9 +- .../AdminFolderDetailPage.tsx | 41 ++- .../AdminLockerCreatePage.tsx | 9 +- .../AdminLockerDetailPage.tsx | 62 ++-- .../AdminRequestPage/AdminRequestPage.tsx | 8 +- .../AdminRoomDetailPage.tsx | 66 ++-- .../AdminStaffCreatePage.tsx | 114 ++++++- .../admin/AdminStaffPage/AdminStaffPage.tsx | 41 ++- src/pages/admin/index.ts | 5 + .../emp/EmpDashboardPage/EmpDashboardPage.tsx | 9 +- .../EmpDocumentDetailPage.tsx | 37 ++- .../emp/EmpDocumentPage/EmpDocumentPage.tsx | 4 +- .../EmpImportCreatePage.tsx | 228 +++++++++++++ .../EmpImportDetailPage.tsx | 65 ++++ src/pages/emp/EmpImportPage/EmpImportPage.tsx | 70 ++++ .../EmpRequestDetailPage.tsx | 156 +++++++-- .../emp/EmpRequestPage/EmpRequestPage.tsx | 2 +- src/pages/emp/index.ts | 3 + .../StaffDashboardPage/StaffDashboardPage.tsx | 18 +- .../StaffDocumentDetailPage.tsx | 77 +++-- .../StaffDocumentPage/StaffDocumentPage.tsx | 2 +- .../StaffFolderDetailPage.tsx | 4 +- .../staff/StaffFolderPage/StaffFolderPage.tsx | 2 +- .../StaffImportCreatePage.tsx | 14 + .../StaffImportDetailPage.tsx | 299 ++++++++++++++++++ .../staff/StaffImportPage/StaffImportPage.tsx | 68 +++- .../StaffLockerDetailPage.tsx | 4 +- .../staff/StaffLockerPage/StaffLockerPage.tsx | 2 +- .../StaffRequestDetailPage.tsx | 96 +++++- .../StaffRequestPage/StaffRequestPage.tsx | 6 +- .../staff/StaffReturnPage/StaffReturnPage.tsx | 48 +-- src/pages/staff/index.ts | 6 +- src/types/item.ts | 21 +- src/types/response.ts | 15 +- src/types/roles.ts | 88 ++++-- 54 files changed, 2054 insertions(+), 314 deletions(-) create mode 100644 src/pages/CallbackPage/CallbackPage.tsx create mode 100644 src/pages/admin/AdminDepartmentCreatePage/AdminDepartmentCreatePage.tsx create mode 100644 src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx create mode 100644 src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx create mode 100644 src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx create mode 100644 src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx create mode 100644 src/pages/emp/EmpImportPage/EmpImportPage.tsx create mode 100644 src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx create mode 100644 src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 75d7d1d..f6cd90d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import AuthGuard from '@/pages/Guards/AuthGuard'; import SignInPage from '@/pages/SignInPage/SignInPage'; import Navbar from './components/Navbar/Navbar.component'; import RoleGuard from './pages/Guards/RoleGuard'; +import CallbackPage from './pages/CallbackPage/CallbackPage'; function App() { return ( @@ -17,6 +18,15 @@ function App() { /> } /> + } + unAuthComponent={} + /> + } + /> { return (
-

+

diff --git a/src/constants/routes.ts b/src/constants/routes.ts index faea0ab..9a1c79c 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -13,7 +13,7 @@ export const AUTH_ROUTES = { REQUEST: '/requests/:requestId', NEW_REQUEST: '/requests/create', RETURNS: '/returns', - IMPORT: '/import', + NEW_IMPORT: '/import/create', DRIVE: '/digital', EMPLOYEES: '/employees', EMPLOYEES_MANAGE: '/employees/manage', @@ -28,12 +28,17 @@ export const AUTH_ROUTES = { STAFF: '/staffs/manage/:staffId', NEW_STAFF: '/staffs/create', DEPARTMENTS: '/departments', - DEPARTMENT: '/departments/:departmentId', + DEPARTMENTS_MANAGE: '/departments/manage', + DEPARTMENT: '/departments/manage/:departmentId', NEW_DEPARTMENT: '/departments/create', + IMPORT_ID: '/import/manage/:importId', + IMPORT_MANAGE: '/import/manage', + IMPORT: '/import', }; export const UNAUTH_ROUTES = { AUTH: '/auth', + CALLBACK: '/callback', }; export type AUTH_ROUTES_KEY = keyof typeof AUTH_ROUTES; diff --git a/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx b/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx index e03dee4..0bd13bc 100644 --- a/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx +++ b/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx @@ -62,7 +62,7 @@ const ImportDocumentContainer = () => { }); const { availableFolders, availableLockers, containerRefetch } = useEmptyContainers({ - roomId: user?.department.roomId || '', + roomId: user?.roomId || '', }); const { documentTypes, typesRefetch } = useDocumentTypes(); @@ -127,7 +127,7 @@ const ImportDocumentContainer = () => { if (!user) { setFieldError('id', 'User not found'); setFieldValue('name', '', false); - } else setFieldValue('name', `${user.data.firstName} ${user.data.lastName}`, false); + } else setFieldValue('name', `${user.data.firstName} ${user.data.lastName}`, true); }; useEffect(() => { @@ -294,7 +294,7 @@ const ImportDocumentContainer = () => { label='Submit' type='submit' className='bg-primary mt-5 rounded-lg' - disabled={isSubmitting || isValid} + disabled={isSubmitting || !isValid} />
diff --git a/src/containers/SignInFormContainer/SignInFormContainer.tsx b/src/containers/SignInFormContainer/SignInFormContainer.tsx index 163e512..9dde7c6 100644 --- a/src/containers/SignInFormContainer/SignInFormContainer.tsx +++ b/src/containers/SignInFormContainer/SignInFormContainer.tsx @@ -5,7 +5,7 @@ import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component import { AuthContext } from '@/context/authContext'; import Spinner from '@/components/Spinner/Spinner.component'; import axiosClient from '@/utils/axiosClient'; -import { LoginResponse } from '@/types/response'; +import { GetRoomByIdResponse, LoginResponse } from '@/types/response'; import { AxiosError } from 'axios'; const SIGNIN_INITIALS = { @@ -56,13 +56,13 @@ const SignInForm = () => { const user = { ...data, role: data.role.toLowerCase(), - department: { - id: 'f2760dcd-6830-4541-9e99-d80bce9e6980', - name: 'Accounting', - roomId: 'f2760dcd-6830-4541-9e99-d80bce9e6980', - }, }; + if (user.role === 'staff') { + const room = (await axiosClient.get(`/staffs/${data.id}/rooms`)).data; + user.roomId = room.data?.id; // If staff is not assigned, it will be null + } + dispatch({ type: 'LOGIN', payload: user, @@ -71,11 +71,17 @@ const SignInForm = () => { } catch (error) { console.error(error); const axiosError = error as AxiosError; - const message = - (axiosError.response?.data as { message?: string }).message || 'Something went wrong'; - setErrors({ - error: message, - }); + if (axiosError.response?.status === 404) { + setErrors({ + error: 'You have not been assigned a room yet, please contact admin for more information', + }); + } else { + const message = + (axiosError.response?.data as { message?: string }).message || 'Something went wrong'; + setErrors({ + error: message, + }); + } } finally { setSubmitting(false); } diff --git a/src/hooks/useEmptyContainers.tsx b/src/hooks/useEmptyContainers.tsx index 522c300..0b04cc8 100644 --- a/src/hooks/useEmptyContainers.tsx +++ b/src/hooks/useEmptyContainers.tsx @@ -16,7 +16,7 @@ const useEmptyContainers = ({ roomId, page = 1, size = 20 }: IUserEmptyContainer async () => ( await axiosClient.post( - `/rooms/empty-containers/${roomId}?page=${page}&size=${size}` + `/rooms/${roomId}/empty-containers?page=${page}&size=${size}` ) ).data, { diff --git a/src/hooks/useLockers.tsx b/src/hooks/useLockers.tsx index 1dec9b2..802e55f 100644 --- a/src/hooks/useLockers.tsx +++ b/src/hooks/useLockers.tsx @@ -1,9 +1,9 @@ -import { DropdownOption } from '@/types/config'; +/* eslint-disable no-mixed-spaces-and-tabs */ import { GetLockersResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { useQuery } from 'react-query'; -const useLockers = () => { +const useLockers = (groupByRoom = false) => { const { data: lockersResult, refetch: lockersRefetch } = useQuery( ['lockers'], async () => (await axiosClient.get('/lockers')).data, @@ -12,11 +12,35 @@ const useLockers = () => { } ); - const lockers: DropdownOption[] = - lockersResult?.data.items.map((locker) => ({ - name: locker.name, - id: locker.id, - })) || []; + const lockers = lockersResult + ? groupByRoom + ? lockersResult.data.items.reduce( + (acc, locker) => { + const roomName = locker.room.name; + const room = acc.find((item) => item.room === roomName); + + if (!room) { + acc.push({ + room: roomName, + lockers: [{ name: locker.name, id: locker.id }], + }); + return acc; + } + + room.lockers.push({ name: locker.name, id: locker.id }); + + return acc; + }, + [] as { + room: string; + lockers: { name: string; id: string }[]; + }[] + ) + : lockersResult.data.items.map((locker) => ({ + name: locker.name, + id: locker.id, + })) + : []; return { lockers, diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index a93daea..7cd4ea5 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -49,7 +49,7 @@ const usePagination = ({ await axiosClient.get>(url, { params: { searchTerm: query, - roomId: user?.role === 'admin' ? undefined : user?.department.roomId, + roomId: user?.role === 'staff' ? user?.roomId : undefined, page: paginate.page + 1, // Primereact datatable page start at 0, our api start at 1 size: paginate.rows, sortBy: paginate?.sortField?.slice(0, 1).toUpperCase() + paginate?.sortField?.slice(1), diff --git a/src/hooks/useRooms.tsx b/src/hooks/useRooms.tsx index 9ac3973..18fb79b 100644 --- a/src/hooks/useRooms.tsx +++ b/src/hooks/useRooms.tsx @@ -1,22 +1,56 @@ -import { DropdownOption } from '@/types/config'; +/* eslint-disable no-mixed-spaces-and-tabs */ import { GetRoomsResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { useQuery } from 'react-query'; -const useRooms = () => { +const useRooms = (groupByDepartment = false, departmentId = '') => { const { data: roomsResult, refetch: roomsRefetch } = useQuery( ['rooms'], - async () => (await axiosClient.get('/rooms')).data, + async () => + ( + await axiosClient.get('/rooms', { + params: { + departmentId: departmentId || undefined, + }, + }) + ).data, { enabled: false, } ); - const rooms: DropdownOption[] = - roomsResult?.data.items.map((room) => ({ - name: room.name, - id: room.id, - })) || []; + const rooms = roomsResult + ? groupByDepartment + ? roomsResult.data.items.reduce( + (acc, room) => { + const departmentName = room.department.name; + const department = acc.find((item) => item.department === departmentName); + + if (!department) { + acc.push({ + department: departmentName, + rooms: [{ name: room.name, id: room.id }], + }); + return acc; + } + + department.rooms.push({ name: room.name, id: room.id }); + + return acc; + }, + [] as { + department: string; + rooms: { name: string; id: string }[]; + }[] + ) + : roomsResult.data.items.map((room) => ({ + ...room, + name: room.name, + id: room.id, + })) + : []; + + console.log(rooms); return { rooms, diff --git a/src/index.css b/src/index.css index 4db2856..459ff59 100644 --- a/src/index.css +++ b/src/index.css @@ -14,7 +14,8 @@ .title { @apply text-2xl font-bold; } - .input, .p-inputnumber-input { + .input, + .p-inputnumber-input { @apply bg-transparent !border-2 transition-shadow text-white disabled:bg-neutral-800 border-primary hover:!border-primary hover:!border-opacity-80 rounded-lg; } .error-input { @@ -60,6 +61,10 @@ body { box-shadow: none !important; } +.p-dropdown-filter-container > .p-inputtext { + color: initial; +} + .p-invalid { border-color: var(--action) !important; } @@ -70,21 +75,27 @@ body { /* width */ ::-webkit-scrollbar { - width: 0.5rem; + width: 0.5rem; height: 0.5rem; } /* Track */ ::-webkit-scrollbar-track { - background: #f1f1f1; + background: #f1f1f1; } /* Handle */ ::-webkit-scrollbar-thumb { - background: var(--primary); + background: var(--primary); } /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { - background: var(--secondary); -} \ No newline at end of file + background: var(--secondary); +} + +.p-dialog .p-dialog-footer, +.p-dialog .p-dialog-header, +.p-dialog .p-dialog-content { + background: var(--layer-700); +} diff --git a/src/pages/CallbackPage/CallbackPage.tsx b/src/pages/CallbackPage/CallbackPage.tsx new file mode 100644 index 0000000..66d4f21 --- /dev/null +++ b/src/pages/CallbackPage/CallbackPage.tsx @@ -0,0 +1,77 @@ +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { Formik } from 'formik'; +import { Button } from 'primereact/button'; + +const initialValues = { + password: '', + confirmPassword: '', +}; + +type FormValues = typeof initialValues; + +const CallbackPage = () => { + const query = new URLSearchParams(window.location.search); + + const validate = (values: FormValues) => { + const errors: Partial = {}; + if (!values.password) { + errors.password = 'Required'; + } else if (values.password.length < 8) { + errors.password = 'Password must be at least 8 characters'; + } + if (!values.confirmPassword) { + errors.confirmPassword = 'Required'; + } else if (values.confirmPassword !== values.password) { + errors.confirmPassword = 'Password does not match'; + } + return errors; + }; + + const onSubmit = async (values: FormValues) => { + try { + // const response = await axiosClient.post('/auth/reset-password', { + // password: values.password, + // confirmPassword: values.confirmPassword, + // token: query.get('token'), + // }); + // console.log(response); + } catch (error) { + console.log(error); + } + }; + + return ( + + {({ values, touched, errors, handleBlur, handleChange, handleSubmit }) => ( +
+

Reset Password

+ + +
+
+ ); +}; + +export default AdminDepartmentDetailPage; diff --git a/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx b/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx new file mode 100644 index 0000000..9f64c3d --- /dev/null +++ b/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx @@ -0,0 +1,65 @@ +import Table from '@/components/Table/Table.component'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { IDepartment } from '@/types/item'; +import usePagination from '@/hooks/usePagination'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import Status from '@/components/Status/Status.component'; +import { useRef } from 'react'; + +const AdminDepartmentPage = () => { + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['departments', query.current], + url: '/departments', + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DEPARTMENTS' }); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + placeholder='department a' + /> +
+
+ + + + + + } + /> + + +
+
+
+ ); +}; + +export default AdminDepartmentPage; diff --git a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx index 0e896bd..7b3471d 100644 --- a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx +++ b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx @@ -4,7 +4,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { BaseResponse, GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -25,17 +25,19 @@ const AdminDocumentDetailPage = () => { const { data, isLoading, error } = useQuery( ['documents', documentId], - async () => (await axiosClient.get(`/documents/${documentId}`)).data, - { - onSuccess: async (data) => { - const { id } = data?.data || { id: '' }; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, - } + async () => (await axiosClient.get(`/documents/${documentId}`)).data ); + useEffect(() => { + const renderQr = async () => { + const { id } = data?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }; + renderQr(); + }, [data]); + if (isLoading) return ; if ((error as AxiosError)?.response?.status === 404 || !data) @@ -43,18 +45,33 @@ const AdminDocumentDetailPage = () => { const { title, - folder: { - id: folderId, - name: folderName, - locker: { - id: lockerId, - name: lockerName, - room: { id: roomId, name: roomName }, + // folder: { + // id: folderId, + // name: folderName, + // locker: { id: lockerId, name: lockerName }, + // }, + } = data.data; + + const folder = data.data.folder || { + id: '', + name: '', + locker: { + id: '', + name: '', + room: { + id: '', + name: '', }, }, - } = data.data; + }; + + const { + id: folderId, + name: folderName, + locker: { id: lockerId, name: lockerName, room: { id: roomId, name: roomName } } = { room: {} }, + } = folder; - const initialValues = data.data; + const initialValues = { ...data.data, folder }; type FormValues = typeof initialValues; @@ -85,7 +102,7 @@ const AdminDocumentDetailPage = () => { return (
-

+

/ {roomName} diff --git a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx index af6b64b..b274871 100644 --- a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx +++ b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx @@ -1,9 +1,7 @@ import Table from '@/components/Table/Table.component'; -import { AUTH_ROUTES } from '@/constants/routes'; import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; import { InputText } from 'primereact/inputtext'; -import { Link } from 'react-router-dom'; import { IDocument } from '@/types/item'; import usePagination from '@/hooks/usePagination'; import useNavigateSelect from '@/hooks/useNavigateSelect'; @@ -38,9 +36,6 @@ const AdminDocumentPage = () => { /> -

diff --git a/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx b/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx index dbb88fc..ed712bf 100644 --- a/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx +++ b/src/pages/admin/AdminFolderCreatePage/AdminFolderCreatePage.tsx @@ -26,7 +26,7 @@ type FormValues = typeof initialValues; const AdminFolderCreatePage = () => { const navigate = useNavigate(); - const { lockers, lockersRefetch } = useLockers(); + const { lockers, lockersRefetch } = useLockers(true); const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { try { @@ -123,6 +123,13 @@ const AdminFolderCreatePage = () => { error={touched.locker && !!errors.locker} small={touched.locker ? errors.locker : undefined} disabled={isSubmitting} + optionGroupLabel='room' + optionGroupChildren='lockers' + optionGroupTemplate={(option) => ( +
+ {option.room} +
+ )} /> { const queryClient = useQueryClient(); const [editMode, setEditMode] = useState(false); const [error, setError] = useState(''); + const navigate = useNavigate(); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); @@ -133,10 +134,21 @@ const AdminFolderDetailPage = () => { } }; + const onDelete = async () => { + try { + await axiosClient.delete(`/folders/${folderId}`); + queryClient.invalidateQueries('folders'); + navigate(AUTH_ROUTES.FOLDERS); + } catch (error) { + const axiosError = error as AxiosError; + setError(axiosError.response?.data.message || 'Something went wrong'); + } + }; + return (
-

+

/ {roomName} @@ -266,16 +278,23 @@ const AdminFolderDetailPage = () => { disabled={editMode || isSubmitting || !isValid} /> )} - -

+ +
+ +
+
{ const queryClient = useQueryClient(); const [editMode, setEditMode] = useState(false); const [error, setError] = useState(''); + const navigate = useNavigate(); const { data: room, @@ -62,7 +63,7 @@ const AdminRoomDetailPage = () => { if (isLoading) return ; if ((axiosError as AxiosError)?.response?.status === 404 || !room) - return ; + return ; const { name: roomName, capacity, numberOfLockers, description, isAvailable } = room.data; @@ -80,11 +81,12 @@ const AdminRoomDetailPage = () => { const onToggleAvailability = async () => { try { - if (isAvailable) { - await axiosClient.put(`/rooms/disable/${roomId}`); - } else { - await axiosClient.put(`/rooms/enable/${roomId}`); - } + await axiosClient.put(`/rooms/${roomId}`, { + name: roomName, + description, + capacity, + isAvailable: !isAvailable, + }); queryClient.invalidateQueries('rooms'); } catch (error) { const axiosError = error as AxiosError; @@ -124,10 +126,21 @@ const AdminRoomDetailPage = () => { } }; + const onDelete = async () => { + try { + await axiosClient.delete(`/rooms/${roomId}`); + queryClient.invalidateQueries('rooms'); + navigate(AUTH_ROUTES.ROOMS); + } catch (error) { + const axiosError = error as AxiosError; + setError(axiosError.response?.data.message || 'Something went wrong'); + } + }; + return (
-

+

/ {roomName}

@@ -150,7 +163,7 @@ const AdminRoomDetailPage = () => {
{' '}
- + {
+ +
{ } + body={(item: ILocker) => } />
diff --git a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx index 27220d0..b478fbc 100644 --- a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx +++ b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx @@ -1,5 +1,117 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import usePagination from '@/hooks/usePagination'; +import { IRoom, IStaff } from '@/types/item'; +import { BaseResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { Formik } from 'formik'; +import { Button } from 'primereact/button'; +import { useState } from 'react'; +import { useNavigate } from 'react-router'; + const AdminStaffCreatePage = () => { - return
; + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const params = new URLSearchParams(window.location.search); + + const initialValues = { + staffId: params.get('staffId') || '', + roomId: '', + }; + + type FormValues = typeof initialValues; + + const { data: staffs } = usePagination({ + key: ['staffs', 'unassigned'], + url: '/staffs', + }); + + const { data: rooms } = usePagination({ + key: ['rooms', 'unassigned'], + url: '/rooms', + }); + + const staffOptions = staffs?.data.items + // .filter((staff) => staff.room === null) + .map((staff) => ({ + label: `${staff.user.firstName} ${staff.user.lastName}`, + value: staff.id, + room: staff.room, + })); + + const roomOptions = rooms?.data.items + .filter((room) => room.staffId === null && room.isAvailable) + .map((room) => ({ + label: room.name, + value: room.id, + })); + + const onSubmit = async (values: FormValues) => { + try { + await axiosClient.post('/staffs', values); + navigate(AUTH_ROUTES.STAFFS_MANAGE); + } catch (error) { + const axiosError = error as AxiosError; + console.error(error); + setError(axiosError.response?.data.message || 'Something went wrong'); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (!value) { + error[key as keyof FormValues] = 'Required'; + } + }); + return error; + }; + + return ( + + {({ handleSubmit, handleBlur, handleChange, errors, touched, values }) => ( + + + ( +
+
{option.label}
+
{option.room?.name || 'Unassigned'}
+
+ )} + /> + + {error &&
{error}
} + +
- +
+ navigate(`${AUTH_ROUTES.NEW_STAFF}?staffId=${(e.value as { id: string }).id}`) + } + {...getPaginatedTableProps()} + > {/* { /> */} - - + <>{staff.room?.department?.name || 'N/A'}} + /> + <>{staff.room?.name || 'N/A'}} + /> { ); }; -export default AdminEmployeePage; +export default AdminStaffPage; diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 0a1af1d..4c4af9b 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -15,3 +15,8 @@ export { default as AdminLockerCreatePage } from './AdminLockerCreatePage/AdminL export { default as AdminRoomCreatePage } from './AdminRoomCreatePage/AdminRoomCreatePage'; export { default as AdminFolderCreatePage } from './AdminFolderCreatePage/AdminFolderCreatePage'; export { default as AdminRoomPage } from './AdminRoomPage/AdminRoomPage'; +export { default as AdminRoomDetailPage } from './AdminRoomDetailPage/AdminRoomDetailPage'; +export { default as AdminStaffCreatePage } from './AdminStaffCreatePage/AdminStaffCreatePage'; +export { default as AdminDepartmentPage } from './AdminDepartmentPage/AdminDepartmentPage'; +export { default as AdminDepartmentCreatePage } from './AdminDepartmentCreatePage/AdminDepartmentCreatePage'; +export { default as AdminDepartmentDetailPage } from './AdminDepartmentDetailPage/AdminDepartmentDetailPage'; \ No newline at end of file diff --git a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx index 4f3020b..7069b51 100644 --- a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx +++ b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx @@ -1,7 +1,5 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { Link } from 'react-router-dom'; -import { useContext } from 'react'; -import { AuthContext } from '@/context/authContext'; import axiosClient from '@/utils/axiosClient'; import { GetDocumentByIdResponse, GetRequestsResponse } from '@/types/response'; import { REFETCH_CONFIG } from '@/constants/config'; @@ -12,17 +10,12 @@ import Status from '@/components/Status/Status.component'; import { dateFormatter } from '@/utils/formatter'; const EmpDashboardPage = () => { - const { user } = useContext(AuthContext); - - const roomId = user?.department.roomId; - const { data: requests, isLoading: isRequestsLoading } = useQuery( ['requests', 'recent'], async () => ( - await axiosClient.get('/borrows/employees', { + await axiosClient.get('/documents/borrows', { params: { - roomId, sortOrder: 'desc', size: 4, page: 1, diff --git a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx index a824c11..28da291 100644 --- a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx +++ b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx @@ -4,7 +4,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -13,25 +13,36 @@ import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.com import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; import { SkeletonPage } from '@/components/Skeleton'; import Status from '@/components/Status/Status.component'; +import { AxiosError } from 'axios'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; const EmpDocumentDetailPage = () => { const { documentId = '' } = useParams<{ documentId: string }>(); const [qr, setQr] = useState(''); - const { data: doc, isLoading } = useQuery( + const { + data: doc, + isLoading, + error: axiosError, + } = useQuery( ['documents', documentId], - async () => (await axiosClient.get(`/documents/${documentId}`)).data, - { - onSuccess: async (data) => { - const { id } = data?.data || { id: '' }; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, - } + async () => (await axiosClient.get(`/documents/${documentId}`)).data ); - if (isLoading || !doc) return ; + useEffect(() => { + const renderQr = async () => { + const { id } = doc?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }; + renderQr(); + }, [doc]); + + if (isLoading) return ; + + if ((axiosError as AxiosError)?.response?.status === 404 || !doc) + return ; const { title, @@ -48,7 +59,7 @@ const EmpDocumentDetailPage = () => { return (
-

+

/ {lockerName} diff --git a/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx b/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx index 15663db..be1b644 100644 --- a/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx +++ b/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx @@ -15,7 +15,7 @@ const EmpDocumentPage = () => { const { getPaginatedTableProps, refetch } = usePagination({ key: ['documents', query.current], - url: '/documents', + url: '/documents/employees', query: query.current, }); @@ -38,7 +38,7 @@ const EmpDocumentPage = () => { />

diff --git a/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx new file mode 100644 index 0000000..e09f127 --- /dev/null +++ b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx @@ -0,0 +1,228 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import axiosClient from '@/utils/axiosClient'; +import { Formik, FormikHelpers } from 'formik'; +import { Button } from 'primereact/button'; +import { useNavigate } from 'react-router'; +import { useContext, useEffect } from 'react'; +import { AuthContext } from '@/context/authContext'; +import { AxiosError } from 'axios'; +import useDocumentTypes from '@/hooks/useDocumentTypes'; +import useRooms from '@/hooks/useRooms'; +import { BaseResponse } from '@/types/response'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import { InputSwitch } from 'primereact/inputswitch'; + +const RequiredValues = { + title: '', + documentType: '', + description: '', + roomId: '', + importReason: '', + isPrivate: true, +}; + +const NOT_REQUIRED = ['description']; + +type FormValues = typeof RequiredValues; + +const EmpImportPage = () => { + const navigate = useNavigate(); + const { user } = useContext(AuthContext); + + const { documentTypes, typesRefetch } = useDocumentTypes(); + + const departmentId = user?.department.id || ''; + + const { rooms, roomsRefetch } = useRooms(false, departmentId); + + const onSubmit = async ( + values: FormValues, + { setSubmitting, setFieldError }: FormikHelpers + ) => { + try { + const result = await axiosClient.post('/documents/import-requests', values); + navigate(`${AUTH_ROUTES.IMPORT_MANAGE}/${result.data.data.id}`); + } catch (error) { + console.log(error); + const axiosError = error as AxiosError; + const msg = axiosError.response?.data.message || 'Incorrect format'; + const status = axiosError.response?.status; + + if (status === 409) { + setFieldError('title', msg); + } + if (status === 400) { + setFieldError('id', msg); + } + } finally { + setSubmitting(false); + } + }; + + const onValidate = (values: FormValues) => { + const errors: { [key: string]: string } = {}; + Object.entries(values).forEach(([key, value]) => { + if (!value && NOT_REQUIRED.indexOf(key) === -1) { + errors[key] = 'This field is required'; + } + }); + return errors; + }; + + useEffect(() => { + const getConfigs = async () => { + await typesRefetch(); + await roomsRefetch(); + }; + getConfigs(); + }, [typesRefetch, roomsRefetch]); + + return ( + + {({ + values, + errors, + touched, + handleBlur, + handleChange, + handleSubmit, + setFieldValue, + setFieldTouched, + isSubmitting, + isValid, + }) => { + return ( + <> +
+
+ + + ( +
+ {value === '' ? ( + 'No item selected' + ) : options?.some( + (option) => option.id.toUpperCase() === value.toUpperCase() + ) ? ( + <> + {value.toUpperCase()} selected + + ) : ( + <> + {value.toUpperCase()} will be added + + )} +
+ )} + /> +
+ + setFieldValue('isPrivate', e.value)} + /> +
+ + +
+ + { + setFieldValue('roomId', ''); + setFieldTouched('roomId', false); + handleChange(e); + }} + onBlur={handleBlur} + value={values.roomId} + error={touched.roomId && !!errors.roomId} + small={touched.roomId ? errors.roomId : undefined} + disabled={isSubmitting} + itemTemplate={(option) => ( +
+
+ {option.name} - Free: {option.capacity - option.numberOfLockers}/ + {option.capacity} +
+
{option.description}
+
+ )} + /> +
+ + + ); + }} +
+ ); +}; + +export default EmpImportPage; diff --git a/src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx b/src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx new file mode 100644 index 0000000..4342c42 --- /dev/null +++ b/src/pages/emp/EmpImportDetailPage/EmpImportDetailPage.tsx @@ -0,0 +1,65 @@ +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import { SkeletonPage } from '@/components/Skeleton'; +import Status from '@/components/Status/Status.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { GetImportByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; +import { useParams } from 'react-router'; + +const ImportDetailPagePage = () => { + const { importId } = useParams<{ importId: string }>(); + + const { + data: importRequest, + isLoading, + error: axiosError, + } = useQuery( + ['import', importId], + async () => + (await axiosClient.get(`/documents/import-requests/${importId}`)).data, + { + enabled: !!importId, + } + ); + + if (isLoading) return ; + + if ((axiosError as AxiosError)?.response?.status === 404 || !importRequest) + return ; + + const { + document: { title, documentType, description, isPrivate }, + importReason, + staffReason, + status: importStatus, + room: { name }, + } = importRequest.data; + + return ( +
+ + } + /> + + + + + + + + + + +
+ ); +}; + +export default ImportDetailPagePage; diff --git a/src/pages/emp/EmpImportPage/EmpImportPage.tsx b/src/pages/emp/EmpImportPage/EmpImportPage.tsx new file mode 100644 index 0000000..f0b3281 --- /dev/null +++ b/src/pages/emp/EmpImportPage/EmpImportPage.tsx @@ -0,0 +1,70 @@ +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import usePagination from '@/hooks/usePagination'; +import { IDocument, IImportRequest } from '@/types/item'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { Link } from 'react-router-dom'; +import { useRef } from 'react'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; + +const EmpImportPage = () => { + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['imports', query.current], + url: '/documents/import-requests', + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'IMPORT_MANAGE' }); + + return ( +
+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> + + +
+
+
+ + + + + } + /> + + +
+
+
+ ); +}; + +export default EmpImportPage; diff --git a/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx b/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx index 046da1a..54dda30 100644 --- a/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx +++ b/src/pages/emp/EmpRequestDetailPage/EmpRequestDetailPage.tsx @@ -5,13 +5,14 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { GetDocumentByIdResponse, GetRequestByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery } from 'react-query'; import { Link, Navigate, useParams } from 'react-router-dom'; import QRCode from 'qrcode'; import { SkeletonPage } from '@/components/Skeleton'; import CustomCalendar from '@/components/Calendar/Calendar.component'; import { REQUEST_STATUS } from '@/constants/status'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; const NO_ACTIONS = [ REQUEST_STATUS.Cancelled.status, @@ -22,22 +23,49 @@ const NO_ACTIONS = [ REQUEST_STATUS.NotProcessable.status, ]; +type Values = { + borrowReason: string; + borrowTime?: string | null | Date; + dueTime?: string | null | Date; +}; + const EmpRequestDetailPage = () => { const { requestId } = useParams<{ requestId: string }>(); const [qr, setQr] = useState(''); const { data, refetch: refetchRequest } = useQuery( ['requests', requestId], - async () => (await axiosClient.get(`/borrows/${requestId}`)).data, + async () => + (await axiosClient.get(`/documents/borrows/${requestId}`)).data, { enabled: !!requestId, - onSuccess: async (data) => { - const id = data?.data?.documentId || ''; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, } ); + const [editMode, setEditMode] = useState(false); + const [values, setValues] = useState({ + borrowReason: '', + borrowTime: '', + dueTime: '', + }); + + useEffect(() => { + const renderQr = async () => { + const { documentId } = data?.data || { documentId: '' }; + if (!documentId) return; + const qrCode = await QRCode.toDataURL(documentId); + setQr(qrCode); + }; + + const updateValues = () => { + const { borrowReason, borrowTime, dueTime } = data?.data || { + borrowReason: 'This is a reason', + borrowTime: new Date(), + dueTime: new Date(new Date().setDate(new Date().getDate() + 7)), + }; + setValues({ borrowReason, borrowTime, dueTime }); + }; + updateValues(); + renderQr(); + }, [data]); const { documentId } = data ? data.data : { documentId: '' }; @@ -51,28 +79,69 @@ const EmpRequestDetailPage = () => { if (!requestId) return ; - if (!data || !document || isLoading) return ; + if (isLoading) return ; + // if (!document || !data) + // return ( + // + // ); + + // const { + // title, + // documentType, + // // folder: { + // // name: folder, + // // locker: { name: locker }, + // // }, + // } = document.data; + + const folder = '', + locker = '', + title = '', + documentType = ''; - const { - title, - documentType, - folder: { - name: folder, - locker: { name: locker }, - }, - } = document.data; + const status = 'Pending'; - const { status, reason, borrowTime, dueTime } = data.data; + // const { status, borrowReason, borrowTime, dueTime } = data.data; const onCancel = async () => { try { - await axiosClient.post(`/borrows/cancel/${requestId}`); + await axiosClient.post(`/documents/borrows/cancel/${requestId}`); + await refetchRequest(); + } catch (error) { + console.log(error); + } + }; + + const onUpdate = async () => { + if (!values.borrowReason || !values.borrowTime || !values.dueTime) return; + if (JSON.stringify(values) === JSON.stringify(data?.data)) return; + try { + await axiosClient.put(`/documents/borrows/${requestId}`, { + reason: values.borrowReason, + borrowFrom: values.borrowTime, + borrowTo: values.dueTime, + }); await refetchRequest(); + setEditMode(false); } catch (error) { console.log(error); } }; + const onEditCancel = () => { + setEditMode(false); + setValues({ + borrowReason: data?.data.borrowReason || 'This is a reason', + borrowTime: data?.data.borrowTime || new Date(), + dueTime: data?.data.dueTime || new Date(new Date().setDate(new Date().getDate() + 7)), + }); + }; + return (
@@ -86,21 +155,32 @@ const EmpRequestDetailPage = () => { - + setValues((prev) => ({ ...prev, borrowReason: e.target.value }))} + error={values.borrowReason === ''} + small={values.borrowReason === '' ? 'Please enter a reason' : ''} + />
- { + if (e.value === null) { + setValues((prev) => ({ ...prev, borrowTime: null, dueTime: null })); + return; + } + const [borrowTime, dueTime] = e.value as Date[]; + setValues((prev) => ({ ...prev, borrowTime, dueTime })); + }} + error={!values.borrowTime || !values.dueTime} + small={!values.borrowTime || !values.dueTime ? 'Please select a date' : ''} />
@@ -112,12 +192,26 @@ const EmpRequestDetailPage = () => { ) : (
)} + {status === REQUEST_STATUS.Pending.status && ( +
diff --git a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx index 6679001..393c2c9 100644 --- a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx +++ b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx @@ -4,7 +4,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { BaseResponse, GetDocumentByIdResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -25,17 +25,19 @@ const StaffDocumentDetailPage = () => { const { data, isLoading, error } = useQuery( ['documents', documentId], - async () => (await axiosClient.get(`/documents/${documentId}`)).data, - { - onSuccess: async (data) => { - const { id } = data?.data || { id: '' }; - if (!id) return; - const qrCode = await QRCode.toDataURL(id); - setQr(qrCode); - }, - } + async () => (await axiosClient.get(`/documents/${documentId}`)).data ); + useEffect(() => { + const renderQr = async () => { + const { id } = data?.data || { id: '' }; + if (!id) return; + const qrCode = await QRCode.toDataURL(id); + setQr(qrCode); + }; + renderQr(); + }, [data]); + if (isLoading) return ; if ((error as AxiosError)?.response?.status === 404 || !data) @@ -43,14 +45,31 @@ const StaffDocumentDetailPage = () => { const { title, - folder: { - id: folderId, - name: folderName, - locker: { id: lockerId, name: lockerName }, - }, + // folder: { + // id: folderId, + // name: folderName, + // locker: { id: lockerId, name: lockerName }, + // }, } = data.data; - const initialValues = data.data; + console.log(data.data.folder); + + const folder = data.data.folder || { + id: '', + name: '', + locker: { + id: '', + name: '', + }, + }; + + const { + id: folderId, + name: folderName, + locker: { id: lockerId, name: lockerName } = {}, + } = folder; + + const initialValues = { ...data.data, folder }; type FormValues = typeof initialValues; @@ -81,15 +100,23 @@ const StaffDocumentDetailPage = () => { return (
-

- / - - {lockerName} - - / - - {folderName} - +

+ {lockerId && ( + <> + / + + {lockerName} + + + )} + {folderId && ( + <> + / + + {folderName} + + + )} / {title}

diff --git a/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx b/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx index 9f11b2a..ed960c5 100644 --- a/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx +++ b/src/pages/staff/StaffDocumentPage/StaffDocumentPage.tsx @@ -38,7 +38,7 @@ const StaffDocumentPage = () => { />
diff --git a/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx b/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx index eebc71a..b5030a6 100644 --- a/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx +++ b/src/pages/staff/StaffFolderDetailPage/StaffFolderDetailPage.tsx @@ -31,7 +31,7 @@ const StaffFolderDetailPage = () => { const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); - const roomId = user?.department.roomId; + const roomId = user?.roomId; const { data: folder, @@ -138,7 +138,7 @@ const StaffFolderDetailPage = () => { return (
-

+

/ {lockerName} diff --git a/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx b/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx index 175e26f..d146efd 100644 --- a/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx +++ b/src/pages/staff/StaffFolderPage/StaffFolderPage.tsx @@ -38,7 +38,7 @@ const StaffFolderPage = () => { />

diff --git a/src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx b/src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx new file mode 100644 index 0000000..8072f20 --- /dev/null +++ b/src/pages/staff/StaffImportCreatePage/StaffImportCreatePage.tsx @@ -0,0 +1,14 @@ +import ImportDocumentContainer from '@/containers/ImportDocumentContainer/ImportDocumentContainer'; + +const StaffImportCreatePage = () => { + return ( +
+
+

Importing documents

+
+ +
+ ); +}; + +export default StaffImportCreatePage; diff --git a/src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx b/src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx new file mode 100644 index 0000000..d3a793d --- /dev/null +++ b/src/pages/staff/StaffImportDetailPage/StaffImportDetailPage.tsx @@ -0,0 +1,299 @@ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; +import Overlay from '@/components/Overlay/Overlay.component'; +import Progress from '@/components/Progress/Progress.component'; +import { SkeletonPage } from '@/components/Skeleton'; +import Status from '@/components/Status/Status.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import { AuthContext } from '@/context/authContext'; +import useEmptyContainers from '@/hooks/useEmptyContainers'; +import { GetImportByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { Button } from 'primereact/button'; +import { useState, useContext, useEffect } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; +import { useNavigate, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; + +const StaffImportDetailPage = () => { + const { importId } = useParams<{ importId: string }>(); + const [showModal, setShowModal] = useState(''); + const [reason, setReason] = useState(''); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const { user } = useContext(AuthContext); + const [selected, setSelected] = useState({ + folder: '', + locker: '', + }); + + const { + data: importRequest, + isLoading, + error, + } = useQuery( + ['imports', importId], + async () => + (await axiosClient.get(`/documents/import-requests/${importId}`)).data, + { + enabled: !!importId, + } + ); + + const { availableFolders, availableLockers, containerRefetch } = useEmptyContainers({ + roomId: user?.roomId || '', + }); + + useEffect(() => { + containerRefetch(); + }, [containerRefetch]); + + if (isLoading) return ; + if (error || !importRequest) + return ; + + const { + document: { title, documentType, isPrivate, description, id: documentId, folder }, + room: { name: roomName }, + importReason, + staffReason, + status: importStatus, + } = importRequest.data; + + const onApprove = async () => { + if (!reason) return; + try { + await axiosClient.put(`/documents/import-requests/${importId}`, { + decision: 'Approve', + staffReason: reason, + }); + queryClient.invalidateQueries('imports'); + setShowModal(''); + } catch (error) { + console.log(error); + } + }; + + const onReject = async () => { + if (!reason) return; + try { + await axiosClient.put(`/documents/import-requests/${importId}`, { + decision: 'Reject', + staffReason: reason, + }); + setShowModal(''); + queryClient.invalidateQueries('imports'); + } catch (error) { + console.log(error); + } + }; + + const onAssign = async () => { + if (!selected.folder || !selected.locker) return; + try { + await axiosClient.put(`/documents/import-requests/assign/${importId}`, { + folderId: selected.folder, + }); + setShowModal(''); + queryClient.invalidateQueries('imports'); + } catch (error) { + console.log(error); + } + }; + + const onCheckIn = async () => { + try { + await axiosClient.put(`/documents/import-requests/checkin/${documentId}`); + navigate(`${AUTH_ROUTES.DOCUMENTS}/${documentId}`); + } catch (error) { + console.log(error); + } + }; + + return ( +
+ + } + /> + + + + {folder && ( +
+ + +
+ )} +
+
+ + {importStatus === 'Pending' && ( +
+
+ )} +
+ {importStatus === 'Approved' ? ( +
+
+ + + + + + +
+ {showModal && ( + setShowModal('')} className='flex items-center justify-center'> +
e.stopPropagation()}> +
+
Confirmation
+ setShowModal('')} + /> +
+
+ {showModal === 'approve' + ? 'Are you sure you want to approve this request?' + : showModal === 'reject' + ? 'Are you sure you want to reject this request?' + : 'Choose the folder to assign this document to'} +
+ {showModal === 'assign' ? ( +
+ ( +
+
+ {option.name} - Free: {option.free}/{option.max} +
+
{option.description}
+
+ )} + onChange={(e) => { + setSelected((prev) => ({ ...prev, locker: e.value })); + }} + value={selected.locker} + /> + ( +
+ {option.name} - Free: {option.free}/{option.max} +
+ )} + onChange={(e) => { + setSelected((prev) => ({ ...prev, folder: e.value })); + }} + value={selected.folder} + /> + {selected.folder && availableFolders && ( + value.id === selected.folder + )?.free || 0 + } + max={ + availableFolders[selected.locker].find( + (value) => value.id === selected.folder + )?.max || 0 + } + /> + )} +
+ ) : ( + setReason(e.target.value)} + placeholder='Enter your reason here' + /> + )} +
+
+
+
+ )} +
+ ); +}; + +export default StaffImportDetailPage; diff --git a/src/pages/staff/StaffImportPage/StaffImportPage.tsx b/src/pages/staff/StaffImportPage/StaffImportPage.tsx index e182f8f..f29586f 100644 --- a/src/pages/staff/StaffImportPage/StaffImportPage.tsx +++ b/src/pages/staff/StaffImportPage/StaffImportPage.tsx @@ -1,12 +1,72 @@ -import ImportDocumentContainer from '@/containers/ImportDocumentContainer/ImportDocumentContainer'; +import Status from '@/components/Status/Status.component'; +import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; +import usePagination from '@/hooks/usePagination'; +import { IImportRequest } from '@/types/item'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { Link } from 'react-router-dom'; +import { useContext, useRef } from 'react'; +import useNavigateSelect from '@/hooks/useNavigateSelect'; +import { AuthContext } from '@/context/authContext'; const StaffImportPage = () => { + const { user } = useContext(AuthContext); + const query = useRef(''); + + const roomId = user?.roomId || ''; + + const { getPaginatedTableProps, refetch } = usePagination({ + key: ['imports', query.current, roomId], + url: `/documents/import-requests?roomId=${roomId}`, + query: query.current, + }); + + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'IMPORT_MANAGE' }); + return (
-
-

Importing documents

+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> + + +
+
+ + + + + + } + /> + + +
-
); }; diff --git a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx index 70af808..c51fff0 100644 --- a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx +++ b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx @@ -30,7 +30,7 @@ const StaffLockerDetailPage = () => { const [editMode, setEditMode] = useState(false); const [error, setError] = useState(''); - const roomId = user?.department.roomId || ''; + const roomId = user?.roomId || ''; const { data: locker, @@ -136,7 +136,7 @@ const StaffLockerDetailPage = () => { return (
-

+

/ {lockerName}

diff --git a/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx b/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx index b8b8653..0d652f9 100644 --- a/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx +++ b/src/pages/staff/StaffLockerPage/StaffLockerPage.tsx @@ -38,7 +38,7 @@ const StaffLockerPage = () => { />
diff --git a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx index dc774be..d01b9c3 100644 --- a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx +++ b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx @@ -1,6 +1,8 @@ /* eslint-disable no-mixed-spaces-and-tabs */ +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import Overlay from '@/components/Overlay/Overlay.component'; import { SkeletonPage } from '@/components/Skeleton'; import { AUTH_ROUTES } from '@/constants/routes'; import { REQUEST_STATUS } from '@/constants/status'; @@ -12,6 +14,8 @@ import { } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { AxiosError } from 'axios'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; import { Button } from 'primereact/button'; import { useState } from 'react'; import { useQuery } from 'react-query'; @@ -29,9 +33,12 @@ const NO_ACTIONS = [ const StaffRequestDetailPage = () => { const { requestId } = useParams<{ requestId: string }>(); const [error, setError] = useState(''); + const [showModal, setShowModal] = useState(''); + const [reason, setReason] = useState(''); const { data, refetch } = useQuery( ['requests', requestId], - async () => (await axiosClient.get(`/borrows/${requestId}`)).data, + async () => + (await axiosClient.get(`/documents/borrows/${requestId}`)).data, { enabled: !!requestId, } @@ -62,19 +69,25 @@ const StaffRequestDetailPage = () => { const { title, documentType, - folder: { - name: folder, - locker: { name: locker }, - }, + // folder: { + // name: folder, + // locker: { name: locker }, + // }, } = document.data; + const folder = '', + locker = ''; + const { id: employeeId, lastName, firstName } = employee.data; - const { borrowTime, dueTime, reason, status } = data.data; + const { borrowTime, dueTime, borrowReason, status } = data.data; const onApprove = async () => { try { - await axiosClient.post(`/borrows/approve/${requestId}`); + await axiosClient.put(`/documents/borrows/staffs/${requestId}`, { + staffReason: reason, + decision: 'approve', + }); await refetch(); } catch (error) { const axiosError = error as AxiosError; @@ -84,9 +97,12 @@ const StaffRequestDetailPage = () => { } }; - const onDeny = async () => { + const onReject = async () => { try { - await axiosClient.post(`/borrows/reject/${requestId}`); + await axiosClient.put(`/documents/borrows/staffs/${requestId}`, { + staffReason: reason, + decision: 'reject', + }); await refetch(); } catch (error) { const axiosError = error as AxiosError; @@ -98,7 +114,7 @@ const StaffRequestDetailPage = () => { const onCheckout = async () => { try { - await axiosClient.post(`/borrows/checkout/${requestId}`); + await axiosClient.post(`/documents/borrows/checkout/${requestId}`); await refetch(); } catch (error) { const axiosError = error as AxiosError; @@ -131,7 +147,7 @@ const StaffRequestDetailPage = () => { - +
@@ -140,13 +156,21 @@ const StaffRequestDetailPage = () => {
+ {showModal && ( + setShowModal('')} className='flex items-center justify-center'> +
e.stopPropagation()}> +
+
Confirmation
+ setShowModal('')} + /> +
+
+ {showModal === 'approve' + ? 'Are you sure you want to approve this request?' + : showModal === 'reject' + ? 'Are you sure you want to reject this request?' + : 'Choose the folder to assign this document to'} +
+ setReason(e.target.value)} + placeholder='Enter your reason here' + /> +
+
+
+
+ )}
); }; diff --git a/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx b/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx index 3db9d3e..19e305e 100644 --- a/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx +++ b/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx @@ -1,15 +1,19 @@ import Status from '@/components/Status/Status.component'; import Table from '@/components/Table/Table.component'; +import { AuthContext } from '@/context/authContext'; import useNavigateSelect from '@/hooks/useNavigateSelect'; import usePagination from '@/hooks/usePagination'; import { IBorrowRequest } from '@/types/item'; import { dateFormatter } from '@/utils/formatter'; import { Column } from 'primereact/column'; +import { useContext } from 'react'; const StaffRequestPage = () => { + const { user } = useContext(AuthContext); + const { getPaginatedTableProps } = usePagination({ key: 'requests', - url: '/borrows/staffs', + url: `/documents/borrows?roomId=${user?.roomId}`, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); diff --git a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx index 5f33a3b..42c84e2 100644 --- a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx +++ b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx @@ -16,7 +16,7 @@ import { OnResultFunction } from 'react-qr-reader'; import { useNavigate } from 'react-router'; const initialValues = { - id: 'N/A', + documentId: 'N/A', types: 'N/A', title: 'N/A', locker: 'N/A', @@ -32,28 +32,34 @@ const StaffReturnsPage = () => { const [values, setValues] = useState(initialValues); const [error, setError] = useState(''); - const { borrowerDepartment, borrowerId, borrowerName, folder, id, locker, title, types } = values; + const { borrowerDepartment, borrowerId, borrowerName, folder, documentId, locker, title, types } = + values; - const getDocumentsById = async (id: string) => { - if (!id) return; + const getDocumentsById = async (documentId: string) => { + if (!documentId) return; try { - const { data } = await axiosClient.get(`/borrows/documents/${id}`, { - params: { - status: 'checkedout,overdue', - page: 1, - size: 1, - sortBy: 'BorrowTime', - sortDirection: 'desc', - }, - }); + const { data } = await axiosClient.get( + `/documents/borrows/${documentId}`, + { + params: { + status: 'checkedout,overdue', + page: 1, + size: 1, + sortBy: 'BorrowTime', + sortDirection: 'desc', + }, + } + ); const request = data.data.items[0]; - const { data: document } = await axiosClient.get(`/documents/${id}`); + const { data: document } = await axiosClient.get( + `/documents/${documentId}` + ); const { data: employee } = await axiosClient.get( `/users/${request.borrowerId}` ); setValues((prev) => ({ ...prev, - id: document.data.id, + documentId: document.data.id, types: document.data.documentType, title: document.data.title, locker: document.data.folder.locker.name, @@ -72,7 +78,7 @@ const StaffReturnsPage = () => { const onScan: OnResultFunction = async (e) => { const id = e?.getText(); - if (!id || id === values.id) return; + if (!id || id === values.documentId) return; try { await getDocumentsById(id); setOpenScan(false); @@ -83,7 +89,7 @@ const StaffReturnsPage = () => { const onApprove = async () => { try { - await axiosClient.post(`/borrows/return/${id}`); + await axiosClient.post(`/documents/borrows/return/${documentId}`); navigate(AUTH_ROUTES.REQUESTS); } catch (error) { const axiosError = error as AxiosError; @@ -105,7 +111,7 @@ const StaffReturnsPage = () => {
- +
@@ -133,19 +139,19 @@ const StaffReturnsPage = () => { className='h-11 rounded-lg' onClick={() => setOpenScan((prev) => !prev)} /> - {id !== 'N/A' && ( + {documentId !== 'N/A' && (
diff --git a/src/pages/staff/index.ts b/src/pages/staff/index.ts index bd5c6fa..a038019 100644 --- a/src/pages/staff/index.ts +++ b/src/pages/staff/index.ts @@ -1,11 +1,13 @@ export { default as StaffDashboardPage } from './StaffDashboardPage/StaffDashboardPage'; export { default as StaffLockerPage } from './StaffLockerPage/StaffLockerPage'; export { default as StaffDocumentPage } from './StaffDocumentPage/StaffDocumentPage'; -export { default as StaffImportPage } from './StaffImportPage/StaffImportPage'; +export { default as StaffImportCreatePage } from './StaffImportCreatePage/StaffImportCreatePage'; export { default as StaffDocumentDetailPage } from './StaffDocumentDetailPage/StaffDocumentDetailPage'; export { default as StaffReturnPage } from './StaffReturnPage/StaffReturnPage'; export { default as StaffRequestDetailPage } from './StaffRequestDetailPage/StaffRequestDetailPage'; export { default as StaffRequestPage } from './StaffRequestPage/StaffRequestPage'; export { default as StaffLockerDetailPage } from './StaffLockerDetailPage/StaffLockerDetailPage'; export { default as StaffFolderPage } from './StaffFolderPage/StaffFolderPage'; -export { default as StaffFolderDetailPage } from './StaffFolderDetailPage/StaffFolderDetailPage'; \ No newline at end of file +export { default as StaffFolderDetailPage } from './StaffFolderDetailPage/StaffFolderDetailPage'; +export { default as StaffImportDetailPage } from './StaffImportDetailPage/StaffImportDetailPage'; +export { default as StaffImportPage } from './StaffImportPage/StaffImportPage'; \ No newline at end of file diff --git a/src/types/item.ts b/src/types/item.ts index 0c4198b..0992888 100644 --- a/src/types/item.ts +++ b/src/types/item.ts @@ -11,7 +11,6 @@ export interface IItem { export interface IDepartment { id: string; name: string; - roomId: string; } export interface IDocument { @@ -23,6 +22,7 @@ export interface IDocument { importer: IUser; folder: IFolder; status: DOCUMENT_STATUS_KEY; + isPrivate: boolean; } export interface IRoom { @@ -71,6 +71,13 @@ export interface IUser { createdBy: string; lastModified: string; lastModifiedBy: string; + roomId?: string; +} + +export interface IStaff { + user: IUser; + room: IRoom; + id: string; } export interface IBorrowRequest { @@ -80,6 +87,16 @@ export interface IBorrowRequest { borrowTime: string; dueTime: string; actualReturnTime: string; - reason: string; + borrowReason: string; + staffReason: string; status: REQUEST_STATUS_KEY; } + +export interface IImportRequest { + room: IRoom; + document: IDocument; + importReason: string; + staffReason: string; + status: string; + id: string; +} diff --git a/src/types/response.ts b/src/types/response.ts index 41c834c..2df90af 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -1,4 +1,13 @@ -import { IBorrowRequest, IDepartment, IDocument, IFolder, ILocker, IRoom, IUser } from './item'; +import { + IBorrowRequest, + IDepartment, + IDocument, + IFolder, + IImportRequest, + ILocker, + IRoom, + IUser, +} from './item'; export type BaseResponse = { data: T; @@ -67,3 +76,7 @@ export type GetFolderByIdResponse = BaseResponse; export type GetRoomsResponse = BaseResponse<{ items: IRoom[] } & PaginationResponse>; export type GetRoomByIdResponse = BaseResponse; + +export type GetDepartmentByIdResponse = BaseResponse; + +export type GetImportByIdResponse = BaseResponse; diff --git a/src/types/roles.ts b/src/types/roles.ts index 5cc6f75..714bbb8 100644 --- a/src/types/roles.ts +++ b/src/types/roles.ts @@ -13,6 +13,20 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.HOME, icon: PrimeIcons.HOME, }, + { + type: 'group', + label: 'Departments', + }, + { + label: 'Manage', + path: AUTH_ROUTES.DEPARTMENTS_MANAGE, + icon: PrimeIcons.BUILDING, + }, + { + label: 'Create', + path: AUTH_ROUTES.NEW_DEPARTMENT, + icon: PrimeIcons.PLUS, + }, { type: 'group', label: 'Users', @@ -36,20 +50,20 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { }, { label: 'Staffs', - path: AUTH_ROUTES.STAFFS_MANAGE, + path: AUTH_ROUTES.STAFFS, icon: PrimeIcons.USER, - // items: [ - // { - // label: 'Manage', - // path: AUTH_ROUTES.STAFFS_MANAGE, - // icon: PrimeIcons.USER, - // }, - // // { - // // label: 'Create', - // // path: AUTH_ROUTES.NEW_STAFF, - // // icon: PrimeIcons.USER_PLUS, - // // }, - // ], + items: [ + { + label: 'Manage', + path: AUTH_ROUTES.STAFFS_MANAGE, + icon: PrimeIcons.USER, + }, + { + label: 'Assign', + path: AUTH_ROUTES.NEW_STAFF, + icon: PrimeIcons.USER_PLUS, + }, + ], }, { type: 'group', @@ -130,17 +144,12 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { ], path: AUTH_ROUTES.PHYSICAL, }, - { - label: 'Import', - path: AUTH_ROUTES.IMPORT, - icon: PrimeIcons.UPLOAD, - }, { type: 'group', - label: 'Borrowed docs', + label: 'Requests', }, { - label: 'Requests', + label: 'Borrows', path: AUTH_ROUTES.REQUESTS, icon: PrimeIcons.BELL, }, @@ -149,6 +158,23 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.RETURNS, icon: PrimeIcons.REPLY, }, + { + label: 'Imports', + path: AUTH_ROUTES.IMPORT, + icon: PrimeIcons.UPLOAD, + items: [ + { + label: 'Employee Requests', + path: AUTH_ROUTES.IMPORT_MANAGE, + icon: PrimeIcons.USERS, + }, + { + label: 'Manual', + path: AUTH_ROUTES.NEW_IMPORT, + icon: PrimeIcons.PLUS, + }, + ], + }, ], // Employee sidebar employee: [ @@ -171,14 +197,32 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.DRIVE, icon: PrimeIcons.CLOUD, }, + { type: 'group', - label: 'Borrowed docs', + label: 'Requests', }, { - label: 'Requests', + label: 'Borrow', path: AUTH_ROUTES.REQUESTS, icon: PrimeIcons.BELL, }, + { + label: 'Import', + path: AUTH_ROUTES.IMPORT, + icon: PrimeIcons.UPLOAD, + items: [ + { + label: 'Manage', + path: AUTH_ROUTES.IMPORT_MANAGE, + icon: PrimeIcons.UPLOAD, + }, + { + label: 'Create', + path: AUTH_ROUTES.NEW_IMPORT, + icon: PrimeIcons.PLUS, + }, + ], + }, ], }; From c7ade6c31a9fbfc83a08ec7044a194c3e579f4b7 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 28 Jun 2023 23:06:43 +0700 Subject: [PATCH 05/18] Rebase --- src/hooks/useRooms.tsx | 2 -- .../staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/hooks/useRooms.tsx b/src/hooks/useRooms.tsx index 18fb79b..c3ce8cb 100644 --- a/src/hooks/useRooms.tsx +++ b/src/hooks/useRooms.tsx @@ -50,8 +50,6 @@ const useRooms = (groupByDepartment = false, departmentId = '') => { })) : []; - console.log(rooms); - return { rooms, roomsRefetch, diff --git a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx index 393c2c9..9032a78 100644 --- a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx +++ b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx @@ -52,8 +52,6 @@ const StaffDocumentDetailPage = () => { // }, } = data.data; - console.log(data.data.folder); - const folder = data.data.folder || { id: '', name: '', From 7f76898fcd0b303fb0041184e8512d07fcccda86 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 28 Jun 2023 23:13:34 +0700 Subject: [PATCH 06/18] patch(admin): fix request detail admin --- .../AdminRequestDetailPage.tsx | 75 ++----------------- .../AdminRequestPage/AdminRequestPage.tsx | 6 +- .../StaffRequestDetailPage.tsx | 1 - 3 files changed, 6 insertions(+), 76 deletions(-) diff --git a/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx b/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx index 768bfe3..753fbf0 100644 --- a/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx +++ b/src/pages/admin/AdminRequestDetailPage/AdminRequestDetailPage.tsx @@ -3,35 +3,23 @@ import InformationPanel from '@/components/InformationPanel/InformationPanel.com import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; import { SkeletonPage } from '@/components/Skeleton'; import { AUTH_ROUTES } from '@/constants/routes'; -import { REQUEST_STATUS } from '@/constants/status'; import { - BaseResponse, GetDocumentByIdResponse, GetRequestByIdResponse, GetUserByIdResponse, } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; -import { AxiosError } from 'axios'; import { Button } from 'primereact/button'; -import { useState } from 'react'; import { useQuery } from 'react-query'; import { Navigate, useParams } from 'react-router'; import { Link } from 'react-router-dom'; -const NO_ACTIONS = [ - REQUEST_STATUS.Cancelled.status, - REQUEST_STATUS.CheckedOut.status, - REQUEST_STATUS.NotProcessable.status, - REQUEST_STATUS.Returned.status, - REQUEST_STATUS.Lost.status, -]; - const AdminRequestDetailPage = () => { const { requestId } = useParams<{ requestId: string }>(); - const [error, setError] = useState(''); - const { data, refetch } = useQuery( + const { data } = useQuery( ['requests', requestId], - async () => (await axiosClient.get(`/borrows/${requestId}`)).data, + async () => + (await axiosClient.get(`/documents/borrows/${requestId}`)).data, { enabled: !!requestId, } @@ -70,43 +58,7 @@ const AdminRequestDetailPage = () => { const { id: employeeId, lastName, firstName } = employee.data; - const { borrowTime, dueTime, reason, status } = data.data; - - const onApprove = async () => { - try { - await axiosClient.post(`/borrows/approve/${requestId}`); - await refetch(); - } catch (error) { - const axiosError = error as AxiosError; - const message = axiosError.response?.data.message || 'Something went wrong'; - console.log(error); - setError(message); - } - }; - - const onDeny = async () => { - try { - await axiosClient.post(`/borrows/reject/${requestId}`); - await refetch(); - } catch (error) { - const axiosError = error as AxiosError; - const message = axiosError.response?.data.message || 'Something went wrong'; - console.log(error); - setError(message); - } - }; - - const onCheckout = async () => { - try { - await axiosClient.post(`/borrows/checkout/${requestId}`); - await refetch(); - } catch (error) { - const axiosError = error as AxiosError; - const message = axiosError.response?.data.message || 'Something went wrong'; - console.log(error); - setError(message); - } - }; + const { borrowTime, dueTime, borrowReason, status } = data.data; return (
@@ -131,32 +83,15 @@ const AdminRequestDetailPage = () => { - +
- {NO_ACTIONS.indexOf(status) !== -1 ? null : status === - REQUEST_STATUS.Approved.status ? ( -
- {error &&
{error}
}
diff --git a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx index 591a7aa..8c2ef27 100644 --- a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx +++ b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx @@ -18,11 +18,7 @@ const AdminRequestPage = () => {

Pending requests

- +
Date: Tue, 11 Jul 2023 00:23:08 +0700 Subject: [PATCH 07/18] lol goodluck --- src/components/Navbar/Navbar.component.tsx | 3 + src/components/Status/Status.component.tsx | 12 +- src/constants/routes.ts | 1 + src/constants/status.ts | 27 +- .../SignInFormContainer.tsx | 2 +- src/hooks/usePagination.tsx | 10 +- src/pages/Guards/RoleMapper.tsx | 8 + .../AdminDepartmentDetailPage.tsx | 2 +- .../AdminDocumentDetailPage.tsx | 47 +- .../AdminEmployeeDetailPage.tsx | 54 +- .../AdminFolderDetailPage.tsx | 17 +- .../AdminLockerDetailPage.tsx | 6 +- src/pages/admin/AdminLogPage/AdminLogPage.tsx | 84 +++ .../AdminRequestPage/AdminRequestPage.tsx | 26 +- .../AdminRoomDetailPage.tsx | 6 +- .../AdminStaffCreatePage.tsx | 2 +- src/pages/admin/index.ts | 3 +- .../emp/EmpDashboardPage/EmpDashboardPage.tsx | 13 +- .../EmpDocumentDetailPage.tsx | 506 +++++++++++++----- .../emp/EmpDocumentPage/EmpDocumentPage.tsx | 46 +- .../EmpImportCreatePage.tsx | 2 +- src/pages/emp/EmpImportPage/EmpImportPage.tsx | 9 +- .../emp/EmpProfilePage/EmpProfilePage.tsx | 217 ++++++++ .../EmpRequestCreatePage.tsx | 50 +- .../EmpRequestDetailPage.tsx | 311 ++++++----- .../emp/EmpRequestPage/EmpRequestPage.tsx | 27 +- .../StaffDashboardPage/StaffDashboardPage.tsx | 49 +- .../StaffDocumentDetailPage.tsx | 26 +- .../StaffFolderDetailPage.tsx | 15 +- .../staff/StaffImportPage/StaffImportPage.tsx | 7 +- .../StaffLockerDetailPage.tsx | 15 +- .../StaffRequestDetailPage.tsx | 50 +- .../StaffRequestPage/StaffRequestPage.tsx | 24 +- .../staff/StaffReturnPage/StaffReturnPage.tsx | 4 +- src/types/item.ts | 21 +- src/types/response.ts | 5 + src/types/roles.ts | 16 +- 37 files changed, 1277 insertions(+), 446 deletions(-) create mode 100644 src/pages/admin/AdminLogPage/AdminLogPage.tsx create mode 100644 src/pages/emp/EmpProfilePage/EmpProfilePage.tsx diff --git a/src/components/Navbar/Navbar.component.tsx b/src/components/Navbar/Navbar.component.tsx index 23418b0..c2d20c5 100644 --- a/src/components/Navbar/Navbar.component.tsx +++ b/src/components/Navbar/Navbar.component.tsx @@ -8,10 +8,12 @@ import clsx from 'clsx'; import DashboardPage from '@/pages/DashboardPage/DashboardPage'; import MobileSideBar from '../Sidebar/MobileSideBar.component'; import axiosClient from '@/utils/axiosClient'; +import { useQueryClient } from 'react-query'; const Navbar = () => { const { user, dispatch } = useContext(AuthContext); const [open, setOpen] = useState(false); + const queryClient = useQueryClient(); const signOut = async () => { try { @@ -24,6 +26,7 @@ const Navbar = () => { payload: null, }); localStorage.removeItem('user'); + queryClient.clear(); } }; diff --git a/src/components/Status/Status.component.tsx b/src/components/Status/Status.component.tsx index 9aced59..b929baa 100644 --- a/src/components/Status/Status.component.tsx +++ b/src/components/Status/Status.component.tsx @@ -1,5 +1,13 @@ import { DOCUMENT_STATUS, REQUEST_STATUS } from '@/constants/status'; -import { IBorrowRequest, IDocument, IFolder, ILocker, IRoom, IUser } from '@/types/item'; +import { + IBorrowRequest, + IDocument, + IFolder, + IImportRequest, + ILocker, + IRoom, + IUser, +} from '@/types/item'; import clsx from 'clsx'; import { FC, HTMLAttributes } from 'react'; @@ -9,7 +17,7 @@ interface IStatusBorrowProps { } interface IStatusDocumentProps { - item: IDocument; + item: IDocument | IImportRequest; type: 'document'; } diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 9a1c79c..ba40630 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -34,6 +34,7 @@ export const AUTH_ROUTES = { IMPORT_ID: '/import/manage/:importId', IMPORT_MANAGE: '/import/manage', IMPORT: '/import', + PROFILE: '/profile', }; export const UNAUTH_ROUTES = { diff --git a/src/constants/status.ts b/src/constants/status.ts index 6a75499..b867d0b 100644 --- a/src/constants/status.ts +++ b/src/constants/status.ts @@ -7,6 +7,7 @@ export const REQUEST_STATUS = { Returned: { status: 'Returned', color: 'bg-primary' }, Lost: { status: 'Lost', color: 'bg-black' }, NotProcessable: { status: 'NotProcessable', color: 'bg-black' }, + Overdue: { status: 'Overdue', color: 'bg-red-500' }, }; export type REQUEST_STATUS_KEY = keyof typeof REQUEST_STATUS; @@ -28,6 +29,30 @@ export const DOCUMENT_STATUS = { status: 'Lost', color: 'bg-black', }, + Pending: { + status: 'Pending', + color: 'bg-yellow-500', + }, + Rejected: { + status: 'Rejected', + color: 'bg-red-500', + }, + Approved: { + status: 'Approved', + color: 'bg-green-500', + }, + Assigned: { + status: 'Assigned', + color: 'bg-blue-500', + }, + CheckedIn: { + status: 'CheckedIn', + color: 'bg-primary', + }, + Overdue: { + status: 'Overdue', + color: 'bg-red-500', + }, }; -export type DOCUMENT_STATUS_KEY = keyof typeof DOCUMENT_STATUS; \ No newline at end of file +export type DOCUMENT_STATUS_KEY = keyof typeof DOCUMENT_STATUS; diff --git a/src/containers/SignInFormContainer/SignInFormContainer.tsx b/src/containers/SignInFormContainer/SignInFormContainer.tsx index 9dde7c6..d10cb43 100644 --- a/src/containers/SignInFormContainer/SignInFormContainer.tsx +++ b/src/containers/SignInFormContainer/SignInFormContainer.tsx @@ -77,7 +77,7 @@ const SignInForm = () => { }); } else { const message = - (axiosError.response?.data as { message?: string }).message || 'Something went wrong'; + (axiosError.response?.data as { message?: string }).message || 'Bad request'; setErrors({ error: message, }); diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index 7cd4ea5..033bd08 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -4,7 +4,7 @@ import { BaseResponse, PaginationResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { DataTableProps, DataTableStateEvent, DataTableValueArray } from 'primereact/datatable'; import { useState, useContext } from 'react'; -import { useQuery } from 'react-query'; +import { UseQueryOptions, useQuery } from 'react-query'; interface ILazyTableState { first: number; @@ -17,11 +17,12 @@ interface ILazyTableState { const DEFAULT_ROWS = 10; const ROWS_PER_PAGE_OPTIONS = [DEFAULT_ROWS, 20, 50, 100]; -type UsePaginationProps = { +type UsePaginationProps = { key: string | Record | string[]; url: string; query?: string; lazyConfig?: ILazyTableState; + queryConfig?: UseQueryOptions>; }; const DEFAULT_LAZY_CONFIG: ILazyTableState = { @@ -37,7 +38,8 @@ const usePagination = ({ url, query = '', lazyConfig = DEFAULT_LAZY_CONFIG, -}: UsePaginationProps) => { + queryConfig, +}: UsePaginationProps) => { const { user } = useContext(AuthContext); const [paginate, setPaginate] = useState(lazyConfig); @@ -59,6 +61,8 @@ const usePagination = ({ ).data, { ...REFETCH_CONFIG, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(queryConfig as any), } ); diff --git a/src/pages/Guards/RoleMapper.tsx b/src/pages/Guards/RoleMapper.tsx index b37406a..9872a45 100644 --- a/src/pages/Guards/RoleMapper.tsx +++ b/src/pages/Guards/RoleMapper.tsx @@ -53,6 +53,7 @@ const EmpImportDetailPage = lazy( () => import('@/pages/emp/EmpImportDetailPage/EmpImportDetailPage') ); const EmpImportPage = lazy(() => import('@/pages/emp/EmpImportPage/EmpImportPage')); +const EmpProfilePage = lazy(() => import('@/pages/emp/EmpProfilePage/EmpProfilePage')); // Admin imports const AdminDashboardPage = lazy( @@ -107,6 +108,7 @@ const AdminDepartmentCreatePage = lazy( const AdminDepartmentDetailPage = lazy( () => import('@/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage') ); +const AdminLogPage = lazy(() => import('@/pages/admin/AdminLogPage/AdminLogPage')); export const ROLE_MAPPER = { [AUTH_ROUTES.HOME]: { @@ -225,4 +227,10 @@ export const ROLE_MAPPER = { employee: () => , staff: () => , }, + [AUTH_ROUTES.LOGS]: { + admin: () => , + }, + [AUTH_ROUTES.PROFILE]: { + employee: () => , + }, }; diff --git a/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx b/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx index 46fc791..49afd8c 100644 --- a/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx +++ b/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx @@ -61,7 +61,7 @@ const AdminDepartmentDetailPage = () => { navigate(AUTH_ROUTES.ROOMS); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; diff --git a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx index 7b3471d..0a714e2 100644 --- a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx +++ b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx @@ -43,14 +43,7 @@ const AdminDocumentDetailPage = () => { if ((error as AxiosError)?.response?.status === 404 || !data) return ; - const { - title, - // folder: { - // id: folderId, - // name: folderName, - // locker: { id: lockerId, name: lockerName }, - // }, - } = data.data; + const { title } = data.data; const folder = data.data.folder || { id: '', @@ -86,7 +79,7 @@ const AdminDocumentDetailPage = () => { setEditMode(false); } catch (error) { const axiosError = error as AxiosError; - setFieldError('title', axiosError?.response?.data?.message || 'Something went wrong'); + setFieldError('title', axiosError?.response?.data?.message || 'Bad request'); } }; @@ -103,18 +96,30 @@ const AdminDocumentDetailPage = () => {

- / - - {roomName} - - / - - {lockerName} - - / - - {folderName} - + {roomId && ( + <> + / + + {roomName} + + + )} + {lockerId && ( + <> + / + + {lockerName} + + + )} + {folderId && ( + <> + / + + {folderName} + + + )} / {title}

diff --git a/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx b/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx index 0352610..b1b3a67 100644 --- a/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx +++ b/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx @@ -19,7 +19,7 @@ const AdminEmployeeDetailPage = () => { const [qr, setQr] = useState(''); const [editMode, setEditMode] = useState(false); const queryClient = useQueryClient(); - const [error] = useState(''); + const [error, setError] = useState(''); const { data: user, @@ -34,12 +34,12 @@ const AdminEmployeeDetailPage = () => { ); useEffect(() => { - const generateQr = async () => { + const renderQr = async () => { if (!empId) return; const qrCode = await QRCode.toDataURL(empId); setQr(qrCode); }; - generateQr(); + renderQr(); }, [empId]); if (isLoading) return ; @@ -56,31 +56,31 @@ const AdminEmployeeDetailPage = () => { role, email, username, - // isActive, + isActive, department: { name: departmentName }, } = user.data; type FormValues = typeof initialValues; - // const onToggleAvailability = async () => { - // try { - // if (isActive) { - // await axiosClient.put(`/users/disable/${empId}`); - // } else { - // await axiosClient.post(`/users/enable/${empId}`, {}); - // } - // queryClient.invalidateQueries('users'); - // } catch (error) { - // const axiosError = error as AxiosError; - // setError(axiosError.response?.data.message || 'Something went wrong'); - // } - // }; + const onToggleAvailability = async () => { + try { + await axiosClient.put(`/users/${empId}`, { + ...initialValues, + role, + isActive: !isActive, + }); + queryClient.invalidateQueries('users'); + } catch (error) { + const axiosError = error as AxiosError; + setError(axiosError.response?.data.message || 'Bad request'); + } + }; const onSubmit = async (values: FormValues) => { if (JSON.stringify(values) === JSON.stringify(initialValues)) return setEditMode(false); try { await axiosClient.put(`/users/${empId}`, values); - queryClient.invalidateQueries('documents'); + queryClient.invalidateQueries('users'); setEditMode(false); } catch (error) { const axiosError = error as AxiosError; @@ -215,15 +215,15 @@ const AdminEmployeeDetailPage = () => { }} /> ) : ( - //
+
+
+ + + + + + + } + /> + +
+
+
+ ); +}; + +export default AdminLogPage; diff --git a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx index 8c2ef27..149260e 100644 --- a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx +++ b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx @@ -1,15 +1,23 @@ import Status from '@/components/Status/Status.component'; import Table from '@/components/Table/Table.component'; +import { AUTH_ROUTES } from '@/constants/routes'; import useNavigateSelect from '@/hooks/useNavigateSelect'; import usePagination from '@/hooks/usePagination'; import { IBorrowRequest } from '@/types/item'; import { dateFormatter } from '@/utils/formatter'; +import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { useRef } from 'react'; +import { Link } from 'react-router-dom'; const AdminRequestPage = () => { - const { getPaginatedTableProps } = usePagination({ + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ key: 'requests', url: '/documents/borrows', + query: query.current, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); @@ -17,6 +25,22 @@ const AdminRequestPage = () => { return (

Pending requests

+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> +
diff --git a/src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx b/src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx index 19819a8..df2c385 100644 --- a/src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx +++ b/src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx @@ -90,7 +90,7 @@ const AdminRoomDetailPage = () => { queryClient.invalidateQueries('rooms'); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -122,7 +122,7 @@ const AdminRoomDetailPage = () => { setEditMode(false); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -133,7 +133,7 @@ const AdminRoomDetailPage = () => { navigate(AUTH_ROUTES.ROOMS); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; diff --git a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx index b478fbc..18822bc 100644 --- a/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx +++ b/src/pages/admin/AdminStaffCreatePage/AdminStaffCreatePage.tsx @@ -56,7 +56,7 @@ const AdminStaffCreatePage = () => { } catch (error) { const axiosError = error as AxiosError; console.error(error); - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 4c4af9b..7aeff12 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -19,4 +19,5 @@ export { default as AdminRoomDetailPage } from './AdminRoomDetailPage/AdminRoomD export { default as AdminStaffCreatePage } from './AdminStaffCreatePage/AdminStaffCreatePage'; export { default as AdminDepartmentPage } from './AdminDepartmentPage/AdminDepartmentPage'; export { default as AdminDepartmentCreatePage } from './AdminDepartmentCreatePage/AdminDepartmentCreatePage'; -export { default as AdminDepartmentDetailPage } from './AdminDepartmentDetailPage/AdminDepartmentDetailPage'; \ No newline at end of file +export { default as AdminDepartmentDetailPage } from './AdminDepartmentDetailPage/AdminDepartmentDetailPage'; +export { default as AdminLogPage } from './AdminLogPage/AdminLogPage'; diff --git a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx index 7069b51..4647e3c 100644 --- a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx +++ b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx @@ -8,14 +8,19 @@ import InfoCard from '@/components/Card/InfoCard.component'; import { useQueries, useQuery } from 'react-query'; import Status from '@/components/Status/Status.component'; import { dateFormatter } from '@/utils/formatter'; +import { useContext } from 'react'; +import { AuthContext } from '@/context/authContext'; const EmpDashboardPage = () => { + const { user } = useContext(AuthContext); + const { data: requests, isLoading: isRequestsLoading } = useQuery( ['requests', 'recent'], async () => ( await axiosClient.get('/documents/borrows', { params: { + employeeId: user?.id, sortOrder: 'desc', size: 4, page: 1, @@ -33,7 +38,13 @@ const EmpDashboardPage = () => { temp.data.items.map((request) => ({ queryKey: ['documents', request.id], queryFn: async () => - (await axiosClient.get(`/documents/${request.documentId}`)).data, + ( + await axiosClient.get(`/documents/${request.documentId}`, { + params: { + employeeId: user?.id, + }, + }) + ).data, })) ); diff --git a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx index 28da291..b165db6 100644 --- a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx +++ b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx @@ -1,27 +1,44 @@ import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; import { AUTH_ROUTES } from '@/constants/routes'; -import { GetDocumentByIdResponse } from '@/types/response'; +import { BaseResponse, GetDocumentByIdResponse, GetPermissionResponse } from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState, useEffect } from 'react'; -import { useQuery } from 'react-query'; +import { useState, useEffect, useContext } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; import QRCode from 'qrcode'; import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; +import { Formik, FormikHelpers } from 'formik'; import { SkeletonPage } from '@/components/Skeleton'; import Status from '@/components/Status/Status.component'; import { AxiosError } from 'axios'; import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; +import Overlay from '@/components/Overlay/Overlay.component'; +import { PrimeIcons } from 'primereact/api'; +import clsx from 'clsx'; +import { AuthContext } from '@/context/authContext'; +import { InputSwitch } from 'primereact/inputswitch'; const EmpDocumentDetailPage = () => { const { documentId = '' } = useParams<{ documentId: string }>(); const [qr, setQr] = useState(''); + const [editMode, setEditMode] = useState(false); + const [showModal, setShowModal] = useState(false); + const queryClient = useQueryClient(); + const { user } = useContext(AuthContext); + const [perms, setPerms] = useState({ + canRead: false, + canBorrow: false, + documentId: '', + employeeId: '', + }); + const [error, setError] = useState(''); const { - data: doc, + data, isLoading, error: axiosError, } = useQuery( @@ -29,146 +46,389 @@ const EmpDocumentDetailPage = () => { async () => (await axiosClient.get(`/documents/${documentId}`)).data ); + const { data: permission } = useQuery( + ['documents', documentId, user?.id], + async () => + (await axiosClient.get(`/documents/${documentId}/permissions`)).data + ); + useEffect(() => { const renderQr = async () => { - const { id } = doc?.data || { id: '' }; + const { id } = data?.data || { id: '' }; if (!id) return; const qrCode = await QRCode.toDataURL(id); setQr(qrCode); }; + const updatePerms = async () => { + if (!permission?.data) return; + setPerms(permission.data); + }; + updatePerms(); renderQr(); - }, [doc]); + }, [data, permission]); if (isLoading) return ; - if ((axiosError as AxiosError)?.response?.status === 404 || !doc) + if ((axiosError as AxiosError)?.response?.status === 404 || !data) return ; - const { - title, - documentType, - folder: { - id: folderId, - name: folderName, - locker: { id: lockerId, name: lockerName }, + const { title, importer, isPrivate } = data.data; + + const folder = data.data.folder || { + id: '', + name: '', + locker: { + id: '', + name: '', }, - description, - importer: { firstName, lastName, id: importerId }, - } = doc.data; + }; + + const { + id: folderId, + name: folderName, + locker: { id: lockerId, name: lockerName } = {}, + } = folder; + + const initialValues = { ...data.data, folder }; + + type FormValues = typeof initialValues; + + const onSubmit = async (values: FormValues, { setFieldError }: FormikHelpers) => { + if (JSON.stringify(values) === JSON.stringify(initialValues)) return setEditMode(false); + try { + await axiosClient.put(`/documents/${documentId}`, { + ...values, + documentType: values.documentType.toUpperCase(), + }); + queryClient.invalidateQueries('documents'); + setEditMode(false); + } catch (error) { + const axiosError = error as AxiosError; + setFieldError('title', axiosError?.response?.data?.message || 'Bad request'); + } + }; + + const validate = (values: FormValues) => { + const errors: Partial = {}; + + if (!values.title) errors.title = 'Title is required'; + if (!values.documentType) errors.documentType = 'Document type is required'; + + return errors; + }; return ( -
-
-

- / - - {lockerName} - - / - - {folderName} - - / - {title} -

+ <> +
+
+

+ {lockerId && ( + <> + / + + {lockerName} + + + )} + {folderId && ( + <> + / + + {folderName} + + + )} + / + {title} +

+
+ + {({ + values, + touched, + errors, + handleChange, + handleBlur, + handleSubmit, + submitForm, + resetForm, + isSubmitting, + setFieldValue, + isValid, + }) => ( +
+
+ +
+ +
+ + +
+ + } + /> + + {importer.id === user?.id && ( +
+ + setFieldValue('isPrivate', e.value)} + disabled={!editMode} + /> +
+ )} + + +
+ + +
+
+
+
+ + {qr ? ( + + ) : ( +
+ )} +
+ {editMode ? ( +
+ + + + + +
+ + )} +
-
-
- -
- -
-
-
-
- - {qr ? ( - - ) : ( -
- )} -
-
- - - - - -
-
-
+
+ + )} + ); }; diff --git a/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx b/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx index be1b644..9065ffe 100644 --- a/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx +++ b/src/pages/emp/EmpDocumentPage/EmpDocumentPage.tsx @@ -13,11 +13,19 @@ import useNavigateSelect from '@/hooks/useNavigateSelect'; const EmpDocumentPage = () => { const query = useRef(''); - const { getPaginatedTableProps, refetch } = usePagination({ - key: ['documents', query.current], - url: '/documents/employees', - query: query.current, - }); + const { getPaginatedTableProps: getPublicTableProps, refetch: publicRefetch } = + usePagination({ + key: ['documents', query.current, 'false'], + url: '/documents/employees', + query: query.current, + }); + + const { getPaginatedTableProps: getPrivateTableProps, refetch: privateRefetch } = + usePagination({ + key: ['documents', query.current, 'true'], + url: '/documents/employees?isPrivate=true', + query: query.current, + }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); @@ -28,7 +36,8 @@ const EmpDocumentPage = () => { className='flex h-11 gap-3' onSubmit={async (e) => { e.preventDefault(); - await refetch(); + await publicRefetch(); + await privateRefetch(); }} > {
+

Private document

+
+
+ + + + + } + /> + + +
+
+

Public document

- +
{ sortable body={(item) => } /> - + } + />
diff --git a/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx b/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx new file mode 100644 index 0000000..e515ff3 --- /dev/null +++ b/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx @@ -0,0 +1,217 @@ +import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; +import { AuthContext } from '@/context/authContext'; +import { BaseResponse, GetUserByIdResponse } from '@/types/response'; +import axiosClient from '@/utils/axiosClient'; +import { useContext, useState, useEffect } from 'react'; +import { useQueryClient } from 'react-query'; +import QRCode from 'qrcode'; +import { AxiosError } from 'axios'; +import { Formik } from 'formik'; +import InformationPanel from '@/components/InformationPanel/InformationPanel.component'; +import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { Button } from 'primereact/button'; + +const EmpProfilePage = () => { + const { user, dispatch } = useContext(AuthContext); + const [editMode, setEditMode] = useState(false); + const [error, setError] = useState(''); + const queryClient = useQueryClient(); + const [qr, setQr] = useState(''); + + useEffect(() => { + const renderQr = async () => { + if (!user?.id) return; + const qrCode = await QRCode.toDataURL(user.id); + setQr(qrCode); + }; + renderQr(); + }, [user?.id]); + + if (!user) return ; + + const initialValues = { + firstName: user.firstName, + lastName: user.lastName, + }; + + const { + role, + email, + username, + position, + department: { name: departmentName }, + } = user; + + type FormValues = typeof initialValues; + + const onSubmit = async (values: FormValues) => { + if (JSON.stringify(values) === JSON.stringify(initialValues)) return setEditMode(false); + try { + const { + data: { data }, + } = await axiosClient.put(`/users/self`, values); + queryClient.invalidateQueries('users'); + const user = { + ...data, + role: data.role.toLowerCase(), + }; + + dispatch({ + type: 'LOGIN', + payload: user, + }); + localStorage.setItem('user', JSON.stringify(user)); + setEditMode(false); + } catch (error) { + const axiosError = error as AxiosError; + // const status = axiosError.response?.status; + const message = axiosError.response?.data.message || 'Bad request'; + console.error(message); + setError(message); + } + }; + + const validate = (values: FormValues) => { + const error = {} as { [key in keyof FormValues]: string }; + Object.entries(values).forEach(([key, value]) => { + if (!value) { + error[key as keyof FormValues] = 'Required'; + } + }); + return error; + }; + + return ( + + {({ + values, + touched, + errors, + handleChange, + handleBlur, + resetForm, + submitForm, + isValid, + isSubmitting, + }) => ( +
+
+ + + + + + + + + + + +
+
+ + {qr ? ( + + ) : ( +
+ )} +
+ {editMode && ( +
+ +
+
+ )} + + ); +}; + +export default EmpProfilePage; diff --git a/src/pages/emp/EmpRequestCreatePage/EmpRequestCreatePage.tsx b/src/pages/emp/EmpRequestCreatePage/EmpRequestCreatePage.tsx index 58a906a..c8757e8 100644 --- a/src/pages/emp/EmpRequestCreatePage/EmpRequestCreatePage.tsx +++ b/src/pages/emp/EmpRequestCreatePage/EmpRequestCreatePage.tsx @@ -16,10 +16,6 @@ import { useNavigate } from 'react-router'; type InitialValues = { id: string; - title: string; - documentType: string; - locker: string; - folder: string; reason: string; dates: Date[]; }; @@ -27,11 +23,12 @@ type InitialValues = { const EmpRequestCreatePage = () => { const query = useQueryParams(); const timeout = useRef(); - const queries = query.entries(); - const [initialValues, setInitialValues] = useState({ - ...(Object.fromEntries(queries) as unknown as InitialValues), + const initialValues = { + id: query.get('id') || '', + dates: [] as Date[], reason: '', - }); + }; + const [document, setDocument] = useState(null); const [openScan, setOpenScan] = useState(false); const navigate = useNavigate(); @@ -75,7 +72,7 @@ const EmpRequestCreatePage = () => { return; } try { - const { data } = await axiosClient.post('/borrows', { + const { data } = await axiosClient.post('/documents/borrows', { documentId: id, borrowFrom: dates[0].toISOString(), borrowTo: dates[1].toISOString(), @@ -84,7 +81,7 @@ const EmpRequestCreatePage = () => { navigate(`${AUTH_ROUTES.REQUESTS}/${data.data.id}`); } catch (error) { const axiosError = error as AxiosError; - setFieldError('id', axiosError.response?.data?.message || 'Something went wrong'); + setFieldError('id', axiosError.response?.data?.message || 'Bad request'); } }; @@ -93,14 +90,7 @@ const EmpRequestCreatePage = () => { try { const data = await getDocumentById(initialValues.id); if (!data) return; - setInitialValues({ - ...initialValues, - title: data.data.title, - documentType: data.data.documentType, - locker: data.data.folder.locker.name, - folder: data.data.folder.name, - reason: '', - }); + setDocument(data.data); } catch (error) { console.log(error); } @@ -112,23 +102,16 @@ const EmpRequestCreatePage = () => { const handleIdChange = async ( id: string, // eslint-disable-next-line - setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void, setFieldError: (field: string, value: string) => void ) => { try { const doc = await getDocumentById(id); if (!doc) { - setFieldValue('title', '', false); - setFieldValue('documentType', '', false); - setFieldValue('locker', '', false); - setFieldValue('folder', '', false); + setDocument(null); setFieldError('id', 'Document not found'); return; } - setFieldValue('title', doc.data.title); - setFieldValue('documentType', doc.data.documentType); - setFieldValue('locker', doc.data.folder.locker.name); - setFieldValue('folder', doc.data.folder.name); + setDocument(doc.data); setFieldError('id', ''); } catch (error) { console.log(error); @@ -154,9 +137,10 @@ const EmpRequestCreatePage = () => { const result = e?.getText(); if (!result) return; setFieldValue('id', result); - handleIdChange(result, setFieldValue, setFieldError); + handleIdChange(result, setFieldError); setOpenScan(false); }; + console.log(errors); return ( <>
@@ -168,7 +152,7 @@ const EmpRequestCreatePage = () => { id='id' onChange={async (e) => { handleChange(e); - handleIdChange(e.target.value, setFieldValue, setFieldError); + handleIdChange(e.target.value, setFieldError); }} onBlur={handleBlur} error={!!errors.id} @@ -185,14 +169,14 @@ const EmpRequestCreatePage = () => { /> {
{ /> { @@ -41,11 +47,7 @@ const EmpRequestDetailPage = () => { } ); const [editMode, setEditMode] = useState(false); - const [values, setValues] = useState({ - borrowReason: '', - borrowTime: '', - dueTime: '', - }); + const queryClient = useQueryClient(); useEffect(() => { const renderQr = async () => { @@ -54,16 +56,6 @@ const EmpRequestDetailPage = () => { const qrCode = await QRCode.toDataURL(documentId); setQr(qrCode); }; - - const updateValues = () => { - const { borrowReason, borrowTime, dueTime } = data?.data || { - borrowReason: 'This is a reason', - borrowTime: new Date(), - dueTime: new Date(new Date().setDate(new Date().getDate() + 7)), - }; - setValues({ borrowReason, borrowTime, dueTime }); - }; - updateValues(); renderQr(); }, [data]); @@ -80,33 +72,31 @@ const EmpRequestDetailPage = () => { if (!requestId) return ; if (isLoading) return ; - // if (!document || !data) - // return ( - // - // ); - - // const { - // title, - // documentType, - // // folder: { - // // name: folder, - // // locker: { name: locker }, - // // }, - // } = document.data; - - const folder = '', - locker = '', - title = '', - documentType = ''; - - const status = 'Pending'; - - // const { status, borrowReason, borrowTime, dueTime } = data.data; + if (!document || !data) + return ( + + ); + + const { + title, + documentType, + folder: { + name: folder, + locker: { name: locker }, + }, + } = document.data; + + const { status } = data.data; + + const initialValues = { + dates: [new Date(data.data.borrowTime), new Date(data.data.dueTime)], + reason: data.data.borrowReason || '', + }; const onCancel = async () => { try { @@ -117,109 +107,146 @@ const EmpRequestDetailPage = () => { } }; - const onUpdate = async () => { - if (!values.borrowReason || !values.borrowTime || !values.dueTime) return; - if (JSON.stringify(values) === JSON.stringify(data?.data)) return; + const validate = (values: InitialValues) => { + const error = {} as { [key: string]: string | Date[] }; + Object.entries(values).forEach(([key, value]) => { + if (key === 'dates') { + const values = value as Date[]; + if (!values || !values[0] || !values[1]) { + error[key] = 'Must provide borrow and return date'; + } + } + if (!value) error[key] = 'Required'; + }); + return error; + }; + + const onSubmit = async ( + values: InitialValues, + { setFieldError, setFieldTouched }: FormikHelpers + ) => { + const { dates, reason } = values; + if (!dates || dates.length !== 2) { + setFieldTouched('dates', true, false); + setFieldError('dates', 'Missing information'); + return; + } try { - await axiosClient.put(`/documents/borrows/${requestId}`, { - reason: values.borrowReason, - borrowFrom: values.borrowTime, - borrowTo: values.dueTime, + await axiosClient.put(`/documents/borrows/${requestId}`, { + borrowFrom: dates[0].toISOString(), + borrowTo: dates[1].toISOString(), + reason: reason, }); - await refetchRequest(); - setEditMode(false); + queryClient.invalidateQueries(['requests']); } catch (error) { - console.log(error); + const axiosError = error as AxiosError; + setFieldError('id', axiosError.response?.data?.message || 'Bad request'); } }; - const onEditCancel = () => { - setEditMode(false); - setValues({ - borrowReason: data?.data.borrowReason || 'This is a reason', - borrowTime: data?.data.borrowTime || new Date(), - dueTime: data?.data.dueTime || new Date(new Date().setDate(new Date().getDate() + 7)), - }); - }; - return ( -
-
- - - -
- - + + {({ + values, + touched, + errors, + handleChange, + handleBlur, + resetForm, + submitForm, + isValid, + isSubmitting, + }) => ( +
+
+ + + +
+ + +
+
+ + + +
+ +
+
- - - - setValues((prev) => ({ ...prev, borrowReason: e.target.value }))} - error={values.borrowReason === ''} - small={values.borrowReason === '' ? 'Please enter a reason' : ''} - /> -
- { - if (e.value === null) { - setValues((prev) => ({ ...prev, borrowTime: null, dueTime: null })); - return; - } - const [borrowTime, dueTime] = e.value as Date[]; - setValues((prev) => ({ ...prev, borrowTime, dueTime })); - }} - error={!values.borrowTime || !values.dueTime} - small={!values.borrowTime || !values.dueTime ? 'Please select a date' : ''} - /> +
+ + {qr ? ( + + ) : ( +
+ )} + {status === REQUEST_STATUS.Pending.status && ( +
-
-
-
- - {qr ? ( - - ) : ( -
- )} - {status === REQUEST_STATUS.Pending.status && ( -
-
+
+ )} + ); }; diff --git a/src/pages/emp/EmpRequestPage/EmpRequestPage.tsx b/src/pages/emp/EmpRequestPage/EmpRequestPage.tsx index 3cea728..32060e7 100644 --- a/src/pages/emp/EmpRequestPage/EmpRequestPage.tsx +++ b/src/pages/emp/EmpRequestPage/EmpRequestPage.tsx @@ -1,30 +1,49 @@ import Status from '@/components/Status/Status.component'; import Table from '@/components/Table/Table.component'; import { AUTH_ROUTES } from '@/constants/routes'; +import { AuthContext } from '@/context/authContext'; import useNavigateSelect from '@/hooks/useNavigateSelect'; import usePagination from '@/hooks/usePagination'; import { GetRequestsResponse } from '@/types/response'; import { dateFormatter } from '@/utils/formatter'; import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { useRef, useContext } from 'react'; import { Link } from 'react-router-dom'; const EmpRequestPage = () => { - const { getPaginatedTableProps } = usePagination({ + const { user } = useContext(AuthContext); + const query = useRef(''); + const { getPaginatedTableProps, refetch } = usePagination({ key: 'requests', - url: '/documents/borrows', + url: `/documents/borrows?employeeId=${user?.id}`, + query: query.current, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); return (
-
+
+ { + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> +
-

Requests

diff --git a/src/pages/staff/StaffDashboardPage/StaffDashboardPage.tsx b/src/pages/staff/StaffDashboardPage/StaffDashboardPage.tsx index 780f69a..32a56f9 100644 --- a/src/pages/staff/StaffDashboardPage/StaffDashboardPage.tsx +++ b/src/pages/staff/StaffDashboardPage/StaffDashboardPage.tsx @@ -2,7 +2,12 @@ import Progress from '@/components/Progress/Progress.component'; import { SkeletonCard } from '@/components/Skeleton'; import { REFETCH_CONFIG } from '@/constants/config'; import { AUTH_ROUTES } from '@/constants/routes'; -import { GetDocumentsResponse, GetFoldersResponse, GetLockersResponse } from '@/types/response'; +import { + GetDocumentsResponse, + GetFoldersResponse, + GetImportsResponse, + GetLockersResponse, +} from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import clsx from 'clsx'; import { useQuery } from 'react-query'; @@ -17,6 +22,25 @@ const StaffDashboardPage = () => { const roomId = user?.roomId || ''; + const { data: importRequests, isLoading: isImportLoading } = useQuery( + ['imports', 'recent'], + async () => + ( + await axiosClient.get('/documents/import-requests', { + params: { + roomId, + sortBy: 'CreatedAt', + sortOrder: 'desc', + size: 3, + page: 1, + }, + }) + ).data, + { + ...REFETCH_CONFIG, + } + ); + const { data: lockers, isLoading: isLockerLoading } = useQuery( ['lockers', 'recent'], async () => @@ -77,12 +101,27 @@ const StaffDashboardPage = () => { return (
- Pending request > + Import request >
-
-
-
+ {isImportLoading ? ( + [...Array(3)].map((_, index) => ) + ) : importRequests && importRequests.data.items.length !== 0 ? ( + importRequests.data.items.map((importRequest) => ( + +

Room: {importRequest.room.name}

+

+ Status: +

+
+ )) + ) : ( + No imports + )}
Lockers > diff --git a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx index 9032a78..3a47b0c 100644 --- a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx +++ b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx @@ -43,14 +43,7 @@ const StaffDocumentDetailPage = () => { if ((error as AxiosError)?.response?.status === 404 || !data) return ; - const { - title, - // folder: { - // id: folderId, - // name: folderName, - // locker: { id: lockerId, name: lockerName }, - // }, - } = data.data; + const { title } = data.data; const folder = data.data.folder || { id: '', @@ -82,7 +75,7 @@ const StaffDocumentDetailPage = () => { setEditMode(false); } catch (error) { const axiosError = error as AxiosError; - setFieldError('title', axiosError?.response?.data?.message || 'Something went wrong'); + setFieldError('title', axiosError?.response?.data?.message || 'Bad request'); } }; @@ -242,14 +235,13 @@ const StaffDocumentDetailPage = () => { setEditMode(false); }} /> - ) : ( -
diff --git a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx index c51fff0..8faee68 100644 --- a/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx +++ b/src/pages/staff/StaffLockerDetailPage/StaffLockerDetailPage.tsx @@ -89,15 +89,16 @@ const StaffLockerDetailPage = () => { const onToggleAvailability = async () => { try { - if (isAvailable) { - await axiosClient.put(`/lockers/disable/${lockerId}`); - } else { - await axiosClient.put(`/lockers/enable/${lockerId}`); - } + await axiosClient.put(`/lockers/${lockerId}`, { + isAvailable: !isAvailable, + name: lockerName, + description, + capacity, + }); queryClient.invalidateQueries('lockers'); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -129,7 +130,7 @@ const StaffLockerDetailPage = () => { setEditMode(false); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; diff --git a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx index 5319146..f69b487 100644 --- a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx +++ b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx @@ -23,7 +23,6 @@ import { Link } from 'react-router-dom'; const NO_ACTIONS = [ REQUEST_STATUS.Cancelled.status, - REQUEST_STATUS.CheckedOut.status, REQUEST_STATUS.NotProcessable.status, REQUEST_STATUS.Returned.status, REQUEST_STATUS.Lost.status, @@ -68,18 +67,15 @@ const StaffRequestDetailPage = () => { const { title, documentType, - // folder: { - // name: folder, - // locker: { name: locker }, - // }, + folder: { + name: folder, + locker: { name: locker }, + }, } = document.data; - const folder = '', - locker = ''; - const { id: employeeId, lastName, firstName } = employee.data; - const { borrowTime, dueTime, borrowReason, status } = data.data; + const { borrowTime, dueTime, borrowReason, status, staffReason } = data.data; const onApprove = async () => { try { @@ -90,7 +86,7 @@ const StaffRequestDetailPage = () => { await refetch(); } catch (error) { const axiosError = error as AxiosError; - const message = axiosError.response?.data.message || 'Something went wrong'; + const message = axiosError.response?.data.message || 'Bad request'; console.log(error); setError(message); } @@ -105,7 +101,7 @@ const StaffRequestDetailPage = () => { await refetch(); } catch (error) { const axiosError = error as AxiosError; - const message = axiosError.response?.data.message || 'Something went wrong'; + const message = axiosError.response?.data.message || 'Bad request'; console.log(error); setError(message); } @@ -117,7 +113,19 @@ const StaffRequestDetailPage = () => { await refetch(); } catch (error) { const axiosError = error as AxiosError; - const message = axiosError.response?.data.message || 'Something went wrong'; + const message = axiosError.response?.data.message || 'Bad request'; + console.log(error); + setError(message); + } + }; + + const onLost = async () => { + try { + await axiosClient.post(`/documents/borrows/lost/${requestId}`); + await refetch(); + } catch (error) { + const axiosError = error as AxiosError; + const message = axiosError.response?.data.message || 'Bad request'; console.log(error); setError(message); } @@ -131,8 +139,8 @@ const StaffRequestDetailPage = () => {
- - + +
@@ -146,13 +154,21 @@ const StaffRequestDetailPage = () => { - + + {staffReason && }
{NO_ACTIONS.indexOf(status) !== -1 ? null : status === - REQUEST_STATUS.Approved.status ? ( -
diff --git a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx index 42c84e2..6566443 100644 --- a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx +++ b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx @@ -72,7 +72,7 @@ const StaffReturnsPage = () => { const axiosError = error as AxiosError; const status = axiosError.response?.status; if (status === 404) setError('Document not found'); - else setError(axiosError.response?.data.message || 'Something went wrong'); + else setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -95,7 +95,7 @@ const StaffReturnsPage = () => { const axiosError = error as AxiosError; const status = axiosError.response?.status; if (status === 404) setError('Document not found'); - else setError(axiosError.response?.data.message || 'Something went wrong'); + else setError(axiosError.response?.data.message || 'Bad request'); } finally { setOpenScan(false); } diff --git a/src/types/item.ts b/src/types/item.ts index 0992888..1c0f4ba 100644 --- a/src/types/item.ts +++ b/src/types/item.ts @@ -97,6 +97,25 @@ export interface IImportRequest { document: IDocument; importReason: string; staffReason: string; - status: string; + status: DOCUMENT_STATUS_KEY; id: string; } + +export interface Ilog { + id: number; + template: string; + message: string; + level: string; + time: string; + event: string; + user: IUser; + objectId: string; + objectType: string; +} + +export interface IPermission { + canRead: boolean; + canBorrow: boolean; + employeeId: string; + documentId: string; +} \ No newline at end of file diff --git a/src/types/response.ts b/src/types/response.ts index 2df90af..eb61359 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -5,6 +5,7 @@ import { IFolder, IImportRequest, ILocker, + IPermission, IRoom, IUser, } from './item'; @@ -80,3 +81,7 @@ export type GetRoomByIdResponse = BaseResponse; export type GetDepartmentByIdResponse = BaseResponse; export type GetImportByIdResponse = BaseResponse; + +export type GetImportsResponse = BaseResponse<{ items: IImportRequest[] } & PaginationResponse>; + +export type GetPermissionResponse = BaseResponse; \ No newline at end of file diff --git a/src/types/roles.ts b/src/types/roles.ts index 714bbb8..8ef1626 100644 --- a/src/types/roles.ts +++ b/src/types/roles.ts @@ -13,6 +13,15 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.HOME, icon: PrimeIcons.HOME, }, + { + type: 'group', + label: 'Audit', + }, + { + label: 'Logs', + path: AUTH_ROUTES.LOGS, + icon: PrimeIcons.FILE, + }, { type: 'group', label: 'Departments', @@ -164,7 +173,7 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { icon: PrimeIcons.UPLOAD, items: [ { - label: 'Employee Requests', + label: 'Requests', path: AUTH_ROUTES.IMPORT_MANAGE, icon: PrimeIcons.USERS, }, @@ -183,6 +192,11 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { path: AUTH_ROUTES.HOME, icon: PrimeIcons.HOME, }, + { + path: AUTH_ROUTES.PROFILE, + label: 'Profile', + icon: PrimeIcons.USER, + }, { type: 'group', label: 'Documents', From 24751e2640355aadb034db85b32b3488cfad1daf Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 12 Jul 2023 01:54:27 +0700 Subject: [PATCH 08/18] lolol --- .../Breadcrumbs/Breadcrumbs.component.tsx | 52 +++ src/components/Drive/CreateFileModal.tsx | 62 +++ src/components/Drive/CreateFolderModal.tsx | 39 ++ src/components/Drive/File.tsx | 40 ++ src/components/Drive/Folder.tsx | 60 +++ src/components/Drive/RenameModal.tsx | 34 ++ src/components/Drive/ShareModal.tsx | 171 ++++++++ src/components/Drive/index.ts | 3 + .../FileInput/FileInput.component.tsx | 37 +- .../InputWithLabel.component.tsx | 98 ++--- src/constants/routes.ts | 5 +- src/index.css | 4 + src/pages/Guards/RoleMapper.tsx | 18 +- src/pages/admin/AdminLogPage/AdminLogPage.tsx | 2 +- src/pages/emp/EmpDrivePage/EmpDrivePage.tsx | 353 +++++++++++++++ .../EmpDriveSharedPage/EmpDriveSharedPage.tsx | 410 ++++++++++++++++++ src/types/item.ts | 23 + src/types/response.ts | 16 +- src/types/roles.ts | 20 +- src/utils/formatter.ts | 10 + 20 files changed, 1368 insertions(+), 89 deletions(-) create mode 100644 src/components/Breadcrumbs/Breadcrumbs.component.tsx create mode 100644 src/components/Drive/CreateFileModal.tsx create mode 100644 src/components/Drive/CreateFolderModal.tsx create mode 100644 src/components/Drive/File.tsx create mode 100644 src/components/Drive/Folder.tsx create mode 100644 src/components/Drive/RenameModal.tsx create mode 100644 src/components/Drive/ShareModal.tsx create mode 100644 src/components/Drive/index.ts create mode 100644 src/pages/emp/EmpDrivePage/EmpDrivePage.tsx create mode 100644 src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx diff --git a/src/components/Breadcrumbs/Breadcrumbs.component.tsx b/src/components/Breadcrumbs/Breadcrumbs.component.tsx new file mode 100644 index 0000000..5343b36 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.component.tsx @@ -0,0 +1,52 @@ +import { AUTH_ROUTES } from '@/constants/routes'; +import { Fragment } from 'react'; +import { Link } from 'react-router-dom'; + +const Breadcrumbs = ({ + path, + pathArr, + shared = false, +}: { + path: string; + pathArr: string[]; + shared?: boolean; +}) => { + return ( +
+ {shared && ( + <> + / + Home + + )} + {path === '/' ? ( + <> + / + Home + + ) : ( + pathArr.map((item, index) => { + const link = item ? pathArr.slice(0, index + 1).join('/') : '/'; + return ( + + {!shared || link !== '/' ? / : null} + {index === pathArr.length - 1 ? ( + {item} + ) : ( + + {item || 'Home'} + + )} + + ); + }) + )} +
+ ); +}; + +export default Breadcrumbs; diff --git a/src/components/Drive/CreateFileModal.tsx b/src/components/Drive/CreateFileModal.tsx new file mode 100644 index 0000000..905a3b5 --- /dev/null +++ b/src/components/Drive/CreateFileModal.tsx @@ -0,0 +1,62 @@ +import { FormEvent } from 'react'; +import InputWithLabel from '../InputWithLabel/InputWithLabel.component'; +import FileInput from '../FileInput/FileInput.component'; +import { Button } from 'primereact/button'; +import { fileSizeFormatter } from '@/utils/formatter'; +import { useRef } from 'react'; + +const CreateFileModal = ({ + onCreateFile, + setModal, + setFile, + file, +}: { + onCreateFile: (e: FormEvent) => void; + setModal: (value: string) => void; + setFile: (value: File) => void; + file: File | null; +}) => { + const ref = useRef(null); + return ( +
e.stopPropagation()} + > +

Creating folder

+ + { + setFile(f); + if (!ref.current) return; + ref.current.value = f.name; + }} + /> + {file && ( +
+ Selected: {file.name} ({fileSizeFormatter(file.size)}) +
+ )} +
+
+ + ); +}; + +export default CreateFileModal; diff --git a/src/components/Drive/CreateFolderModal.tsx b/src/components/Drive/CreateFolderModal.tsx new file mode 100644 index 0000000..3264cd8 --- /dev/null +++ b/src/components/Drive/CreateFolderModal.tsx @@ -0,0 +1,39 @@ +import { Button } from 'primereact/button'; +import InputWithLabel from '../InputWithLabel/InputWithLabel.component'; +import { FormEvent } from 'react'; + +const CreateFolderModal = ({ + onCreateFolder, + setModal, +}: { + onCreateFolder: (e: FormEvent) => void; + setModal: (value: string) => void; +}) => { + return ( +
e.stopPropagation()} + > +

Creating folder

+ +
+
+ + ); +}; + +export default CreateFolderModal; diff --git a/src/components/Drive/File.tsx b/src/components/Drive/File.tsx new file mode 100644 index 0000000..15b0d25 --- /dev/null +++ b/src/components/Drive/File.tsx @@ -0,0 +1,40 @@ +import { IDrive } from '@/types/item'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { MouseEvent } from 'react'; + +const File = ({ + // shared = false, + file, + onContextMenu, +}: { + shared?: boolean; + file: IDrive; + onContextMenu: (value: string, e: MouseEvent, type: 'file') => void; +}) => { + return ( + <> +
{ + onContextMenu(file.id, e, 'file'); + }} + > +
+
+ +
+

+ {file.name} +

+
+
+ + ); +}; + +export default File; diff --git a/src/components/Drive/Folder.tsx b/src/components/Drive/Folder.tsx new file mode 100644 index 0000000..644bb35 --- /dev/null +++ b/src/components/Drive/Folder.tsx @@ -0,0 +1,60 @@ +import { AUTH_ROUTES } from '@/constants/routes'; +import { IDrive } from '@/types/item'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { Link } from 'react-router-dom'; +import { useState, MouseEvent } from 'react'; + +const Folder = ({ + currentPath, + folder, + shared = false, + onContextMenu, +}: { + currentPath: string; + folder: IDrive; + shared?: boolean; + onContextMenu: (value: string, e: MouseEvent, type: 'folder') => void; +}) => { + const [hover, setHover] = useState(false); + const link = `${shared ? AUTH_ROUTES.DRIVE_SHARED : AUTH_ROUTES.DRIVE}?path=${encodeURIComponent( + currentPath + )}${encodeURIComponent(`${shared ? folder.id : `/${folder.name}`}`)}`; + + return ( + <> + setHover(true)} + onMouseLeave={() => setHover(false)} + onContextMenu={(e) => { + onContextMenu(folder.id, e, 'folder'); + }} + > +
+
+ {hover ? ( + + ) : ( + + )} +
+

{folder.name}

+
+ + + ); +}; + +export default Folder; diff --git a/src/components/Drive/RenameModal.tsx b/src/components/Drive/RenameModal.tsx new file mode 100644 index 0000000..0694cda --- /dev/null +++ b/src/components/Drive/RenameModal.tsx @@ -0,0 +1,34 @@ +import { Button } from 'primereact/button'; +import InputWithLabel from '../InputWithLabel/InputWithLabel.component'; +import { FormEvent } from 'react'; + +const RenameModal = ({ + onRename, + setModal, +}: { + onRename: (e: FormEvent) => void; + setModal: (value: string) => void; +}) => { + return ( +
e.stopPropagation()} + > +

Renaming

+ +
+
+ + ); +}; + +export default RenameModal; diff --git a/src/components/Drive/ShareModal.tsx b/src/components/Drive/ShareModal.tsx new file mode 100644 index 0000000..f539121 --- /dev/null +++ b/src/components/Drive/ShareModal.tsx @@ -0,0 +1,171 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import { Button } from 'primereact/button'; +import { FormEvent, useState } from 'react'; +import { IUser } from '@/types/item'; +import Overlay from '../Overlay/Overlay.component'; +import CustomDropdown from '../Dropdown/Dropdown.component'; +import { PrimeIcons } from 'primereact/api'; +import { InputSwitch } from 'primereact/inputswitch'; +import InputWithLabel from '../InputWithLabel/InputWithLabel.component'; + +type UserWithPermission = { canView: boolean; canEdit: boolean; employee: IUser }; + +const ShareModal = ({ + onShare, + setModal, + users, + sharedUsers, +}: { + onShare: ( + e: FormEvent, + perms: { userId: string; canEdit: boolean; canView: boolean; expiryDate?: Date } + ) => void; + setModal: (value: string) => void; + users: IUser[]; + sharedUsers: UserWithPermission[]; +}) => { + const [shareModal, setShareModal] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [selectedId, setSelectedId] = useState(''); + const [edit, setEdit] = useState(false); + console.log(sharedUsers); + return ( + <> +
e.preventDefault()} + onClick={(e) => e.stopPropagation()} + > +

Changing permission

+ {sharedUsers.map((user) => ( +
+
{user.employee.email}
+
+
+
+ ))} + + + + +

Folders

+
+ {folders?.map((folder) => ( + { + globalCm.current?.hide(e); + fileCm.current?.hide(e); + folderCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+

Files

+
+ {files?.map((file) => ( + { + globalCm.current?.hide(e); + folderCm.current?.hide(e); + fileCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + + + + {modal && ( + setModal('')} className='flex justify-center items-center'> + {modal === 'create-folder' && ( + + )} + {modal === 'upload-file' && ( + + )} + {modal === 'rename-file' && } + {modal === 'share' && ( + ({ ...user, ...user.employee })) || [] + } + users={ + // Only show users that are not already shared + users?.data.items.filter( + (x) => + !sharedUsers?.data.items.find((u) => x.id === u.employee.id) && + // Themselves + x.id !== user?.id + ) || [] + } + onShare={onShare} + setModal={setModal} + /> + )} + + )} + + + ); +}; + +export default EmpDrivePage; diff --git a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx new file mode 100644 index 0000000..8113508 --- /dev/null +++ b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx @@ -0,0 +1,410 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import axiosClient from '@/utils/axiosClient'; +import { InputText } from 'primereact/inputtext'; +import { useQuery, useQueryClient } from 'react-query'; +import { useRef, useState, useContext, useEffect } from 'react'; +import { Button } from 'primereact/button'; +import useQueryParams from '@/hooks/useQueryParams'; +import { ContextMenu } from 'primereact/contextmenu'; +import { PrimeIcons } from 'primereact/api'; +import { MenuItem } from 'primereact/menuitem'; +import { + BaseResponse, + GetCurrentDrivePermissionResponse, + GetDriveByIDResponse, + GetDrivePermissionResponse, + GetDriveResponse, + GetUsersResponse, +} from '@/types/response'; +import Overlay from '@/components/Overlay/Overlay.component'; +import Breadcrumbs from '@/components/Breadcrumbs/Breadcrumbs.component'; +import Folder from '@/components/Drive/Folder'; +import { CreateFolderModal, File } from '@/components/Drive'; +import CreateFileModal from '@/components/Drive/CreateFileModal'; +import RenameModal from '@/components/Drive/RenameModal'; +import { Toast } from 'primereact/toast'; +import { AxiosError } from 'axios'; +import ShareModal from '@/components/Drive/ShareModal'; +import { AuthContext } from '@/context/authContext'; + +const EmpDrivePage = () => { + const query = useRef(''); + const toast = useRef(null); + const globalCm = useRef(null); + const folderCm = useRef(null); + const fileCm = useRef(null); + + const queryClient = useQueryClient(); + const queryParams = useQueryParams(); + const { user } = useContext(AuthContext); + + const path = queryParams.get('path') || ''; + const pathArr = path.split('/'); + const currentPath = pathArr.slice(1).join('/') ? pathArr.join('/') : ''; + + const [modal, setModal] = useState(''); + const [file, setFile] = useState(null); + const [currentItem, setCurrentItem] = useState(path); + const [currentPathPerm, setCurrentPathPerm] = useState<{ canEdit: boolean; canView: boolean }>({ + canEdit: false, + canView: false, + }); + + useEffect(() => { + const getPerms = async () => { + const { data } = await axiosClient.get( + `/shared/entries/${path}/permissions` + ); + setCurrentPathPerm({ + canEdit: data.data.canEdit, + canView: data.data.canView, + }); + }; + getPerms(); + }, [path, setCurrentPathPerm]); + + const { data, refetch } = useQuery( + ['digital', 'private', path], + async () => + ( + await axiosClient.get( + `/shared/entries?entryId=${encodeURIComponent(path)}` + ) + ).data + ); + + const { data: users } = useQuery( + ['digital', 'private', 'users'], + async () => (await axiosClient.get('/users/employees')).data + ); + + const { data: sharedUsers } = useQuery( + ['digital', 'private', 'sharedUsers', currentItem], + async () => + ( + await axiosClient.get( + `/shared/entries/${currentItem}/shared-users` + ) + ).data, + { + enabled: !!currentItem, + } + ); + + const { data: owner } = useQuery( + ['digital', 'private', 'owner', currentItem], + async () => + (await axiosClient.get(`/shared/entries/${currentItem}`)).data, + { + enabled: !!currentItem, + } + ); + + const onError = (error: AxiosError) => { + const msg = error.response?.data.message || 'Something went wrong'; + toast.current?.show({ + severity: 'error', + summary: 'Error', + detail: msg, + className: '!bg-red-200 overflow-hidden', + }); + console.error(error); + }; + + const closeModals = () => { + setModal(''); + setFile(null); + setCurrentItem(''); + }; + + const files = data?.data.items.filter((item) => !item.isDirectory); + const folders = data?.data.items.filter((item) => item.isDirectory); + + const onDelete = async () => { + try { + await axiosClient.post(`/bin/entries?entryId=${currentItem}`); + queryClient.invalidateQueries(['digital', 'private', path]); + } catch (error) { + onError(error as AxiosError); + } + }; + + const onDownload = async () => { + try { + // Open a new window with the file + window.open(`${import.meta.env.VITE_API_ENDPOINT}/entries/${currentItem}/file`, '_blank'); + closeModals(); + } catch (error) { + onError(error as AxiosError); + } + }; + + const fileItems: MenuItem[] = currentPathPerm?.canEdit + ? [ + { + label: 'Share', + icon: PrimeIcons.SHARE_ALT, + command: () => { + setModal('share'); + }, + }, + { + label: 'Rename', + icon: PrimeIcons.PENCIL, + command: () => { + setModal('rename-file'); + }, + }, + { + label: 'Download', + icon: PrimeIcons.DOWNLOAD, + command: onDownload, + }, + { + label: 'Delete', + icon: PrimeIcons.TRASH, + command: onDelete, + }, + ] + : [ + { + label: 'Download', + icon: PrimeIcons.DOWNLOAD, + command: onDownload, + }, + ]; + + const folderItems: MenuItem[] = currentPathPerm?.canEdit + ? [ + { + label: 'Share', + icon: PrimeIcons.SHARE_ALT, + command: () => { + setModal('share'); + }, + }, + { + label: 'Rename', + icon: PrimeIcons.PENCIL, + command: () => { + setModal('rename-file'); + }, + }, + { + label: 'Delete', + icon: PrimeIcons.TRASH, + command: onDelete, + }, + ] + : []; + + const items: MenuItem[] = [ + { + label: 'Create folder', + icon: PrimeIcons.FOLDER, + command: () => { + setModal('create-folder'); + }, + }, + { + label: 'Upload file', + icon: PrimeIcons.UPLOAD, + command: () => { + setModal('upload-file'); + }, + }, + ]; + + const onCreateFolder = async (event: React.FormEvent) => { + event.preventDefault(); + const name = event.currentTarget.folderName.value; + if (!name) return; + + const formData = new FormData(); + formData.set('Name', name); + formData.set('isDirectory', 'true'); + try { + await axiosClient.post(`/shared/entries/${path}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + queryClient.invalidateQueries(['digital', 'private', path]); + closeModals(); + } catch (error) { + onError(error as AxiosError); + } + }; + + const onCreateFile = async (event: React.FormEvent) => { + event.preventDefault(); + if (!file) return; + const name = event.currentTarget.fileName.value || file.name; + + const formData = new FormData(); + formData.set('Name', name); + formData.set('File', file); + formData.set('isDirectory', 'false'); + try { + await axiosClient.post(`/shared/entries/${path}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + queryClient.invalidateQueries(['digital', 'private', path]); + closeModals(); + } catch (error) { + onError(error as AxiosError); + } + }; + + const onRename = async (event: React.FormEvent) => { + event.preventDefault(); + const name = event.currentTarget.newName.value; + if (!name) return; + try { + await axiosClient.put(`/entries/${currentItem}`, { name }); + queryClient.invalidateQueries(['digital', 'private', path]); + closeModals(); + } catch (error) { + onError(error as AxiosError); + } + }; + + const onShare = async ( + e: React.FormEvent, + perms: { + userId: string; + expiryDate?: Date; + canView: boolean; + canEdit: boolean; + } + ) => { + e.preventDefault(); + try { + await axiosClient.put(`/entries/${currentItem}/permissions`, perms); + queryClient.invalidateQueries(['digital', 'private']); + closeModals(); + } catch (error) { + onError(error as AxiosError); + } + }; + + return ( + <> +
{ + if (path === '') return; + currentPathPerm.canEdit && globalCm.current?.show(e); + fileCm.current?.hide(e); + folderCm.current?.hide(e); + }} + > +
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> + +
+ + +

Folders

+
+ {folders?.map((folder) => ( + { + // Race condition + setTimeout(() => { + globalCm.current?.hide(e); + }, 0); + fileCm.current?.hide(e); + const { data: perm } = await axiosClient.get( + `/shared/entries/${value}/permissions` + ); + perm.data.canEdit && folderCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+

Files

+
+ {files?.map((file) => ( + { + // Race condition + setTimeout(() => { + globalCm.current?.hide(e); + }, 0); + folderCm.current?.hide(e); + const { data: perm } = await axiosClient.get( + `/shared/entries/${value}/permissions` + ); + perm.data.canView && fileCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + + +
+ {modal && ( + setModal('')} className='flex justify-center items-center'> + {modal === 'create-folder' && ( + + )} + {modal === 'upload-file' && ( + + )} + {modal === 'rename-file' && } + {modal === 'share' && ( + u.employee.id !== user?.id) + .map((user) => ({ ...user, ...user.employee })) || [] + } + users={ + // Only show users that are not already shared + users?.data.items.filter( + (x) => + !sharedUsers?.data.items.find((u) => x.id === u.employee.id) && + // Themselves + x.id !== user?.id && + // Owners + x.id !== owner?.data.owner.id + ) || [] + } + onShare={onShare} + setModal={setModal} + /> + )} + + )} + + + ); +}; + +export default EmpDrivePage; diff --git a/src/types/item.ts b/src/types/item.ts index 1c0f4ba..823116a 100644 --- a/src/types/item.ts +++ b/src/types/item.ts @@ -118,4 +118,27 @@ export interface IPermission { canBorrow: boolean; employeeId: string; documentId: string; +} + +export interface IDrive { + name: string; + path: string; + fileId: string | null; + fileType: string | null; + fileExtension: string | null; + isDirectory: boolean; + sizeInBytes: number | null; + owner: IUser; + created: string; + createdBy: string; + lastModified: string; + lastModifiedBy: string; + uploader: IUser; + id: string; +} + +export interface IDrivePermission { + canView: boolean; + canEdit: boolean; + employee: IUser; } \ No newline at end of file diff --git a/src/types/response.ts b/src/types/response.ts index eb61359..db5f741 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -2,6 +2,8 @@ import { IBorrowRequest, IDepartment, IDocument, + IDrive, + IDrivePermission, IFolder, IImportRequest, ILocker, @@ -64,6 +66,8 @@ export type GetRequestsResponse = BaseResponse< export type GetRequestByIdResponse = BaseResponse; +export type GetUsersResponse = BaseResponse<{ items: IUser[] } & PaginationResponse>; + export type GetUserByIdResponse = BaseResponse; export type GetLockersResponse = BaseResponse<{ items: ILocker[] } & PaginationResponse>; @@ -84,4 +88,14 @@ export type GetImportByIdResponse = BaseResponse; export type GetImportsResponse = BaseResponse<{ items: IImportRequest[] } & PaginationResponse>; -export type GetPermissionResponse = BaseResponse; \ No newline at end of file +export type GetPermissionResponse = BaseResponse; + +export type GetDriveResponse = BaseResponse<{ items: IDrive[] } & PaginationResponse>; + +export type GetDriveByIDResponse = BaseResponse; + +export type GetDrivePermissionResponse = BaseResponse< + { items: IDrivePermission[] } & PaginationResponse +>; + +export type GetCurrentDrivePermissionResponse = BaseResponse; \ No newline at end of file diff --git a/src/types/roles.ts b/src/types/roles.ts index 8ef1626..e2c5e7d 100644 --- a/src/types/roles.ts +++ b/src/types/roles.ts @@ -105,11 +105,6 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { ], path: AUTH_ROUTES.PHYSICAL, }, - { - label: 'Digital', - path: AUTH_ROUTES.DRIVE, - icon: PrimeIcons.CLOUD, - }, { type: 'group', label: 'Borrowed docs', @@ -207,11 +202,24 @@ export const SIDEBAR_ROLES: { [key: string]: IItem[] } = { icon: PrimeIcons.FOLDER_OPEN, }, { + type: 'group', label: 'Digital', + }, + { + label: 'My Drive', path: AUTH_ROUTES.DRIVE, icon: PrimeIcons.CLOUD, }, - + { + label: 'Shared', + path: AUTH_ROUTES.DRIVE_SHARED, + icon: PrimeIcons.CLOUD_UPLOAD, + }, + { + label: 'Trash', + path: AUTH_ROUTES.DRIVE_TRASH, + icon: PrimeIcons.TRASH, + }, { type: 'group', label: 'Requests', diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts index 107852a..2cdd706 100644 --- a/src/utils/formatter.ts +++ b/src/utils/formatter.ts @@ -7,3 +7,13 @@ export const dateFormatter = ( region = 'vi-VN', options = DEFAULT_DATE_FORMAT_OPTIONS ) => Intl.DateTimeFormat(region, options).format(date); + +export const fileSizeFormatter = (size: number) => { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return `${Math.round(size)} ${units[i]}`; +}; \ No newline at end of file From a3da5a012d746332baf1aaad59566054fbf66539 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 12 Jul 2023 02:34:56 +0700 Subject: [PATCH 09/18] fixed context menu of browser overlapping --- src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx index 8113508..ed7bf76 100644 --- a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx +++ b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx @@ -291,6 +291,8 @@ const EmpDrivePage = () => {
{ + e.preventDefault(); + e.stopPropagation(); if (path === '') return; currentPathPerm.canEdit && globalCm.current?.show(e); fileCm.current?.hide(e); From 2f06c7a4b27eb593db4800e48209e8582044d630 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 12 Jul 2023 15:40:06 +0700 Subject: [PATCH 10/18] Finish reset password --- src/App.tsx | 4 +- .../Breadcrumbs/Breadcrumbs.component.tsx | 15 +- src/components/Drive/File.tsx | 4 +- src/components/Drive/Folder.tsx | 12 +- src/constants/routes.ts | 2 +- .../SignInFormContainer.tsx | 16 +- src/hooks/useDepartments.tsx | 2 +- src/index.css | 6 +- src/pages/CallbackPage/CallbackPage.tsx | 49 +++-- src/pages/Guards/RoleMapper.tsx | 3 +- .../AdminDepartmentDetailPage.tsx | 22 +- .../AdminDepartmentPage.tsx | 14 +- .../AdminEmployeeDetailPage.tsx | 8 +- .../emp/EmpDashboardPage/EmpDashboardPage.tsx | 65 ++++-- src/pages/emp/EmpDrivePage/EmpDrivePage.tsx | 149 ++++++++----- .../EmpDriveSharedPage/EmpDriveSharedPage.tsx | 204 +++++++++++------- .../EmpDriveTrashPage/EmpDriveTrashPage.tsx | 196 +++++++++++++++++ src/types/response.ts | 2 +- 18 files changed, 560 insertions(+), 213 deletions(-) create mode 100644 src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx diff --git a/src/App.tsx b/src/App.tsx index f6cd90d..34f50ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import AuthGuard from '@/pages/Guards/AuthGuard'; import SignInPage from '@/pages/SignInPage/SignInPage'; import Navbar from './components/Navbar/Navbar.component'; import RoleGuard from './pages/Guards/RoleGuard'; -import CallbackPage from './pages/CallbackPage/CallbackPage'; +import ResetPage from './pages/CallbackPage/CallbackPage'; function App() { return ( @@ -23,7 +23,7 @@ function App() { element={ } - unAuthComponent={} + unAuthComponent={} /> } /> diff --git a/src/components/Breadcrumbs/Breadcrumbs.component.tsx b/src/components/Breadcrumbs/Breadcrumbs.component.tsx index 5343b36..556e291 100644 --- a/src/components/Breadcrumbs/Breadcrumbs.component.tsx +++ b/src/components/Breadcrumbs/Breadcrumbs.component.tsx @@ -6,27 +6,30 @@ const Breadcrumbs = ({ path, pathArr, shared = false, + trashed = false, }: { path: string; pathArr: string[]; shared?: boolean; + trashed?: boolean; }) => { return (
{shared && ( <> / - Home + Shared )} {path === '/' ? ( <> / - Home + {trashed ? 'Trashed' : 'Home'} ) : ( pathArr.map((item, index) => { const link = item ? pathArr.slice(0, index + 1).join('/') : '/'; + console.log(item); return ( {!shared || link !== '/' ? / : null} @@ -35,10 +38,14 @@ const Breadcrumbs = ({ ) : ( - {item || 'Home'} + {item || (trashed ? 'Trashed' : 'Home')} )} diff --git a/src/components/Drive/File.tsx b/src/components/Drive/File.tsx index 15b0d25..e3b3005 100644 --- a/src/components/Drive/File.tsx +++ b/src/components/Drive/File.tsx @@ -7,7 +7,9 @@ const File = ({ // shared = false, file, onContextMenu, -}: { +}: // trashed = false, +{ + trashed?: boolean; shared?: boolean; file: IDrive; onContextMenu: (value: string, e: MouseEvent, type: 'file') => void; diff --git a/src/components/Drive/Folder.tsx b/src/components/Drive/Folder.tsx index 644bb35..b5f45f5 100644 --- a/src/components/Drive/Folder.tsx +++ b/src/components/Drive/Folder.tsx @@ -9,23 +9,27 @@ const Folder = ({ currentPath, folder, shared = false, + trashed = false, onContextMenu, }: { currentPath: string; folder: IDrive; shared?: boolean; + trashed?: boolean; onContextMenu: (value: string, e: MouseEvent, type: 'folder') => void; }) => { const [hover, setHover] = useState(false); - const link = `${shared ? AUTH_ROUTES.DRIVE_SHARED : AUTH_ROUTES.DRIVE}?path=${encodeURIComponent( - currentPath - )}${encodeURIComponent(`${shared ? folder.id : `/${folder.name}`}`)}`; + const link = `${ + shared ? AUTH_ROUTES.DRIVE_SHARED : trashed ? AUTH_ROUTES.DRIVE_TRASH : AUTH_ROUTES.DRIVE + }?path=${encodeURIComponent(currentPath)}${encodeURIComponent( + `${shared ? folder.id : `/${folder.name}`}` + )}`; return ( <> setHover(true)} onMouseLeave={() => setHover(false)} onContextMenu={(e) => { diff --git a/src/constants/routes.ts b/src/constants/routes.ts index e206246..5f44248 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -42,7 +42,7 @@ export const AUTH_ROUTES = { export const UNAUTH_ROUTES = { AUTH: '/auth', - CALLBACK: '/callback', + CALLBACK: '/reset', }; export type AUTH_ROUTES_KEY = keyof typeof AUTH_ROUTES; diff --git a/src/containers/SignInFormContainer/SignInFormContainer.tsx b/src/containers/SignInFormContainer/SignInFormContainer.tsx index d10cb43..2f78df2 100644 --- a/src/containers/SignInFormContainer/SignInFormContainer.tsx +++ b/src/containers/SignInFormContainer/SignInFormContainer.tsx @@ -5,8 +5,9 @@ import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component import { AuthContext } from '@/context/authContext'; import Spinner from '@/components/Spinner/Spinner.component'; import axiosClient from '@/utils/axiosClient'; -import { GetRoomByIdResponse, LoginResponse } from '@/types/response'; +import { BaseResponse, GetRoomByIdResponse, LoginResponse } from '@/types/response'; import { AxiosError } from 'axios'; +import { useNavigate } from 'react-router'; const SIGNIN_INITIALS = { email: '', @@ -22,6 +23,7 @@ interface ISiginFormValues { const SignInForm = () => { const { dispatch } = useContext(AuthContext); + const navigate = useNavigate(); const onValidate = async (values: ISiginFormValues) => { const errors: { email?: string; password?: string } = {}; @@ -70,14 +72,20 @@ const SignInForm = () => { localStorage.setItem('user', JSON.stringify(user)); } catch (error) { console.error(error); - const axiosError = error as AxiosError; + const axiosError = error as AxiosError>; if (axiosError.response?.status === 404) { setErrors({ error: 'You have not been assigned a room yet, please contact admin for more information', }); } else { - const message = - (axiosError.response?.data as { message?: string }).message || 'Bad request'; + const token = axiosError.response?.data.data.token; + if (token) { + navigate(`/reset`, { + state: token, + }); + return; + } + const message = axiosError.response?.data.message || 'Bad request'; setErrors({ error: message, }); diff --git a/src/hooks/useDepartments.tsx b/src/hooks/useDepartments.tsx index 28d0389..998354e 100644 --- a/src/hooks/useDepartments.tsx +++ b/src/hooks/useDepartments.tsx @@ -13,7 +13,7 @@ const useDepartments = () => { ); const departments: DropdownOption[] = - departmentsResult?.data.map((department) => ({ + departmentsResult?.data.items.map((department) => ({ name: department.name, id: department.id, })) || []; diff --git a/src/index.css b/src/index.css index caf9e21..2595063 100644 --- a/src/index.css +++ b/src/index.css @@ -69,7 +69,7 @@ body { border-color: var(--action) !important; } -#password { +#password, #confirmPassword { width: 100%; } @@ -102,4 +102,8 @@ body { .p-toast .p-toast-message .p-toast-message-content { @apply border-red-600; +} + +.p-confirm-dialog-message { + color: white; } \ No newline at end of file diff --git a/src/pages/CallbackPage/CallbackPage.tsx b/src/pages/CallbackPage/CallbackPage.tsx index 66d4f21..077760a 100644 --- a/src/pages/CallbackPage/CallbackPage.tsx +++ b/src/pages/CallbackPage/CallbackPage.tsx @@ -1,6 +1,10 @@ import InputWithLabel from '@/components/InputWithLabel/InputWithLabel.component'; +import { UNAUTH_ROUTES } from '@/constants/routes'; +import axiosClient from '@/utils/axiosClient'; import { Formik } from 'formik'; import { Button } from 'primereact/button'; +import { useEffect } from 'react'; +import { Navigate, useLocation, useNavigate } from 'react-router'; const initialValues = { password: '', @@ -9,15 +13,25 @@ const initialValues = { type FormValues = typeof initialValues; -const CallbackPage = () => { - const query = new URLSearchParams(window.location.search); +const ResetPage = () => { + const location = useLocation(); + const navigate = useNavigate(); + const token = location.state; + + useEffect(() => { + window.history.replaceState({}, ''); + }, []); + + if (!token) { + return ; + } const validate = (values: FormValues) => { const errors: Partial = {}; if (!values.password) { errors.password = 'Required'; - } else if (values.password.length < 8) { - errors.password = 'Password must be at least 8 characters'; + } else if (values.password.length < 5) { + errors.password = 'Password must be at least 5 characters'; } if (!values.confirmPassword) { errors.confirmPassword = 'Required'; @@ -29,12 +43,15 @@ const CallbackPage = () => { const onSubmit = async (values: FormValues) => { try { - // const response = await axiosClient.post('/auth/reset-password', { - // password: values.password, - // confirmPassword: values.confirmPassword, - // token: query.get('token'), - // }); - // console.log(response); + const response = await axiosClient.post('/auth/reset-password', { + newPassword: values.password, + confirmPassword: values.confirmPassword, + token, + }); + console.log(response); + navigate(UNAUTH_ROUTES.AUTH, { + replace: true, + }); } catch (error) { console.log(error); } @@ -43,8 +60,11 @@ const CallbackPage = () => { return ( {({ values, touched, errors, handleBlur, handleChange, handleSubmit }) => ( -
-

Reset Password

+ +

Reset Password

{ small={touched.password ? errors.password : undefined} /> { error={touched.confirmPassword && !!errors.confirmPassword} small={touched.confirmPassword ? errors.confirmPassword : undefined} /> -
{ @@ -17,7 +16,7 @@ const AdminDepartmentPage = () => { query: query.current, }); - const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DEPARTMENTS' }); + const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DEPARTMENTS_MANAGE' }); return (
@@ -46,16 +45,7 @@ const AdminDepartmentPage = () => { className='break-keep overflow-ellipsis max-w-[5rem]' sortable /> - - - } - /> - - +
diff --git a/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx b/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx index b1b3a67..a88c2d2 100644 --- a/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx +++ b/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx @@ -57,6 +57,7 @@ const AdminEmployeeDetailPage = () => { email, username, isActive, + isActivated, department: { name: departmentName }, } = user.data; @@ -195,7 +196,7 @@ const AdminEmployeeDetailPage = () => {
- + {qr ? ( ) : ( @@ -218,7 +219,7 @@ const AdminEmployeeDetailPage = () => {
+ {error &&
{error}
}
diff --git a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx index 4647e3c..16ad0c7 100644 --- a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx +++ b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx @@ -1,7 +1,7 @@ import { AUTH_ROUTES } from '@/constants/routes'; import { Link } from 'react-router-dom'; import axiosClient from '@/utils/axiosClient'; -import { GetDocumentByIdResponse, GetRequestsResponse } from '@/types/response'; +import { GetDocumentByIdResponse, GetImportsResponse, GetRequestsResponse } from '@/types/response'; import { REFETCH_CONFIG } from '@/constants/config'; import { SkeletonCard } from '@/components/Skeleton'; import InfoCard from '@/components/Card/InfoCard.component'; @@ -31,6 +31,23 @@ const EmpDashboardPage = () => { ...REFETCH_CONFIG, } ); + const { data: imports, isLoading: isImportsLoading } = useQuery( + ['imports', 'recent'], + async () => + ( + await axiosClient.get('/documents/import-requests', { + params: { + employeeId: user?.id, + sortOrder: 'desc', + size: 4, + page: 1, + }, + }) + ).data, + { + ...REFETCH_CONFIG, + } + ); const temp = requests || { data: { items: [] } }; @@ -50,23 +67,13 @@ const EmpDashboardPage = () => { return (
- - MiniDrive > - -
- {[...Array(3)].map((_, index) => ( -
- File {index} -
- ))} -
- Request > + Borrow request >
{isRequestsLoading ? ( [...Array(3)].map((_, index) => ) - ) : requests ? ( + ) : requests?.data.items.length ? ( requests.data.items.map((request, index) => documents[index].isLoading ? ( @@ -91,7 +98,37 @@ const EmpDashboardPage = () => { ) ) ) : ( -
No folders
+
No requests
+ )} +
+ + Import requests > + +
+ {isImportsLoading ? ( + [...Array(3)].map((_, index) => ) + ) : imports?.data.items.length ? ( + imports.data.items.map((request) => ( + +
+

+ {request.document.title} +

+ +
+

+ Types: {request.document.documentType} +

+

+ Your reason: {request.importReason} +

+

+ Staff reason: {request.staffReason} +

+
+ )) + ) : ( +
No import requests
)}
diff --git a/src/pages/emp/EmpDrivePage/EmpDrivePage.tsx b/src/pages/emp/EmpDrivePage/EmpDrivePage.tsx index 1e94a89..e70d363 100644 --- a/src/pages/emp/EmpDrivePage/EmpDrivePage.tsx +++ b/src/pages/emp/EmpDrivePage/EmpDrivePage.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-mixed-spaces-and-tabs */ import axiosClient from '@/utils/axiosClient'; -import { InputText } from 'primereact/inputtext'; +// import { InputText } from 'primereact/inputtext'; import { useQuery, useQueryClient } from 'react-query'; import { useRef, useState, useContext } from 'react'; -import { Button } from 'primereact/button'; +// import { Button } from 'primereact/button'; import useQueryParams from '@/hooks/useQueryParams'; import { ContextMenu } from 'primereact/contextmenu'; import { PrimeIcons } from 'primereact/api'; @@ -24,6 +24,8 @@ import { Toast } from 'primereact/toast'; import { AxiosError } from 'axios'; import ShareModal from '@/components/Drive/ShareModal'; import { AuthContext } from '@/context/authContext'; +import Spinner from '@/components/Spinner/Spinner.component'; +import { REFETCH_CONFIG } from '@/constants/config'; const EmpDrivePage = () => { const query = useRef(''); @@ -44,16 +46,29 @@ const EmpDrivePage = () => { const [file, setFile] = useState(null); const [currentItem, setCurrentItem] = useState(''); - const { data, refetch } = useQuery( - ['digital', 'private', path], + const { data, isLoading } = useQuery( + ['digital', 'private', path, query.current], async () => - (await axiosClient.get(`/entries?EntryPath=${encodeURIComponent(path)}`)) - .data + ( + await axiosClient.get(`/entries?EntryPath=${encodeURIComponent(path)}`, { + params: { + searchTerm: query.current, + }, + }) + ).data, + REFETCH_CONFIG ); const { data: users } = useQuery( ['digital', 'private', 'users'], - async () => (await axiosClient.get('/users/employees')).data + async () => + ( + await axiosClient.get('/users/employees', { + params: { + pageSize: 100, + }, + }) + ).data ); const { data: sharedUsers } = useQuery( @@ -61,7 +76,12 @@ const EmpDrivePage = () => { async () => ( await axiosClient.get( - `/shared/entries/${currentItem}/shared-users` + `/shared/entries/${currentItem}/shared-users`, + { + params: { + pageSize: 100, + }, + } ) ).data, { @@ -249,15 +269,20 @@ const EmpDrivePage = () => { return ( <> -
{ - globalCm.current?.show(e); - fileCm.current?.hide(e); - folderCm.current?.hide(e); - }} - > -
+ {isLoading ? ( +
+ +
+ ) : ( +
{ + globalCm.current?.show(e); + fileCm.current?.hide(e); + folderCm.current?.hide(e); + }} + > + {/*
{ @@ -273,44 +298,60 @@ const EmpDrivePage = () => {
+
*/} + + {!data || + (data.data.items.length === 0 && ( +
+ This drive is empty +
+ Upload a file or create a folder +
+ ))} + {folders && folders.length !== 0 && ( + <> +

Folders

+
+ {folders.map((folder) => ( + { + globalCm.current?.hide(e); + fileCm.current?.hide(e); + folderCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} + {files && files.length !== 0 && ( + <> +

Files

+
+ {files.map((file) => ( + { + globalCm.current?.hide(e); + folderCm.current?.hide(e); + fileCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} + + +
- - -

Folders

-
- {folders?.map((folder) => ( - { - globalCm.current?.hide(e); - fileCm.current?.hide(e); - folderCm.current?.show(e); - setCurrentItem(value); - }} - /> - ))} -
-

Files

-
- {files?.map((file) => ( - { - globalCm.current?.hide(e); - folderCm.current?.hide(e); - fileCm.current?.show(e); - setCurrentItem(value); - }} - /> - ))} -
- - - -
+ )} {modal && ( setModal('')} className='flex justify-center items-center'> {modal === 'create-folder' && ( diff --git a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx index ed7bf76..c4edf80 100644 --- a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx +++ b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-mixed-spaces-and-tabs */ import axiosClient from '@/utils/axiosClient'; -import { InputText } from 'primereact/inputtext'; +// import { InputText } from 'primereact/inputtext'; import { useQuery, useQueryClient } from 'react-query'; import { useRef, useState, useContext, useEffect } from 'react'; -import { Button } from 'primereact/button'; +// import { Button } from 'primereact/button'; import useQueryParams from '@/hooks/useQueryParams'; import { ContextMenu } from 'primereact/contextmenu'; import { PrimeIcons } from 'primereact/api'; @@ -26,6 +26,8 @@ import { Toast } from 'primereact/toast'; import { AxiosError } from 'axios'; import ShareModal from '@/components/Drive/ShareModal'; import { AuthContext } from '@/context/authContext'; +import Spinner from '@/components/Spinner/Spinner.component'; +import { REFETCH_CONFIG } from '@/constants/config'; const EmpDrivePage = () => { const query = useRef(''); @@ -49,9 +51,14 @@ const EmpDrivePage = () => { canEdit: false, canView: false, }); + const [currentItemPerm, setCurrentItemPerm] = useState({ + canEdit: false, + canView: false, + }); useEffect(() => { const getPerms = async () => { + if (!path) return; const { data } = await axiosClient.get( `/shared/entries/${path}/permissions` ); @@ -63,23 +70,30 @@ const EmpDrivePage = () => { getPerms(); }, [path, setCurrentPathPerm]); - const { data, refetch } = useQuery( - ['digital', 'private', path], + const { data, isLoading } = useQuery( + ['digital', 'shared', path, query.current], async () => ( await axiosClient.get( - `/shared/entries?entryId=${encodeURIComponent(path)}` + `/shared/entries?entryId=${encodeURIComponent(path)}`, + { + params: { + searchTerm: query.current, + pageSize: 100, + }, + } ) - ).data + ).data, + REFETCH_CONFIG ); const { data: users } = useQuery( - ['digital', 'private', 'users'], + ['digital', 'shared', 'users'], async () => (await axiosClient.get('/users/employees')).data ); const { data: sharedUsers } = useQuery( - ['digital', 'private', 'sharedUsers', currentItem], + ['digital', 'shared', 'sharedUsers', currentItem], async () => ( await axiosClient.get( @@ -92,7 +106,7 @@ const EmpDrivePage = () => { ); const { data: owner } = useQuery( - ['digital', 'private', 'owner', currentItem], + ['digital', 'shared', 'owner', currentItem], async () => (await axiosClient.get(`/shared/entries/${currentItem}`)).data, { @@ -123,7 +137,7 @@ const EmpDrivePage = () => { const onDelete = async () => { try { await axiosClient.post(`/bin/entries?entryId=${currentItem}`); - queryClient.invalidateQueries(['digital', 'private', path]); + queryClient.invalidateQueries(['digital', 'shared', path]); } catch (error) { onError(error as AxiosError); } @@ -139,7 +153,7 @@ const EmpDrivePage = () => { } }; - const fileItems: MenuItem[] = currentPathPerm?.canEdit + const fileItems: MenuItem[] = currentItemPerm?.canEdit ? [ { label: 'Share', @@ -174,7 +188,7 @@ const EmpDrivePage = () => { }, ]; - const folderItems: MenuItem[] = currentPathPerm?.canEdit + const folderItems: MenuItem[] = currentItemPerm?.canEdit ? [ { label: 'Share', @@ -227,7 +241,7 @@ const EmpDrivePage = () => { await axiosClient.post(`/shared/entries/${path}`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); - queryClient.invalidateQueries(['digital', 'private', path]); + queryClient.invalidateQueries(['digital', 'shared', path]); closeModals(); } catch (error) { onError(error as AxiosError); @@ -247,7 +261,7 @@ const EmpDrivePage = () => { await axiosClient.post(`/shared/entries/${path}`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); - queryClient.invalidateQueries(['digital', 'private', path]); + queryClient.invalidateQueries(['digital', 'shared', path]); closeModals(); } catch (error) { onError(error as AxiosError); @@ -260,7 +274,7 @@ const EmpDrivePage = () => { if (!name) return; try { await axiosClient.put(`/entries/${currentItem}`, { name }); - queryClient.invalidateQueries(['digital', 'private', path]); + queryClient.invalidateQueries(['digital', 'shared', path]); closeModals(); } catch (error) { onError(error as AxiosError); @@ -279,7 +293,7 @@ const EmpDrivePage = () => { e.preventDefault(); try { await axiosClient.put(`/entries/${currentItem}/permissions`, perms); - queryClient.invalidateQueries(['digital', 'private']); + queryClient.invalidateQueries(['digital', 'shared']); closeModals(); } catch (error) { onError(error as AxiosError); @@ -288,18 +302,23 @@ const EmpDrivePage = () => { return ( <> -
{ - e.preventDefault(); - e.stopPropagation(); - if (path === '') return; - currentPathPerm.canEdit && globalCm.current?.show(e); - fileCm.current?.hide(e); - folderCm.current?.hide(e); - }} - > -
+ {isLoading ? ( +
+ +
+ ) : ( +
{ + e.preventDefault(); + e.stopPropagation(); + if (path === '') return; + currentPathPerm.canEdit && globalCm.current?.show(e); + fileCm.current?.hide(e); + folderCm.current?.hide(e); + }} + > + {/*
{ @@ -315,57 +334,88 @@ const EmpDrivePage = () => {
-
- +
*/} + + {!data || + (data.data.items.length === 0 && ( +
+ This drive is empty +
+ Any files that are being shared with you will appear here +
+ ))} + {folders && folders.length !== 0 && ( + <> +

Folders

+
+ {folders.map((folder) => ( + { + e.preventDefault(); + e.stopPropagation(); + // Race condition + setTimeout(() => { + globalCm.current?.hide(e); + }, 0); + fileCm.current?.hide(e); + const { data: perm } = + await axiosClient.get( + `/shared/entries/${value}/permissions` + ); + setCurrentItemPerm({ + canEdit: perm.data.canEdit, + canView: perm.data.canView, + }); + setCurrentItem(value); + perm.data.canEdit && folderCm.current?.show(e); + }} + /> + ))} +
+ + )} + {files && files.length !== 0 && ( + <> +

Files

+
+ {files.map((file) => ( + { + e.preventDefault(); + e.stopPropagation(); + // Race condition + setTimeout(() => { + globalCm.current?.hide(e); + }, 0); + folderCm.current?.hide(e); + const { data: perm } = + await axiosClient.get( + `/shared/entries/${value}/permissions` + ); + setCurrentItemPerm({ + canEdit: perm.data.canEdit, + canView: perm.data.canView, + }); + setCurrentItem(value); + perm.data.canView && fileCm.current?.show(e); + }} + /> + ))} +
+ + )} -

Folders

-
- {folders?.map((folder) => ( - { - // Race condition - setTimeout(() => { - globalCm.current?.hide(e); - }, 0); - fileCm.current?.hide(e); - const { data: perm } = await axiosClient.get( - `/shared/entries/${value}/permissions` - ); - perm.data.canEdit && folderCm.current?.show(e); - setCurrentItem(value); - }} - /> - ))} -
-

Files

-
- {files?.map((file) => ( - { - // Race condition - setTimeout(() => { - globalCm.current?.hide(e); - }, 0); - folderCm.current?.hide(e); - const { data: perm } = await axiosClient.get( - `/shared/entries/${value}/permissions` - ); - perm.data.canView && fileCm.current?.show(e); - setCurrentItem(value); - }} - /> - ))} + + +
- - - -
+ )} {modal && ( setModal('')} className='flex justify-center items-center'> {modal === 'create-folder' && ( diff --git a/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx b/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx new file mode 100644 index 0000000..dd487f9 --- /dev/null +++ b/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx @@ -0,0 +1,196 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import axiosClient from '@/utils/axiosClient'; +// import { InputText } from 'primereact/inputtext'; +import { useQuery, useQueryClient } from 'react-query'; +import { useRef, useState } from 'react'; +// import { Button } from 'primereact/button'; +import useQueryParams from '@/hooks/useQueryParams'; +import { ContextMenu } from 'primereact/contextmenu'; +import { PrimeIcons } from 'primereact/api'; +import { MenuItem } from 'primereact/menuitem'; +import { BaseResponse, GetDriveResponse } from '@/types/response'; +import Folder from '@/components/Drive/Folder'; +import { File } from '@/components/Drive'; +import { Toast } from 'primereact/toast'; +import { AxiosError } from 'axios'; +import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog'; +import Spinner from '@/components/Spinner/Spinner.component'; +import { REFETCH_CONFIG } from '@/constants/config'; +import Breadcrumbs from '@/components/Breadcrumbs/Breadcrumbs.component'; + +const EmpDriveTrashPage = () => { + const query = useRef(''); + const toast = useRef(null); + const cm = useRef(null); + + const queryClient = useQueryClient(); + const queryParams = useQueryParams(); + + const path = queryParams.get('path') || '/'; + const pathArr = path.split('/'); + const currentPath = pathArr.slice(1).join('/') ? pathArr.join('/') : ''; + + const [currentItem, setCurrentItem] = useState(''); + + const { data, isLoading } = useQuery( + ['digital', 'bin', path, query.current], + async () => + ( + await axiosClient.get( + `/bin/entries?EntryPath=${encodeURIComponent(path)}`, + { + params: { + searchTerm: query.current, + pageSize: 100, + }, + } + ) + ).data, + REFETCH_CONFIG + ); + + const onError = (error: AxiosError) => { + const msg = error.response?.data.message || 'Something went wrong'; + toast.current?.show({ + severity: 'error', + summary: 'Error', + detail: msg, + className: '!bg-red-200 overflow-hidden', + }); + console.error(error); + }; + + const files = data?.data.items.filter((item) => !item.isDirectory); + const folders = data?.data.items.filter((item) => item.isDirectory); + + const onDelete = async () => { + try { + await axiosClient.delete(`/bin/entries/${currentItem}`); + queryClient.invalidateQueries(['digital', 'bin', path]); + } catch (error) { + onError(error as AxiosError); + } + }; + + const onRestore = async () => { + try { + await axiosClient.put(`/bin/entries/${currentItem}/restore`); + queryClient.invalidateQueries(['digital', 'bin', path]); + } catch (error) { + onError(error as AxiosError); + } + }; + + const items: MenuItem[] = [ + { + label: 'Restore', + icon: PrimeIcons.REFRESH, + command: () => { + confirmDialog({ + message: 'Are you sure you want to restore this item?', + header: 'Confirmation', + className: '!text-white', + accept: onRestore, + rejectClassName: + '!border-red-500 text-white bg-transparent hover:!bg-[#fff3] transition-colors', + }); + }, + }, + { + label: 'Delete permanently', + icon: PrimeIcons.TRASH, + command: () => { + confirmDialog({ + message: 'Are you sure you want to delete this item permanently? This cannot be undone', + header: 'Confirmation', + className: '!text-white', + accept: onDelete, + rejectClassName: + '!border-red-500 text-white bg-transparent hover:!bg-[#fff3] transition-colors', + }); + }, + }, + ]; + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( +
+ {/*
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> +
*/} + + {(!data || data.data.items.length === 0) && ( +
+ This bin is empty +
+ Any recently deleted files or folder will appear here +
+ )} + {folders && folders.length !== 0 && ( + <> +

Folders

+
+ {folders?.map((folder) => ( + { + e.preventDefault(); + e.stopPropagation(); + cm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} + {files && files.length !== 0 && ( + <> +

Files

+
+ {files?.map((file) => ( + { + e.preventDefault(); + e.stopPropagation(); + cm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} + + +
+ )} + + + ); +}; + +export default EmpDriveTrashPage; diff --git a/src/types/response.ts b/src/types/response.ts index db5f741..0ce5be7 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -28,7 +28,7 @@ export type PaginationResponse = { export type GetDocumentTypesResponse = BaseResponse; -export type GetDepartmentsResponse = BaseResponse; +export type GetDepartmentsResponse = BaseResponse<{ items: IDepartment[] } & PaginationResponse>; export type GetEmptyContainersResponse = BaseResponse< { From 6cd735c12c5955f37fed271ca60816ccf51be699 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 12 Jul 2023 16:42:30 +0700 Subject: [PATCH 11/18] change employee detail permimssion --- .../ImportDocumentContainer.tsx | 23 ++++- .../AdminDocumentDetailPage.tsx | 27 +++-- .../AdminEmployeeDetailPage.tsx | 2 +- .../EmpDocumentDetailPage.tsx | 98 ++++++++++++++----- .../emp/EmpProfilePage/EmpProfilePage.tsx | 2 +- .../StaffDocumentDetailPage.tsx | 12 +-- 6 files changed, 114 insertions(+), 50 deletions(-) diff --git a/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx b/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx index 0bd13bc..d0bd57c 100644 --- a/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx +++ b/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx @@ -75,6 +75,25 @@ const ImportDocumentContainer = () => { { setSubmitting, setFieldError }: FormikHelpers ) => { try { + // const formData = new FormData(); + // formData.set('Name', values.title); + // formData.set('Path', '/'); + // formData.set('isDirectory', 'true'); + // await axiosClient.post('/entries', formData, { + // headers: { 'Content-Type': 'multipart/form-data' }, + // }); + // await Promise.all( + // values.files.map((file) => { + // const fileData = new FormData(); + // fileData.set('Name', file.name); + // fileData.set('Path', `/${values.title}`); + // fileData.set('File', file); + // fileData.set('isDirectory', 'false'); + // return axiosClient.post('/entries', fileData, { + // headers: { 'Content-Type': 'multipart/form-data' }, + // }); + // }) + // ); mutation.mutate( { title: values.title, @@ -329,7 +348,7 @@ const ImportDocumentContainer = () => { /> - + {/* { className='bg-primary rounded-lg h-11' disabled={isSubmitting} /> - + */}
{openScan && ( diff --git a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx index 0a714e2..94351f7 100644 --- a/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx +++ b/src/pages/admin/AdminDocumentDetailPage/AdminDocumentDetailPage.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; import QRCode from 'qrcode'; import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; -import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; +// import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; import { Formik, FormikHelpers } from 'formik'; import { SkeletonPage } from '@/components/Skeleton'; import Status from '@/components/Status/Status.component'; @@ -140,7 +140,7 @@ const AdminDocumentDetailPage = () => {
-
+ {/*
{ className='self-end bg-primary rounded-lg h-11' type='button' /> -
+
*/} { ) : (
)} -
+
{editMode ? (
- + {/* { ]} /> - + */}
)} diff --git a/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx b/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx index a88c2d2..3d1d568 100644 --- a/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx +++ b/src/pages/admin/AdminEmployeeDetailPage/AdminEmployeeDetailPage.tsx @@ -202,7 +202,7 @@ const AdminEmployeeDetailPage = () => { ) : (
)} -
+
{editMode ? (
- + {/* { ]} /> - + */}
)} @@ -360,11 +395,12 @@ const EmpDocumentDetailPage = () => {
- user?.id !== u.id)} value={perms.employeeId} + optionLabel='email' + optionValue='id' onChange={(e) => { setError(''); setPerms((prev) => ({ ...prev, employeeId: e.target.value })); @@ -378,7 +414,13 @@ const EmpDocumentDetailPage = () => { checked={perms.canRead} name='canRead' id='canRead' - onChange={(e) => setPerms((prev) => ({ ...prev, canRead: e.value as boolean }))} + onChange={(e) => + setPerms((prev) => ({ + ...prev, + canRead: e.value as boolean, + canBorrow: e.value ? prev.canBorrow : false, + })) + } />
Can borrow:
{ name='canBorrow' id='canBorrow' onChange={(e) => setPerms((prev) => ({ ...prev, canBorrow: e.value as boolean }))} + disabled={!perms.canRead} />
@@ -417,6 +460,9 @@ const EmpDocumentDetailPage = () => { new Date().setDate(new Date().getDate() + 7) ).toISOString(), }); + queryClient.invalidateQueries(['documents', documentId, user?.id]); + setError(''); + setShowModal(false); } catch (error) { const axiosError = error as AxiosError; const msg = axiosError.response?.data.message || 'Bad request'; diff --git a/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx b/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx index e515ff3..e025c5e 100644 --- a/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx +++ b/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx @@ -177,7 +177,7 @@ const EmpProfilePage = () => { ) : (
)} -
+
{editMode && (
- + {/* { ]} /> - + */}
)} From 51d5fcb9821c990c8d324c91546fb3acf6466f6e Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Wed, 12 Jul 2023 19:08:35 +0700 Subject: [PATCH 12/18] clean up on modal close --- src/components/Drive/CreateFileModal.tsx | 6 +++--- src/components/Drive/CreateFolderModal.tsx | 6 +++--- src/components/Drive/File.tsx | 9 +++++---- src/components/Drive/Folder.tsx | 12 ++++++------ src/components/Drive/RenameModal.tsx | 6 +++--- src/components/Drive/ShareModal.tsx | 6 +++--- src/pages/emp/EmpDrivePage/EmpDrivePage.tsx | 14 +++++++------- .../emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx | 8 ++++---- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/components/Drive/CreateFileModal.tsx b/src/components/Drive/CreateFileModal.tsx index 905a3b5..4d8a185 100644 --- a/src/components/Drive/CreateFileModal.tsx +++ b/src/components/Drive/CreateFileModal.tsx @@ -7,12 +7,12 @@ import { useRef } from 'react'; const CreateFileModal = ({ onCreateFile, - setModal, + handleClose, setFile, file, }: { onCreateFile: (e: FormEvent) => void; - setModal: (value: string) => void; + handleClose: () => void; setFile: (value: File) => void; file: File | null; }) => { @@ -49,7 +49,7 @@ const CreateFileModal = ({ @@ -58,7 +59,10 @@ const Navbar = () => {
- +
+ + +
diff --git a/src/components/Sidebar/MenuItem.component.tsx b/src/components/Sidebar/MenuItem.component.tsx index f0a58f9..359e86e 100644 --- a/src/components/Sidebar/MenuItem.component.tsx +++ b/src/components/Sidebar/MenuItem.component.tsx @@ -20,7 +20,7 @@ const MenuItem: FC = ({ item }) => {
{ +const Sidebar = ({ open = false }) => { const { user } = useContext(AuthContext); const role = user?.role || 'employee'; const items = SIDEBAR_ROLES[role as Role]; return ( -
); }; diff --git a/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx b/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx index 0254b0c..522f2f9 100644 --- a/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx +++ b/src/pages/admin/AdminDashboardPage/AdminDashboardPage.tsx @@ -86,7 +86,7 @@ const AdminDashboardPage = () => { ); return ( -
+
{/* Pending request > diff --git a/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx b/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx index 5505613..54183e8 100644 --- a/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx +++ b/src/pages/admin/AdminDepartmentDetailPage/AdminDepartmentDetailPage.tsx @@ -50,7 +50,7 @@ const AdminDepartmentDetailPage = () => { }; return ( -
+
diff --git a/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx b/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx index ae9c3a3..ca67726 100644 --- a/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx +++ b/src/pages/admin/AdminDepartmentPage/AdminDepartmentPage.tsx @@ -19,7 +19,7 @@ const AdminDepartmentPage = () => { const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DEPARTMENTS_MANAGE' }); return ( -
+
{ }; return ( -
+

{roomId && ( diff --git a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx index b274871..607c2b8 100644 --- a/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx +++ b/src/pages/admin/AdminDocumentPage/AdminDocumentPage.tsx @@ -20,7 +20,7 @@ const AdminDocumentPage = () => { const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'DOCUMENTS' }); return ( -
+
{ isValid, isSubmitting, }) => ( - + { isValid, isSubmitting, }) => ( -
+
{ const query = useRef(''); @@ -45,6 +50,7 @@ const EmpDrivePage = () => { const [modal, setModal] = useState(''); const [file, setFile] = useState(null); const [currentItem, setCurrentItem] = useState(''); + const [showInfo, setShowInfo] = useState(null); const { data, isLoading } = useQuery( ['digital', 'private', path, query.current], @@ -53,6 +59,7 @@ const EmpDrivePage = () => { await axiosClient.get(`/entries?EntryPath=${encodeURIComponent(path)}`, { params: { searchTerm: query.current, + size: 100, }, }) ).data, @@ -274,15 +281,16 @@ const EmpDrivePage = () => {
) : ( -
{ - globalCm.current?.show(e); - fileCm.current?.hide(e); - folderCm.current?.hide(e); - }} - > - {/*
+ <> +
{ + globalCm.current?.show(e); + fileCm.current?.hide(e); + folderCm.current?.hide(e); + }} + > + {/*
{ @@ -299,63 +307,77 @@ const EmpDrivePage = () => {
*/} - - {!data || - (data.data.items.length === 0 && ( -
- This drive is empty -
- Upload a file or create a folder + + {!data || + (data.data.items.length === 0 && ( +
+ This drive is empty +
+ Upload a file or create a folder +
+ ))} +
+
+ {folders && folders.length !== 0 && ( + <> +

Folders

+
+ {folders.map((folder) => ( + { + globalCm.current?.hide(e); + fileCm.current?.hide(e); + folderCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} + {files && files.length !== 0 && ( + <> +

Files

+
+ {files.map((file) => ( + { + globalCm.current?.hide(e); + folderCm.current?.hide(e); + fileCm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )}
- ))} - {folders && folders.length !== 0 && ( - <> -

Folders

-
- {folders.map((folder) => ( - { - globalCm.current?.hide(e); - fileCm.current?.hide(e); - folderCm.current?.show(e); - setCurrentItem(value); - }} - /> - ))} -
- - )} - {files && files.length !== 0 && ( - <> -

Files

-
- {files.map((file) => ( - { - globalCm.current?.hide(e); - folderCm.current?.hide(e); - fileCm.current?.show(e); - setCurrentItem(value); - }} - /> - ))} -
- - )} - - - -
+ +
+ + + + +
+ )} {modal && ( diff --git a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx index 2344968..8a1815c 100644 --- a/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx +++ b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx @@ -19,7 +19,7 @@ import { import Overlay from '@/components/Overlay/Overlay.component'; import Breadcrumbs from '@/components/Breadcrumbs/Breadcrumbs.component'; import Folder from '@/components/Drive/Folder'; -import { CreateFolderModal, File } from '@/components/Drive'; +import { CreateFolderModal, DetailInfo, File } from '@/components/Drive'; import CreateFileModal from '@/components/Drive/CreateFileModal'; import RenameModal from '@/components/Drive/RenameModal'; import { Toast } from 'primereact/toast'; @@ -28,6 +28,7 @@ import ShareModal from '@/components/Drive/ShareModal'; import { AuthContext } from '@/context/authContext'; import Spinner from '@/components/Spinner/Spinner.component'; import { REFETCH_CONFIG } from '@/constants/config'; +import { IDrive } from '@/types/item'; const EmpDrivePage = () => { const query = useRef(''); @@ -55,6 +56,7 @@ const EmpDrivePage = () => { canEdit: false, canView: false, }); + const [showInfo, setShowInfo] = useState(null); useEffect(() => { const getPerms = async () => { @@ -308,7 +310,7 @@ const EmpDrivePage = () => {
) : (
{ e.preventDefault(); e.stopPropagation(); @@ -344,72 +346,89 @@ const EmpDrivePage = () => { Any files that are being shared with you will appear here
))} - {folders && folders.length !== 0 && ( - <> -

Folders

-
- {folders.map((folder) => ( - { - e.preventDefault(); - e.stopPropagation(); - // Race condition - setTimeout(() => { - globalCm.current?.hide(e); - }, 0); - fileCm.current?.hide(e); - const { data: perm } = - await axiosClient.get( - `/shared/entries/${value}/permissions` - ); - setCurrentItemPerm({ - canEdit: perm.data.canEdit, - canView: perm.data.canView, - }); - setCurrentItem(value); - perm.data.canEdit && folderCm.current?.show(e); +
+
+ {folders && folders.length !== 0 && ( + <> +

Folders

+
- ))} -
- - )} - {files && files.length !== 0 && ( - <> -

Files

-
- {files.map((file) => ( - { - e.preventDefault(); - e.stopPropagation(); - // Race condition - setTimeout(() => { - globalCm.current?.hide(e); - }, 0); - folderCm.current?.hide(e); - const { data: perm } = - await axiosClient.get( - `/shared/entries/${value}/permissions` - ); - setCurrentItemPerm({ - canEdit: perm.data.canEdit, - canView: perm.data.canView, - }); - setCurrentItem(value); - perm.data.canView && fileCm.current?.show(e); + > + {folders.map((folder) => ( + { + e.preventDefault(); + e.stopPropagation(); + // Race condition + setTimeout(() => { + globalCm.current?.hide(e); + }, 0); + fileCm.current?.hide(e); + const { data: perm } = + await axiosClient.get( + `/shared/entries/${value}/permissions` + ); + setCurrentItemPerm({ + canEdit: perm.data.canEdit, + canView: perm.data.canView, + }); + setCurrentItem(value); + perm.data.canEdit && folderCm.current?.show(e); + }} + /> + ))} +
+ + )} + {files && files.length !== 0 && ( + <> +

Files

+
- ))} -
- - )} + > + {files.map((file) => ( + { + e.preventDefault(); + e.stopPropagation(); + // Race condition + setTimeout(() => { + globalCm.current?.hide(e); + }, 0); + folderCm.current?.hide(e); + const { data: perm } = + await axiosClient.get( + `/shared/entries/${value}/permissions` + ); + setCurrentItemPerm({ + canEdit: perm.data.canEdit, + canView: perm.data.canView, + }); + setCurrentItem(value); + perm.data.canView && fileCm.current?.show(e); + }} + /> + ))} +
+ + )} +
+ +
diff --git a/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx b/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx index dd487f9..4c3f258 100644 --- a/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx +++ b/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx @@ -10,13 +10,14 @@ import { PrimeIcons } from 'primereact/api'; import { MenuItem } from 'primereact/menuitem'; import { BaseResponse, GetDriveResponse } from '@/types/response'; import Folder from '@/components/Drive/Folder'; -import { File } from '@/components/Drive'; +import { DetailInfo, File } from '@/components/Drive'; import { Toast } from 'primereact/toast'; import { AxiosError } from 'axios'; import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog'; import Spinner from '@/components/Spinner/Spinner.component'; import { REFETCH_CONFIG } from '@/constants/config'; import Breadcrumbs from '@/components/Breadcrumbs/Breadcrumbs.component'; +import { IDrive } from '@/types/item'; const EmpDriveTrashPage = () => { const query = useRef(''); @@ -31,6 +32,7 @@ const EmpDriveTrashPage = () => { const currentPath = pathArr.slice(1).join('/') ? pathArr.join('/') : ''; const [currentItem, setCurrentItem] = useState(''); + const [showInfo, setShowInfo] = useState(null); const { data, isLoading } = useQuery( ['digital', 'bin', path, query.current], @@ -119,7 +121,7 @@ const EmpDriveTrashPage = () => {
) : ( -
+
{/*
{ Any recently deleted files or folder will appear here
)} - {folders && folders.length !== 0 && ( - <> -

Folders

-
- {folders?.map((folder) => ( - { - e.preventDefault(); - e.stopPropagation(); - cm.current?.show(e); - setCurrentItem(value); +
+
+ {folders && folders.length !== 0 && ( + <> +

Folders

+
- ))} -
- - )} - {files && files.length !== 0 && ( - <> -

Files

-
- {files?.map((file) => ( - { - e.preventDefault(); - e.stopPropagation(); - cm.current?.show(e); - setCurrentItem(value); + > + {folders?.map((folder) => ( + { + e.preventDefault(); + e.stopPropagation(); + cm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} + {files && files.length !== 0 && ( + <> +

Files

+
- ))} -
- - )} + > + {files?.map((file) => ( + { + e.preventDefault(); + e.stopPropagation(); + cm.current?.show(e); + setCurrentItem(value); + }} + /> + ))} +
+ + )} +
+ +
+
diff --git a/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx index 2759265..71e8da8 100644 --- a/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx +++ b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx @@ -23,6 +23,7 @@ const RequiredValues = { roomId: '', importReason: '', isPrivate: true, + // files: [] as File[], }; const NOT_REQUIRED = ['description', 'isPrivate']; From eae52d62c4bc1ab13edc691a71017366b653f959 Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Thu, 13 Jul 2023 13:16:14 +0700 Subject: [PATCH 17/18] Remove unused variables --- src/components/FileInput/FileInput.component.tsx | 9 ++++++++- .../ImportDocumentContainer/ImportDocumentContainer.tsx | 6 +++--- src/pages/admin/AdminRequestPage/AdminRequestPage.tsx | 2 -- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/FileInput/FileInput.component.tsx b/src/components/FileInput/FileInput.component.tsx index 19ef2d1..38148b4 100644 --- a/src/components/FileInput/FileInput.component.tsx +++ b/src/components/FileInput/FileInput.component.tsx @@ -1,6 +1,13 @@ import clsx from 'clsx'; import { PrimeIcons } from 'primereact/api'; -import { ChangeEvent, FC, InputHTMLAttributes, SetStateAction, useState, useRef } from 'react'; +import { + ChangeEvent, + FC, + InputHTMLAttributes, + SetStateAction, + // useState, + useRef, +} from 'react'; interface IFileInputProps extends InputHTMLAttributes { setData?: React.Dispatch>; diff --git a/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx b/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx index d0bd57c..ee6a636 100644 --- a/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx +++ b/src/containers/ImportDocumentContainer/ImportDocumentContainer.tsx @@ -18,8 +18,8 @@ import { AxiosError, AxiosResponse } from 'axios'; import useEmptyContainers from '@/hooks/useEmptyContainers'; import useDocumentTypes from '@/hooks/useDocumentTypes'; import TextareaWithLabel from '@/components/InputWithLabel/TextareaWithLabel.component'; -import FileInput from '@/components/FileInput/FileInput.component'; -import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; +// import FileInput from '@/components/FileInput/FileInput.component'; +// import ImagePreviewer from '@/components/ImagePreviewer/ImagePreviewer.component'; import { getUser } from '@/utils/services/getUser'; const RequiredValues = { @@ -68,7 +68,7 @@ const ImportDocumentContainer = () => { const { documentTypes, typesRefetch } = useDocumentTypes(); const [openScan, setOpenScan] = useState(false); - const [data, setData] = useState([]); + // const [data, setData] = useState([]); const onSubmit = async ( values: FormValues, diff --git a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx index 149260e..968839d 100644 --- a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx +++ b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx @@ -1,6 +1,5 @@ import Status from '@/components/Status/Status.component'; import Table from '@/components/Table/Table.component'; -import { AUTH_ROUTES } from '@/constants/routes'; import useNavigateSelect from '@/hooks/useNavigateSelect'; import usePagination from '@/hooks/usePagination'; import { IBorrowRequest } from '@/types/item'; @@ -9,7 +8,6 @@ import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; import { InputText } from 'primereact/inputtext'; import { useRef } from 'react'; -import { Link } from 'react-router-dom'; const AdminRequestPage = () => { const query = useRef(''); From dfcf9ddef897dd7888e15816741c148c4cf23ceb Mon Sep 17 00:00:00 2001 From: KhanhNG Date: Thu, 13 Jul 2023 13:38:49 +0700 Subject: [PATCH 18/18] add animation to page-wrapper --- src/components/ErrorBoundary/ErrorBoundary.tsx | 0 src/pages/DashboardPage/DashboardPage.tsx | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/components/ErrorBoundary/ErrorBoundary.tsx diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx index 85f1195..d13b87b 100644 --- a/src/pages/DashboardPage/DashboardPage.tsx +++ b/src/pages/DashboardPage/DashboardPage.tsx @@ -6,7 +6,10 @@ import { Outlet } from 'react-router'; const DashboardPage = ({ open = false }) => { return (