diff --git a/src/App.tsx b/src/App.tsx index 75d7d1d..34f50ac 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 ResetPage from './pages/CallbackPage/CallbackPage'; function App() { return ( @@ -17,6 +18,15 @@ function App() { /> } /> + } + unAuthComponent={} + /> + } + /> { + return ( +
+ {shared && ( + <> + / + Shared + + )} + {path === '/' ? ( + <> + / + {trashed ? 'Trashed' : 'Home'} + + ) : ( + pathArr.map((item, index) => { + const link = item ? pathArr.slice(0, index + 1).join('/') : '/'; + console.log(item); + return ( + + {!shared || link !== '/' ? / : null} + {index === pathArr.length - 1 ? ( + {item} + ) : ( + + {item || (trashed ? 'Trashed' : 'Home')} + + )} + + ); + }) + )} +
+ ); +}; + +export default Breadcrumbs; diff --git a/src/components/Drive/CreateFileModal.tsx b/src/components/Drive/CreateFileModal.tsx new file mode 100644 index 0000000..4d8a185 --- /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, + handleClose, + setFile, + file, +}: { + onCreateFile: (e: FormEvent) => void; + handleClose: () => 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..b63ca7b --- /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, + handleClose, +}: { + onCreateFolder: (e: FormEvent) => void; + handleClose: () => void; +}) => { + return ( +
e.stopPropagation()} + > +

Creating folder

+ +
+
+ + ); +}; + +export default CreateFolderModal; diff --git a/src/components/Drive/DetailInfo.tsx b/src/components/Drive/DetailInfo.tsx new file mode 100644 index 0000000..c3fbf2f --- /dev/null +++ b/src/components/Drive/DetailInfo.tsx @@ -0,0 +1,69 @@ +import { IDrive } from '@/types/item'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { TextOverflow } from '.'; +import { fileSizeFormatter } from '@/utils/formatter'; + +const DetailInfo = ({ + showInfo, + setShowInfo, +}: { + showInfo: IDrive | null; + setShowInfo: (value: IDrive | null) => void; +}) => { + return ( +
+
setShowInfo(null)} + > + +
+ {showInfo && ( + <> +

{showInfo.name}

+
+ +
+

+ {showInfo.isDirectory ? 'Folder' : 'File'} information +

+
+
Sizes
+
+ {fileSizeFormatter(showInfo.sizeInBytes || 0)} +
+
Location
+
{showInfo.path}
+
Owner
+ {showInfo.owner.email} +
Created
+
+ {new Date(showInfo.created).toLocaleDateString()} +
+
Created by
+ {showInfo.owner.email} +
Last modified
+
+ {showInfo.lastModified + ? new Date(showInfo.lastModified).toLocaleDateString() + : 'Never'} +
+
+ + )} +
+ ); +}; + +export default DetailInfo; diff --git a/src/components/Drive/File.tsx b/src/components/Drive/File.tsx new file mode 100644 index 0000000..c59fccb --- /dev/null +++ b/src/components/Drive/File.tsx @@ -0,0 +1,48 @@ +import { IDrive } from '@/types/item'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { MouseEvent } from 'react'; + +const File = ({ + // shared = false, + file, + onContextMenu, + showInfo, +}: // trashed = false, +{ + trashed?: boolean; + shared?: boolean; + file: IDrive; + showInfo?: (file: IDrive | null) => void; + onContextMenu: (value: string, e: MouseEvent, type: 'file') => void; +}) => { + return ( + <> +
{ + onContextMenu(file.id, e, 'file'); + }} + onClick={() => { + showInfo && showInfo(file); + }} + className='group w-full cursor-pointer max-w-[160px]' + > +
+
+ +
+

+ {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..69240a9 --- /dev/null +++ b/src/components/Drive/Folder.tsx @@ -0,0 +1,85 @@ +import { AUTH_ROUTES } from '@/constants/routes'; +import { IDrive } from '@/types/item'; +import clsx from 'clsx'; +import { PrimeIcons } from 'primereact/api'; +import { useState, MouseEvent, useRef } from 'react'; +import { useNavigate } from 'react-router'; + +const Folder = ({ + currentPath, + folder, + shared = false, + trashed = false, + showInfo, + onContextMenu, +}: { + currentPath: string; + folder: IDrive; + shared?: boolean; + trashed?: boolean; + showInfo?: (folder: IDrive | null) => void; + onContextMenu: (value: string, e: MouseEvent, type: 'folder') => void; +}) => { + const [hover, setHover] = useState(false); + const navigate = useNavigate(); + + const link = `${ + shared ? AUTH_ROUTES.DRIVE_SHARED : trashed ? AUTH_ROUTES.DRIVE_TRASH : AUTH_ROUTES.DRIVE + }?path=${encodeURIComponent(currentPath)}${encodeURIComponent( + `${shared ? folder.id : `/${folder.name}`}` + )}`; + + const doubleClick = useRef(false); + + const handleDoubleClick = () => { + if (doubleClick.current) { + showInfo && showInfo(null); + navigate(link); + } else { + showInfo && showInfo(folder); + doubleClick.current = true; + setTimeout(() => { + doubleClick.current = false; + }, 300); + } + }; + + 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..5043f46 --- /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, + handleClose, +}: { + onRename: (e: FormEvent) => void; + handleClose: () => 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..0858f2d --- /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, + handleClose, + users, + sharedUsers, +}: { + onShare: ( + e: FormEvent, + perms: { userId: string; canEdit: boolean; canView: boolean; expiryDate?: Date } + ) => void; + handleClose: () => 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}
+
+
+
+ ))} +
@@ -55,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 ( -
{openScan && ( diff --git a/src/containers/SignInFormContainer/SignInFormContainer.tsx b/src/containers/SignInFormContainer/SignInFormContainer.tsx index 163e512..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 { 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 } = {}; @@ -56,13 +58,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, @@ -70,12 +72,24 @@ const SignInForm = () => { localStorage.setItem('user', JSON.stringify(user)); } 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, - }); + 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 token = axiosError.response?.data.data.token; + if (token) { + navigate(`/reset`, { + state: token, + }); + return; + } + const message = axiosError.response?.data.message || 'Bad request'; + setErrors({ + error: message, + }); + } } finally { setSubmitting(false); } 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/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..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); @@ -49,7 +51,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), @@ -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/hooks/useRooms.tsx b/src/hooks/useRooms.tsx index 9ac3973..c3ce8cb 100644 --- a/src/hooks/useRooms.tsx +++ b/src/hooks/useRooms.tsx @@ -1,22 +1,54 @@ -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, + })) + : []; return { rooms, diff --git a/src/index.css b/src/index.css index 4db2856..5a2f970 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,31 +61,54 @@ body { box-shadow: none !important; } +.p-dropdown-filter-container > .p-inputtext { + color: initial; +} + .p-invalid { border-color: var(--action) !important; } -#password { +#password, #confirmPassword { width: 100%; } /* 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); + background: var(--secondary); +} + +.p-dialog .p-dialog-footer, +.p-dialog .p-dialog-header, +.p-dialog .p-dialog-content { + background: var(--layer-700); +} + +.p-toast .p-toast-message .p-toast-message-content { + @apply border-red-600; +} + +.p-confirm-dialog-message { + color: white; +} + +.page-wrapper > * { + width: 100%; + flex: 1; } \ No newline at end of file diff --git a/src/pages/CallbackPage/CallbackPage.tsx b/src/pages/CallbackPage/CallbackPage.tsx new file mode 100644 index 0000000..077760a --- /dev/null +++ b/src/pages/CallbackPage/CallbackPage.tsx @@ -0,0 +1,98 @@ +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: '', + confirmPassword: '', +}; + +type FormValues = typeof initialValues; + +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 < 5) { + errors.password = 'Password must be at least 5 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', { + newPassword: values.password, + confirmPassword: values.confirmPassword, + token, + }); + console.log(response); + navigate(UNAUTH_ROUTES.AUTH, { + replace: true, + }); + } catch (error) { + console.log(error); + } + }; + + return ( + + {({ values, touched, errors, handleBlur, handleChange, handleSubmit }) => ( +
+

Reset Password

+ + + -
diff --git a/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx b/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx index f872167..55489a8 100644 --- a/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx +++ b/src/pages/admin/AdminEmployeeCreatePage/AdminEmployeeCreatePage.tsx @@ -83,7 +83,7 @@ const AdminEmployeeCreatePage = () => { isValid, isSubmitting, }) => ( - + { 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,32 @@ const AdminEmployeeDetailPage = () => { role, email, username, - // isActive, + isActive, + isActivated, 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; @@ -113,7 +114,7 @@ const AdminEmployeeDetailPage = () => { isValid, isSubmitting, }) => ( -
+
{
- + {qr ? ( ) : (
)} -
+
{editMode ? (
+ {error &&
{error}
}
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' }); @@ -89,15 +90,16 @@ const AdminFolderDetailPage = () => { const onToggleAvailability = async () => { try { - if (isAvailable) { - await axiosClient.put(`/folders/disable/${folderId}`); - } else { - await axiosClient.put(`/folders/enable/${folderId}`); - } + await axiosClient.put(`/folders/${folderId}`, { + name: folderName, + description, + capacity, + isAvailable: !isAvailable, + }); queryClient.invalidateQueries('folders'); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -129,14 +131,25 @@ const AdminFolderDetailPage = () => { setEditMode(false); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); + } + }; + + 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 || 'Bad request'); } }; return (
-

+

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

+ +
+ +
+
+
+ + + ( +
+ {item.message} +
+ )} + sortable + /> + + + + +
+
+ + ); +}; + +export default AdminLogPage; 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 870e807..968839d 100644 --- a/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx +++ b/src/pages/admin/AdminRequestPage/AdminRequestPage.tsx @@ -4,12 +4,18 @@ 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'; const AdminRequestPage = () => { - const { getPaginatedTableProps } = usePagination({ + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ key: 'requests', - url: '/borrows', + url: '/documents/borrows', + query: query.current, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); @@ -17,6 +23,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 791c27a..df2c385 100644 --- a/src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx +++ b/src/pages/admin/AdminRoomDetailPage/AdminRoomDetailPage.tsx @@ -9,10 +9,10 @@ import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; import { useQuery, useQueryClient } from 'react-query'; -import { Link, useParams } from 'react-router-dom'; +import { Link, useNavigate, useParams } from 'react-router-dom'; import { useState } from 'react'; import useNavigateSelect from '@/hooks/useNavigateSelect'; -import { IFolder } from '@/types/item'; +import { ILocker } from '@/types/item'; import Status from '@/components/Status/Status.component'; import { AxiosError } from 'axios'; import ErrorTemplate from '@/components/ErrorTemplate/ErrorTemplate.component'; @@ -27,6 +27,7 @@ const AdminRoomDetailPage = () => { 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,15 +81,16 @@ 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; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -120,14 +122,25 @@ 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'); + } + }; + + 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 || 'Bad request'); } }; 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..18822bc 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 || 'Bad request'); + } + }; + + 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..7aeff12 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -15,3 +15,9 @@ 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'; +export { default as AdminLogPage } from './AdminLogPage/AdminLogPage'; diff --git a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx index 4f3020b..a1999e9 100644 --- a/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx +++ b/src/pages/emp/EmpDashboardPage/EmpDashboardPage.tsx @@ -1,28 +1,43 @@ 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 { GetDocumentByIdResponse, GetImportsResponse, GetRequestsResponse } from '@/types/response'; import { REFETCH_CONFIG } from '@/constants/config'; import { SkeletonCard } from '@/components/Skeleton'; 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 roomId = user?.department.roomId; - const { data: requests, isLoading: isRequestsLoading } = useQuery( ['requests', 'recent'], async () => ( - await axiosClient.get('/borrows/employees', { + await axiosClient.get('/documents/borrows', { + params: { + employeeId: user?.id, + sortOrder: 'desc', + size: 4, + page: 1, + }, + }) + ).data, + { + ...REFETCH_CONFIG, + } + ); + const { data: imports, isLoading: isImportsLoading } = useQuery( + ['imports', 'recent'], + async () => + ( + await axiosClient.get('/documents/import-requests', { params: { - roomId, + employeeId: user?.id, sortOrder: 'desc', size: 4, page: 1, @@ -40,29 +55,25 @@ 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, })) ); 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 ? ( @@ -87,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/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx index a824c11..613c540 100644 --- a/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx +++ b/src/pages/emp/EmpDocumentDetailPage/EmpDocumentDetailPage.tsx @@ -1,163 +1,480 @@ 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, + GetUsersResponse, +} from '@/types/response'; import axiosClient from '@/utils/axiosClient'; import { Button } from 'primereact/button'; -import { useState } 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 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'; +import CustomDropdown from '@/components/Dropdown/Dropdown.component'; 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, isLoading } = useQuery( + const { + data, + 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 ; + const { data: permission } = useQuery( + ['documents', documentId, user?.id], + async () => + (await axiosClient.get(`/documents/${documentId}/permissions`)).data + ); - const { - title, - documentType, - folder: { - id: folderId, - name: folderName, - locker: { id: lockerId, name: lockerName }, + const { data: users } = useQuery( + ['users'], + async () => + ( + await axiosClient.get('/users/employees', { + params: { + pageSize: 100, + }, + }) + ).data + ); + + useEffect(() => { + const renderQr = async () => { + 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(); + }, [data, permission]); + + if (isLoading) return ; + + if ( + (axiosError as AxiosError)?.response?.status === 404 || + !data || + (data.data.importer.id !== user?.id && (data.data.isPrivate && perms.employeeId !== user?.id)) + ) + return ; + + 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 15663db..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', - 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

- +
{ + 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(''); + const [showInfo, setShowInfo] = useState(null); + + const { data, isLoading } = useQuery( + ['digital', 'private', path, query.current], + async () => + ( + await axiosClient.get(`/entries?EntryPath=${encodeURIComponent(path)}`, { + params: { + searchTerm: query.current, + size: 100, + }, + }) + ).data, + REFETCH_CONFIG + ); + + const { data: users } = useQuery( + ['digital', 'private', 'users'], + async () => + ( + await axiosClient.get('/users/employees', { + params: { + pageSize: 100, + }, + }) + ).data + ); + + const { data: sharedUsers } = useQuery( + ['digital', 'private', 'sharedUsers', currentItem], + async () => + ( + await axiosClient.get( + `/shared/entries/${currentItem}/shared-users`, + { + params: { + pageSize: 100, + }, + } + ) + ).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[] = [ + { + 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, + }, + ]; + + const folderItems: MenuItem[] = [ + { + 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('Path', currentPath || '/'); + formData.set('isDirectory', 'true'); + try { + await axiosClient.post('/entries', 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('Path', currentPath || '/'); + formData.set('File', file); + formData.set('isDirectory', 'false'); + try { + await axiosClient.post('/entries', 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 ( + <> + {isLoading ? ( +
+ +
+ ) : ( + <> +
{ + globalCm.current?.show(e); + fileCm.current?.hide(e); + folderCm.current?.hide(e); + }} + > + {/*
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> + +
*/} + + {!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); + }} + /> + ))} +
+ + )} +
+ +
+ + + + +
+ + )} + {modal && ( + + {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} + handleClose={closeModals} + /> + )} + + )} + + + ); +}; + +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..8a1815c --- /dev/null +++ b/src/pages/emp/EmpDriveSharedPage/EmpDriveSharedPage.tsx @@ -0,0 +1,481 @@ +/* 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, DetailInfo, 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'; +import Spinner from '@/components/Spinner/Spinner.component'; +import { REFETCH_CONFIG } from '@/constants/config'; +import { IDrive } from '@/types/item'; + +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, + }); + const [currentItemPerm, setCurrentItemPerm] = useState({ + canEdit: false, + canView: false, + }); + const [showInfo, setShowInfo] = useState(null); + + useEffect(() => { + const getPerms = async () => { + if (!path) return; + const { data } = await axiosClient.get( + `/shared/entries/${path}/permissions` + ); + setCurrentPathPerm({ + canEdit: data.data.canEdit, + canView: data.data.canView, + }); + }; + getPerms(); + }, [path, setCurrentPathPerm]); + + const { data, isLoading } = useQuery( + ['digital', 'shared', path, query.current], + async () => + ( + await axiosClient.get( + `/shared/entries?entryId=${encodeURIComponent(path)}`, + { + params: { + searchTerm: query.current, + pageSize: 100, + }, + } + ) + ).data, + REFETCH_CONFIG + ); + + const { data: users } = useQuery( + ['digital', 'shared', 'users'], + async () => (await axiosClient.get('/users/employees')).data + ); + + const { data: sharedUsers } = useQuery( + ['digital', 'shared', 'sharedUsers', currentItem], + async () => + ( + await axiosClient.get( + `/shared/entries/${currentItem}/shared-users` + ) + ).data, + { + enabled: !!currentItem, + } + ); + + const { data: owner } = useQuery( + ['digital', 'shared', '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', 'shared', 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[] = currentItemPerm?.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[] = currentItemPerm?.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', 'shared', 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', 'shared', 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', 'shared', 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', 'shared']); + closeModals(); + } catch (error) { + onError(error as AxiosError); + } + }; + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( +
{ + e.preventDefault(); + e.stopPropagation(); + 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)} + /> + +
*/} + + {!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); + }} + /> + ))} +
+ + )} +
+ +
+ + + + +
+ )} + {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} + handleClose={closeModals} + /> + )} + + )} + + + ); +}; + +export default EmpDrivePage; diff --git a/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx b/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx new file mode 100644 index 0000000..4c3f258 --- /dev/null +++ b/src/pages/emp/EmpDriveTrashPage/EmpDriveTrashPage.tsx @@ -0,0 +1,216 @@ +/* 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 { 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(''); + 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 [showInfo, setShowInfo] = useState(null); + + 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/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx new file mode 100644 index 0000000..71e8da8 --- /dev/null +++ b/src/pages/emp/EmpImportCreatePage/EmpImportCreatePage.tsx @@ -0,0 +1,229 @@ +/* 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, + // files: [] as File[], +}; + +const NOT_REQUIRED = ['description', 'isPrivate']; + +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..3fb57f2 --- /dev/null +++ b/src/pages/emp/EmpImportPage/EmpImportPage.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 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 { 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/EmpProfilePage/EmpProfilePage.tsx b/src/pages/emp/EmpProfilePage/EmpProfilePage.tsx new file mode 100644 index 0000000..e025c5e --- /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 = () => { /> {
{ /> { 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 queryClient = useQueryClient(); + + useEffect(() => { + const renderQr = async () => { + const { documentId } = data?.data || { documentId: '' }; + if (!documentId) return; + const qrCode = await QRCode.toDataURL(documentId); + setQr(qrCode); + }; + renderQr(); + }, [data]); const { documentId } = data ? data.data : { documentId: '' }; @@ -51,7 +71,16 @@ const EmpRequestDetailPage = () => { if (!requestId) return ; - if (!data || !document || isLoading) return ; + if (isLoading) return ; + if (!document || !data) + return ( + + ); const { title, @@ -62,70 +91,162 @@ const EmpRequestDetailPage = () => { }, } = document.data; - const { status, reason, borrowTime, dueTime } = data.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 { - await axiosClient.post(`/borrows/cancel/${requestId}`); + await axiosClient.post(`/documents/borrows/cancel/${requestId}`); await refetchRequest(); } catch (error) { console.log(error); } }; + 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}`, { + borrowFrom: dates[0].toISOString(), + borrowTo: dates[1].toISOString(), + reason: reason, + }); + queryClient.invalidateQueries(['requests']); + } catch (error) { + const axiosError = error as AxiosError; + setFieldError('id', axiosError.response?.data?.message || 'Bad request'); + } + }; + return ( -
-
- - - -
- - + + {({ + values, + touched, + errors, + handleChange, + handleBlur, + resetForm, + submitForm, + isValid, + isSubmitting, + }) => ( +
+
+ + + +
+ + +
+
+ + + +
+ +
+
- - - - -
- - +
+ + {qr ? ( + + ) : ( +
+ )} + {status === REQUEST_STATUS.Pending.status && ( +
-
-
-
- - {qr ? ( - - ) : ( -
- )} - {NO_ACTIONS.indexOf(status) === -1 && ( -
-
+
+ )} + ); }; diff --git a/src/pages/emp/EmpRequestPage/EmpRequestPage.tsx b/src/pages/emp/EmpRequestPage/EmpRequestPage.tsx index 275eaca..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: '/borrows/employees', + 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/emp/index.ts b/src/pages/emp/index.ts index cab9749..68c0960 100644 --- a/src/pages/emp/index.ts +++ b/src/pages/emp/index.ts @@ -4,3 +4,6 @@ export { default as EmpDocumentDetailPage } from './EmpDocumentDetailPage/EmpDoc export { default as EmpRequestPage } from './EmpRequestPage/EmpRequestPage'; export { default as EmpRequestDetailPage } from './EmpRequestDetailPage/EmpRequestDetailPage'; export { default as EmpRequestCreatePage } from './EmpRequestCreatePage/EmpRequestCreatePage'; +export { default as EmpImportCreatePage } from './EmpImportCreatePage/EmpImportCreatePage'; +export { default as EmpImportDetailPage } from './EmpImportDetailPage/EmpImportDetailPage'; +export { default as EmpImportPage } from './EmpImportPage/EmpImportPage'; diff --git a/src/pages/staff/StaffDashboardPage/StaffDashboardPage.tsx b/src/pages/staff/StaffDashboardPage/StaffDashboardPage.tsx index 37a9e10..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'; @@ -15,7 +20,26 @@ import Status from '@/components/Status/Status.component'; const StaffDashboardPage = () => { const { user } = useContext(AuthContext); - const roomId = user?.department.roomId; + 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'], @@ -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 > @@ -90,7 +129,7 @@ const StaffDashboardPage = () => {
{isLockerLoading ? ( [...Array(3)].map((_, index) => ) - ) : lockers ? ( + ) : lockers && lockers.data.items.length !== 0 ? ( lockers.data.items.map((locker) => ( { )) ) : ( -
No lockers
+ No lockers )}
@@ -127,7 +166,7 @@ const StaffDashboardPage = () => {
{isFolderLoading ? ( [...Array(3)].map((_, index) => ) - ) : folders ? ( + ) : folders && folders.data.items.length !== 0 ? ( folders.data.items.map((folder) => ( { )) ) : ( -
No folders
+ No folders )}
@@ -164,23 +203,23 @@ const StaffDashboardPage = () => {
{isDocumentLoading ? ( [...Array(3)].map((_, index) => ) - ) : documents ? ( + ) : documents && documents.data.items.length !== 0 ? ( documents.data.items.map((document) => ( - +

{document.title}

-

{document.folder.name}

+

{document.folder?.name}

Type: {document.documentType}

)) ) : ( -
No documents
+ No documents )}
diff --git a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx index 6679001..1e4c1b4 100644 --- a/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx +++ b/src/pages/staff/StaffDocumentDetailPage/StaffDocumentDetailPage.tsx @@ -4,13 +4,13 @@ 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'; 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'; @@ -25,32 +25,42 @@ 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) return ; - const { - title, - folder: { - id: folderId, - name: folderName, - locker: { id: lockerId, name: lockerName }, + const { title } = data.data; + + const folder = data.data.folder || { + id: '', + name: '', + locker: { + id: '', + name: '', }, - } = data.data; + }; + + const { + id: folderId, + name: folderName, + locker: { id: lockerId, name: lockerName } = {}, + } = folder; - const initialValues = data.data; + const initialValues = { ...data.data, folder }; type FormValues = typeof initialValues; @@ -65,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'); } }; @@ -81,15 +91,23 @@ const StaffDocumentDetailPage = () => { return (
-

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

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

@@ -110,7 +128,7 @@ const StaffDocumentDetailPage = () => {
-
+ {/*
{ className='self-end bg-primary rounded-lg h-11' type='button' /> -
+
*/} { ) : (
)} -
+
{editMode ? (
- + {/* { ]} /> - + */}
)} 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..737f282 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, @@ -91,15 +91,16 @@ const StaffFolderDetailPage = () => { const onToggleAvailability = async () => { try { - if (isAvailable) { - await axiosClient.put(`/folders/disable/${folderId}`); - } else { - await axiosClient.put(`/folders/enable/${folderId}`); - } + await axiosClient.put(`/folders/${folderId}`, { + name: folderName, + description, + capacity, + isAvailable: !isAvailable, + }); queryClient.invalidateQueries('folders'); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; @@ -131,14 +132,14 @@ const StaffFolderDetailPage = () => { setEditMode(false); } catch (error) { const axiosError = error as AxiosError; - setError(axiosError.response?.data.message || 'Something went wrong'); + setError(axiosError.response?.data.message || 'Bad request'); } }; 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..f60718c 100644 --- a/src/pages/staff/StaffImportPage/StaffImportPage.tsx +++ b/src/pages/staff/StaffImportPage/StaffImportPage.tsx @@ -1,12 +1,77 @@ -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..8faee68 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, @@ -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,14 +130,14 @@ 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'); } }; 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..f69b487 100644 --- a/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx +++ b/src/pages/staff/StaffRequestDetailPage/StaffRequestDetailPage.tsx @@ -1,6 +1,7 @@ /* eslint-disable no-mixed-spaces-and-tabs */ 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 +13,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'; @@ -20,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, @@ -29,9 +31,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, } @@ -70,27 +75,33 @@ const StaffRequestDetailPage = () => { const { id: employeeId, lastName, firstName } = employee.data; - const { borrowTime, dueTime, reason, status } = data.data; + const { borrowTime, dueTime, borrowReason, status, staffReason } = 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; - const message = axiosError.response?.data.message || 'Something went wrong'; + const message = axiosError.response?.data.message || 'Bad request'; console.log(error); setError(message); } }; - 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; - const message = axiosError.response?.data.message || 'Something went wrong'; + const message = axiosError.response?.data.message || 'Bad request'; console.log(error); setError(message); } @@ -98,11 +109,23 @@ 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; - 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); } @@ -116,8 +139,8 @@ const StaffRequestDetailPage = () => {
- - + +
@@ -131,22 +154,38 @@ const StaffRequestDetailPage = () => { - + + {staffReason && }
{NO_ACTIONS.indexOf(status) !== -1 ? null : status === - REQUEST_STATUS.Approved.status ? ( -
+ {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..bbc0e4e 100644 --- a/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx +++ b/src/pages/staff/StaffRequestPage/StaffRequestPage.tsx @@ -1,15 +1,23 @@ 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 { Button } from 'primereact/button'; import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { useContext, useRef } from 'react'; const StaffRequestPage = () => { - const { getPaginatedTableProps } = usePagination({ + const { user } = useContext(AuthContext); + const query = useRef(''); + + const { getPaginatedTableProps, refetch } = usePagination({ key: 'requests', - url: '/borrows/staffs', + url: `/documents/borrows?roomId=${user?.roomId}`, + query: query.current, }); const { getNavigateOnSelectProps } = useNavigateSelect({ route: 'REQUESTS' }); @@ -17,6 +25,22 @@ const StaffRequestPage = () => { return (

Pending requests

+
+
{ + e.preventDefault(); + await refetch(); + }} + > + (query.current = e.target.value)} + /> +
diff --git a/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx b/src/pages/staff/StaffReturnPage/StaffReturnPage.tsx index 5f33a3b..6566443 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, @@ -66,13 +72,13 @@ 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'); } }; 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,13 +89,13 @@ 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; 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); } @@ -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..823116a 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,58 @@ 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: 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; +} + +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 41c834c..0ce5be7 100644 --- a/src/types/response.ts +++ b/src/types/response.ts @@ -1,4 +1,16 @@ -import { IBorrowRequest, IDepartment, IDocument, IFolder, ILocker, IRoom, IUser } from './item'; +import { + IBorrowRequest, + IDepartment, + IDocument, + IDrive, + IDrivePermission, + IFolder, + IImportRequest, + ILocker, + IPermission, + IRoom, + IUser, +} from './item'; export type BaseResponse = { data: T; @@ -16,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< { @@ -54,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>; @@ -67,3 +81,21 @@ export type GetFolderByIdResponse = BaseResponse; export type GetRoomsResponse = BaseResponse<{ items: IRoom[] } & PaginationResponse>; export type GetRoomByIdResponse = BaseResponse; + +export type GetDepartmentByIdResponse = BaseResponse; + +export type GetImportByIdResponse = BaseResponse; + +export type GetImportsResponse = BaseResponse<{ items: IImportRequest[] } & PaginationResponse>; + +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 5cc6f75..e2c5e7d 100644 --- a/src/types/roles.ts +++ b/src/types/roles.ts @@ -13,6 +13,29 @@ 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', + }, + { + 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 +59,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', @@ -82,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', @@ -130,17 +148,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 +162,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: 'Requests', + path: AUTH_ROUTES.IMPORT_MANAGE, + icon: PrimeIcons.USERS, + }, + { + label: 'Manual', + path: AUTH_ROUTES.NEW_IMPORT, + icon: PrimeIcons.PLUS, + }, + ], + }, ], // Employee sidebar employee: [ @@ -157,6 +187,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', @@ -167,18 +202,49 @@ 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, }, { - type: 'group', - label: 'Borrowed docs', + 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', + }, + { + 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, + }, + ], + }, ], }; 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