Fix sort, order, search function

This commit is contained in:
2023-12-14 19:50:33 +07:00
parent ad0b2826e2
commit e2ff52d864
21 changed files with 546 additions and 719 deletions
@@ -57,7 +57,7 @@ const AppLayout = () => {
}, []);
return (
<UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable}}>
<UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable, dispatch}}>
<StyledAppLayout>
<Header accountName={account?.user_metadata.fullName} />
<Sidebar heading="Hotel Management" />
@@ -17,7 +17,12 @@ interface ISearch {
const Search = ({ setPlaceHolder }: ISearch) => {
const field = 'search';
const [searchParams, setSearchParams] = useSearchParams();
const [query, setQuery] = useState<Nullable<string>>(null);
const [query, setQuery] = useState<Nullable<string>>(
searchParams.get(field)
? searchParams.get(field)
: '',
);
const debounceValue = useDebounce<Nullable<string>>(query, 700);
useEffect(() => {
@@ -32,6 +37,7 @@ const Search = ({ setPlaceHolder }: ISearch) => {
<StyledSearch
onChange={(e) => setQuery(e.target.value)}
placeholder={setPlaceHolder}
value={query!}
/>
);
};
@@ -1,60 +0,0 @@
import { render } from '@testing-library/react';
// Components
import Table from '.';
// Types
import { IUser } from '@src/types/user';
interface ITableRow {
user: IUser;
}
describe('Table', () => {
const columnName = ['Column 1', 'Column 2'];
const tempUser: IUser[] = [
{
id: 1,
name: 'Nezumi',
phone: '123',
isBooked: true,
isDelete: true,
},
{
id: 2,
name: 'Loi Phan',
phone: '453',
isBooked: false,
isDelete: true,
},
];
const TableRow = ({ user }: ITableRow) => {
const {
id,
name,
phone,
isBooked
} = user;
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{phone}</div>
<div>{isBooked}</div>
</Table.Row>
);
};
const renderRow = (user: IUser) => <TableRow user={user} key={user.id} />;
const wrapper = render(
<Table columns="10% 35% 30% 15% 10%">
<Table.Header headerColumn={columnName} />
<Table.Body<IUser> data={tempUser} render={renderRow} />
</Table>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
+71 -37
View File
@@ -1,29 +1,38 @@
// Types
import { ColumnProps } from '@src/types/common';
import {ColumnProps} from '@src/types/common';
// Styled
import {
StyledBody, StyledFooter, StyledHeader, StyledRow, StyledTable
StyledBody,
StyledFooter,
StyledHeader,
StyledRow,
StyledTable,
} from './styled';
// Components
import Pagination from '../Pagination';
import { TbArrowNarrowRight } from 'react-icons/tb';
import { StyledTableOption } from '@src/pages/Room/styled.ts';
import {TbArrowNarrowRight} from 'react-icons/tb';
import {StyledTableOption} from '@src/pages/Room/styled.ts';
import OrderBy from '@src/components/OrderBy';
import { ORDERBY_OPTIONS, ROOM_PAGE } from '@src/constants/commons.ts';
import {ORDERBY_OPTIONS} from '@src/constants/commons.ts';
import SortBy from '@src/components/SortBy';
import Search from '@src/components/Search';
import Message from '@src/components/Message';
import Direction from '@src/commons/styles/Direction.ts';
import { Dispatch, SetStateAction } from 'react';
import {Dispatch, SetStateAction} from 'react';
const parseWidthString = (columnsWidth: number[]): string => {
let result: string = '';
const totalWidth = columnsWidth.reduce((partialSum, a) => partialSum + a, 0);
const totalWidth = columnsWidth.reduce((
partialSum,
a
) => partialSum + a, 0);
columnsWidth.forEach((width) => {
result += Math.round((width / totalWidth) * 100).toString() + '% ';
result += Math.round((
width / totalWidth
) * 100)
.toString() + '% ';
});
return result;
@@ -34,13 +43,16 @@ type Props<T> = {
rows: T[];
stateSelected: {
itemSelected: T | undefined;
setItemSelected: Dispatch<SetStateAction<T | undefined>>
setItemSelected: Dispatch<SetStateAction<T | undefined>>;
};
count?: number | null;
onRowClick?: (rowData: T) => void;
enabledOrder?: boolean;
enabledSort?: boolean;
enabledSearch?: boolean;
sortBy?: {
value: string;
label: string;
}[];
searchPlaceHolder?: string;
};
const Table = <T, >({
@@ -49,18 +61,24 @@ const Table = <T, >({
count,
onRowClick,
stateSelected,
enabledSort = false,
enabledSearch = false,
enabledOrder = false
sortBy,
searchPlaceHolder,
enabledOrder = false,
}: Props<T>) => {
const width = parseWidthString(columns.map((item) => item.width));
const { itemSelected, setItemSelected } = stateSelected;
const {itemSelected, setItemSelected} = stateSelected;
const headers = columns.map((column, index) => {
const headers = columns.map((
column,
index
) => {
return <div key={`headCell-${index}`}>{column.title}</div>;
});
const renderRows = rows.map((row, rowIndex) => {
const renderRows = rows.map((
row,
rowIndex
) => {
const handleRowClick = () => {
if (onRowClick) {
onRowClick(row);
@@ -73,45 +91,61 @@ const Table = <T, >({
key={`row-${rowIndex}`}
width={width}
onDoubleClick={handleRowClick}
className={JSON.stringify(itemSelected) === JSON.stringify(row)
className={
JSON.stringify(itemSelected) === JSON.stringify(row)
? 'selected'
: ''
}>
{columns.map((column, columnIndex) => {
}
>
{columns.map((
column,
columnIndex
) => {
const value = row[column.key as keyof typeof row] as string;
if (column.isDateValue) {
return (<div
style={{ display: 'flex', alignItems: 'center' }}
return (
<div
style={{display: 'flex', alignItems: 'center'}}
key={`cell-${columnIndex}`}
>
{value[0]} &nbsp; <TbArrowNarrowRight />
{value[0]} &nbsp; <TbArrowNarrowRight/>
&nbsp; {value[1]}
</div>);
</div>
);
}
return <div key={`cell-${columnIndex}`}>{value}</div>;
})}
</StyledRow>);
</StyledRow>
);
});
return (<Direction>
return (
<Direction>
<StyledTableOption>
{enabledOrder && <OrderBy options={ORDERBY_OPTIONS} />}
{enabledSort && <SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />}
{enabledSearch && <Search setPlaceHolder="Search by name..." />}
{enabledOrder && <OrderBy options={ORDERBY_OPTIONS}/>}
{Boolean(sortBy?.length) && <SortBy options={sortBy!}/>}
{Boolean(searchPlaceHolder) && <Search setPlaceHolder={searchPlaceHolder!}/>}
</StyledTableOption>
{rows && rows.length ? (<StyledTable>
{rows && rows.length &&
(
<StyledTable>
<StyledHeader width={width}>{headers}</StyledHeader>
<StyledBody>{renderRows}</StyledBody>
{Boolean(count) && (<StyledFooter>
<Pagination count={count!} />
</StyledFooter>)}
</StyledTable>) : (<Message>No data to show here!</Message>)}
</Direction>);
};
{Boolean(count) && (
<StyledFooter>
<Pagination count={count!}/>
</StyledFooter>
)}
</StyledTable>
)
}
</Direction>
);
}
export default Table;
@@ -26,6 +26,12 @@ const StyledBody = styled.div`
const StyledRow = styled(CommonRow)`
padding: 20px;
cursor: pointer;
&:hover {
background-color: var(--item-hover);
}
&.selected {
background-color: var(--item-selected);
}
+5
View File
@@ -29,7 +29,12 @@ const getDayDiff = (startDate: Date, endDate: Date): number => {
return 0;
}
const findItemInListById = <T,>(id: number, listItem: T[]): T | undefined => {
return listItem.find((item) => item["id" as keyof typeof item] === id);
}
export {
formatCurrency,
getDayDiff,
findItemInListById,
};
@@ -37,6 +37,7 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => {
usersAvailable,
dispatch
} = useUserRoomAvailable();
const { isCreating, createBooking } = useCreateBooking();
const { isUpdating, updateBooking } = useUpdateBooking();
const isLoading = isCreating || isUpdating;
@@ -1,138 +0,0 @@
import toast from 'react-hot-toast';
import { useCallback, useMemo } from 'react';
// Helpers
import { formatCurrency } from '@src/helpers/helper';
// Types
import { TBookingResponse } from '@src/types/booking';
// Components
import Modal from '@src/components/Modal';
import Table from '@src/components/Table';
import Menus from '@src/components/Menus';
import { RiEditBoxFill } from 'react-icons/ri';
import { TbArrowNarrowRight } from 'react-icons/tb';
import { ImExit } from 'react-icons/im';
import BookingForm from './BookingForm';
import ConfirmMessage from '@src/components/ConfirmMessage';
// Hooks
import useCheckOut from '@src/hooks/bookings/useCheckout';
// Constants
import { FORM } from '@src/constants/commons';
interface IBookingRow {
booking: TBookingResponse;
}
const BookingRow = ({ booking }: IBookingRow) => {
const { checkOutBooking } = useCheckOut();
const { id, users, startDate, endDate, rooms, amount, status } = booking;
const formattedPrice = useMemo(() => formatCurrency(amount), [amount]);
const renderEditBtn = useCallback(
(onOpenModal: () => void) => (
<Menus.Button
onClick={onOpenModal}
icon={<RiEditBoxFill />}
disabled={!status}
label={'Edit'}
/>
),
[status]
);
const renderCheckOutBtn = useCallback(
(onOpenModal: () => void) => (
<Menus.Button
onClick={onOpenModal}
icon={<ImExit />}
disabled={!status}
label={'Check out'}
/>
),
[status]
);
const handleClickCheckOutBtn = useCallback(() => {
if (booking.status) {
checkOutBooking({
idBooking: booking.id,
roomId: booking!.rooms!.id,
userId: booking!.users!.id,
});
} else {
toast.error('User already checkout!');
}
}, [booking, checkOutBooking]);
const renderRow = useCallback(
(onCloseModal: () => void) => (
<BookingForm
booking={booking}
key={booking.id}
onCloseModal={onCloseModal}
/>
),
[booking]
);
const renderConfirmMessage = useCallback(
(onCloseModal: () => void) => (
<ConfirmMessage
message={`Are you sure to checkout this user? '${booking.users?.name}'`}
onConfirm={handleClickCheckOutBtn}
onCloseModal={onCloseModal}
/>
),
[booking.users?.name, handleClickCheckOutBtn]
);
return (
<Table.Row>
<div>{users?.name}</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
{startDate} &nbsp; <TbArrowNarrowRight />
&nbsp; {endDate}
</div>
<div>{rooms?.name}</div>
<div>{formattedPrice}</div>
<div>{status ? 'Check in' : 'Check out'}</div>
<div>
<Modal>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={renderEditBtn}
/>
<Modal.Open
modalName={FORM.CHECKOUT}
renderChildren={renderCheckOutBtn}
/>
</Menus.List>
<Modal.Window
name={FORM.EDIT}
title="Edit Booking"
renderChildren={renderRow}
/>
<Modal.Window
name={FORM.CHECKOUT}
title="Checkout"
renderChildren={renderConfirmMessage}
/>
</Menus.Menu>
</Modal>
</div>
</Table.Row>
);
};
export default BookingRow;
@@ -1,96 +0,0 @@
// Components
import Table from '@src/components/Table';
import Message from '@src/components/Message';
import Search from '@src/components/Search';
// Styled
import { StyledOperationTable } from './styled';
import Direction from '@src/commons/styles/Direction';
import Spinner from '@src/commons/styles/Spinner';
// Hooks
import { useBookings } from '@src/hooks/bookings/useBookings';
// Types
import { ColumnProps } from '@src/types/common';
import { formatCurrency } from '@src/helpers/helper';
import { useItemSelect } from '@src/hooks/useItemSelected';
interface IBookingTable {
user: string;
date: string[];
room: string;
amount: string;
status: string;
onClick: () => void;
}
const BookingTable = () => {
const { dispatch } = useItemSelect();
const columns: ColumnProps[] = [
{
key: 'user',
title: 'User',
width: 10,
},
{
key: 'date',
title: 'Date',
width: 25,
isDateValue: true,
},
{
key: 'room',
title: 'Room',
width: 20,
},
{
key: 'amount',
title: 'Amount',
width: 15,
},
{
key: 'status',
title: 'Status',
width: 20,
},
];
const { isLoading, bookings, count } = useBookings();
const tempBookings = bookings?.map((booking) => ({
date: [booking.startDate, booking.endDate],
amount: formatCurrency(booking.amount),
room: booking.rooms!.name,
user: booking.users!.name,
status: booking.status ? 'Check in' : 'Check out',
onClick: () => {
dispatch!({type: 'setData', payload: booking})
},
}));
return (
<>
<Direction>
<StyledOperationTable>
<Search setPlaceHolder="Search by name..." />
</StyledOperationTable>
{isLoading && <Spinner />}
{tempBookings && tempBookings.length ? (
<Table<IBookingTable>
columns={columns}
rows={tempBookings}
count={count}
/>
) : (
!isLoading && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default BookingTable;
@@ -1,37 +0,0 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
// Types
import { TBookingResponse } from '@src/types/booking';
// Components
import BookingRow from '../BookingRow';
describe('BookingRow', () => {
const tempBooking: TBookingResponse = {
id: 1,
startDate: '2024-11-21',
endDate: '2024-11-22',
amount: 1200,
status: true,
rooms: {
id: 1,
name: 'Room 1',
},
users: {
id: 2,
name: 'Room 2',
},
};
const queryClient = new QueryClient();
const wrapper = render(
<QueryClientProvider client={queryClient}>
<BookingRow booking={tempBooking} />
</QueryClientProvider>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
@@ -1,22 +0,0 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
// Components
import BookingTable from '@src/pages/Booking/BookingTable';
describe('UserTable', () => {
const queryClient = new QueryClient();
const wrapper = render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<BookingTable />
</BrowserRouter>
</QueryClientProvider>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
+183 -16
View File
@@ -1,42 +1,209 @@
// Styled
import Direction from '@src/commons/styles/Direction';
import { StyledBooking, Title } from './styled';
import Button from '@src/commons/styles/Button';
// Components
import Modal from '@src/components/Modal';
import BookingTable from './BookingTable';
import BookingForm from './BookingForm';
import Modal from '@src/components/Modal';
import ButtonIcon from '@src/components/ButtonIcon';
import Direction from '@src/commons/styles/Direction';
import {FiEdit} from 'react-icons/fi';
import {MdOutlineAddCircleOutline} from 'react-icons/md';
import Table from '@src/components/Table';
// Constants
import { FORM } from '@src/constants/commons';
// Styled
import {ActionTable, StyledBooking, Title} from './styled';
import Spinner from '@src/commons/styles/Spinner.ts';
// Types
import {ColumnProps} from '@src/types/common.ts';
import {TBookingResponse} from '@src/types/booking.ts';
// Hooks
import {useBookings} from '@src/hooks/bookings/useBookings.ts';
import {useEffect, useState} from 'react';
import {FORM} from '@src/constants/commons.ts';
import {findItemInListById, formatCurrency} from '@src/helpers/helper.ts';
import ConfirmMessage from '@src/components/ConfirmMessage';
import useCheckOut from '@src/hooks/bookings/useCheckout.ts';
import toast from 'react-hot-toast';
import {ImExit} from "react-icons/im";
import Message from "@src/components/Message";
interface IBookingTable extends Omit<TBookingResponse, 'status' | 'amount'> {
amount: string;
status: string;
}
const Booking = () => {
const {checkOutBooking} = useCheckOut();
const [itemSelected, setItemSelected] = useState<IBookingTable>();
let bookingSelected: TBookingResponse | undefined;
const columns: ColumnProps[] = [
{
key: 'user',
title: 'User',
width: 10
},
{
key: 'date',
title: 'Date',
width: 25,
isDateValue: true
},
{
key: 'room',
title: 'Room',
width: 20
},
{
key: 'amount',
title: 'Amount',
width: 15
},
{
key: 'status',
title: 'Status',
width: 20
}
];
const {isLoading, bookings, count} = useBookings();
// Reset value when bookings changed.
useEffect(() => {
setItemSelected(undefined);
}, [bookings]);
const tempBookings = bookings?.map((booking) => (
{
...booking,
date: [booking.startDate, booking.endDate],
amount: formatCurrency(booking.amount),
room: booking.rooms!.name,
user: booking.users!.name,
status: booking.status
? 'Check in'
: 'Check out',
}
));
if (itemSelected && bookings) {
bookingSelected = findItemInListById<TBookingResponse>(itemSelected.id, bookings);
}
const handleClickCheckOutBtn = () => {
if (itemSelected && itemSelected.status) {
checkOutBooking({
idBooking: itemSelected.id,
roomId: itemSelected!.rooms!.id,
userId: itemSelected!.users!.id
});
} else {
toast.error('User already checkout!');
}
};
return (
<StyledBooking>
<Direction type="horizontal">
<Title>List Booking</Title>
<div style={{ display: 'flex', gap: '15px' }}>
<Modal>
<ActionTable>
<Modal.Open
modalName={FORM.BOOKING}
modalName={FORM.ROOM}
renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add booking</Button>
<ButtonIcon
icon={<MdOutlineAddCircleOutline/>}
text={'Add Booking'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'primary'}
iconColor={'white'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<FiEdit/>}
text={'Edit'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'success'}
iconColor={'white'}
disabled={!itemSelected || itemSelected?.status === 'Check out'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.CHECKOUT}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<ImExit />}
text={'Check out'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'danger'}
iconColor={'white'}
disabled={!itemSelected || itemSelected?.status === 'Check out'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Window
name={FORM.BOOKING}
name={FORM.ROOM}
title="Add form"
renderChildren={(onCloseModal) => (
<BookingForm onCloseModal={onCloseModal} />
<BookingForm onCloseModal={onCloseModal}/>
)}
/>
<Modal.Window
name={FORM.EDIT}
title="Edit Booking"
renderChildren={(onCloseModal) =>
<BookingForm booking={bookingSelected} onCloseModal={onCloseModal}/>
}/>
<Modal.Window
name={FORM.CHECKOUT}
title="Check Out"
renderChildren={(onCloseModal) =>
<ConfirmMessage
message={`Are you sure to check out this user? "${bookingSelected!.users!.name}"?`}
onConfirm={handleClickCheckOutBtn}
onCloseModal={onCloseModal}
/>
}/>
</ActionTable>
</Modal>
</div>
</Direction>
<BookingTable />
{isLoading && <Spinner/>}
{
tempBookings &&
Boolean(tempBookings.length) &&
<Table<IBookingTable>
columns={columns}
rows={tempBookings}
count={count}
searchPlaceHolder={'Search by name...'}
stateSelected={{
itemSelected,
setItemSelected
}}
onRowClick={(data) => {
setItemSelected(data);
}}/>
}
{tempBookings && Boolean(!tempBookings.length) && <Message>No data to show here!</Message>}
</StyledBooking>
);
};
+8 -2
View File
@@ -14,14 +14,20 @@ const Title = styled.h2`
text-transform: capitalize;
`;
const StyledOperationTable = styled.div`
const StyledTableOption = styled.div`
display: flex;
gap: 30px;
justify-content: flex-end;
`;
const ActionTable = styled.div`
display: flex;
gap: 30px;
`
export {
StyledBooking,
Title,
StyledOperationTable
StyledTableOption,
ActionTable
};
+61 -14
View File
@@ -19,13 +19,20 @@ import { IRoom } from '@src/types/room.ts';
// Hooks
import { useRooms } from '@src/hooks/rooms/useRooms.ts';
import { useEffect, useState } from 'react';
import {FORM, ROOM_PAGE} from '@src/constants/commons.ts';
import { findItemInListById } from '@src/helpers/helper.ts';
import ConfirmMessage from '@src/components/ConfirmMessage';
import { useSetIsDeleteRoom } from '@src/hooks/rooms/useSetIsDeleteRoom.ts';
import Message from "@src/components/Message";
interface IRoomTable extends Omit<IRoom, 'status'> {
status: string;
}
const Room = () => {
const { setIsDeleteRoom } = useSetIsDeleteRoom();
const [itemSelected, setItemSelected] = useState<IRoomTable>();
let roomSelected: IRoom | undefined;
const columns: ColumnProps[] = [
{
@@ -63,6 +70,10 @@ const Room = () => {
: 'Available'
}));
if (itemSelected && rooms) {
roomSelected = findItemInListById<IRoom>(itemSelected.id, rooms);
}
return (
<StyledRoom>
<Direction type="horizontal">
@@ -71,7 +82,7 @@ const Room = () => {
<Modal>
<ActionTable>
<Modal.Open
modalName="room-form"
modalName={FORM.ROOM}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<MdOutlineAddCircleOutline />}
@@ -84,14 +95,10 @@ const Room = () => {
/>
)}
/>
<Modal.Window
name="room-form"
title="Add form"
renderChildren={(onCloseModal) => (
<RoomForm onCloseModal={onCloseModal} />
)}
/>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<FiEdit />}
text={'Edit'}
@@ -99,8 +106,15 @@ const Room = () => {
fontSize={'var(--fs-sm)'}
variations={'success'}
iconColor={'white'}
disabled={Boolean(!itemSelected)}
disabled={!itemSelected}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.DELETE}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<FaRegTrashAlt />}
text={'Delete'}
@@ -108,8 +122,38 @@ const Room = () => {
fontSize={'var(--fs-sm)'}
variations={'danger'}
iconColor={'white'}
disabled={Boolean(!itemSelected)}
disabled={!itemSelected || itemSelected?.status === 'Available'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Window
name={FORM.ROOM}
title="Add form"
renderChildren={(onCloseModal) => (
<RoomForm onCloseModal={onCloseModal} />
)}
/>
<Modal.Window
name={FORM.EDIT}
title="Edit Room"
renderChildren={(onCloseModal) =>
<RoomForm room={roomSelected} onCloseModal={onCloseModal} />
} />
<Modal.Window
name={FORM.DELETE}
title="Delete Room"
renderChildren={(onCloseModal) =>
<ConfirmMessage
message={`Are you sure to delete ${roomSelected!.name}?`}
onConfirm={() => setIsDeleteRoom(roomSelected!.id)}
onCloseModal={onCloseModal}
/>
} />
</ActionTable>
</Modal>
</Direction>
@@ -123,16 +167,19 @@ const Room = () => {
columns={columns}
rows={tempRooms}
count={count}
enabledSort={true}
sortBy={ROOM_PAGE.SORTBY_OPTIONS}
enabledOrder={true}
enabledSearch={true}
searchPlaceHolder={'Search by name...'}
stateSelected={{
itemSelected,
setItemSelected,
setItemSelected
}}
onRowClick={(data) => {
setItemSelected(data);
}} />}
}} />
}
{tempRooms && Boolean(!tempRooms.length) && <Message>No data to show here!</Message>}
</StyledRoom>
);
};
-111
View File
@@ -1,111 +0,0 @@
import { useCallback } from 'react';
// Components
import { RiEditBoxFill } from 'react-icons/ri';
import Modal from '@src/components/Modal';
import Table from '@src/components/Table';
import Menus from '@src/components/Menus';
import UserForm from './UserForm';
import ConfirmMessage from '@src/components/ConfirmMessage';
import { HiTrash } from 'react-icons/hi';
// Types
import { IUser } from '@src/types/user';
// Constants
import { FORM } from '@src/constants/commons';
// Hooks
import { useIsDeleteUser } from '@src/hooks/users/useSetIsDeleteUser';
interface IUserRow {
user: IUser;
}
const UserRow = ({ user }: IUserRow) => {
const { id, name, phone, isBooked } = user;
const { setIsDeleteUser } = useIsDeleteUser();
const renderEditBtn = useCallback(
(onOpenModal: () => void) => (
<Menus.Button
onClick={onOpenModal}
icon={<RiEditBoxFill />}
label={'Edit'}
/>
),
[]
);
const renderDeleteBtn = useCallback(
(onOpenModal: () => void) => (
<Menus.Button
icon={<HiTrash />}
onClick={onOpenModal}
disabled={isBooked}
label={'Delete'}
/>
),
[isBooked]
);
const renderRow = useCallback(
(onCloseModal: () => void) => (
<UserForm user={user} onCloseModal={onCloseModal} />
),
[user]
);
const renderConfirmMessage = useCallback(
(onCloseModal: () => void) => (
<ConfirmMessage
message={`Are you sure to delete ${name}?`}
onConfirm={() => setIsDeleteUser(id)}
onCloseModal={onCloseModal}
/>
),
[id, name, setIsDeleteUser]
);
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{phone}</div>
<div>{isBooked ? 'Yes' : 'No'}</div>
<div>
<Modal>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={renderEditBtn}
/>
<Modal.Open
modalName={FORM.DELETE}
renderChildren={renderDeleteBtn}
/>
</Menus.List>
<Modal.Window
name={FORM.EDIT}
title="Edit user"
renderChildren={renderRow}
/>
<Modal.Window
name={FORM.DELETE}
title="Delete User"
renderChildren={renderConfirmMessage}
/>
</Menus.Menu>
</Modal>
</div>
</Table.Row>
);
};
export default UserRow;
@@ -1,81 +0,0 @@
// Components
import Table from '@src/components/Table';
import Message from '@src/components/Message';
import Search from '@src/components/Search';
import SortBy from '@src/components/SortBy';
import OrderBy from '@src/components/OrderBy';
// Constants
import { ORDERBY_OPTIONS, USER_PAGE } from '@src/constants/commons';
// Styled
import Direction from '@src/commons/styles/Direction';
import { StyledOperationTable } from './styled';
import Spinner from '@src/commons/styles/Spinner';
// Hooks
import { useUsers } from '@src/hooks/users/useUsers';
import { ColumnProps } from '@src/types/common';
import { IUser } from '@src/types/user';
interface IUserTable extends Omit<IUser, 'isBooked'>{
isBooked: string;
onClick: () => void;
}
const UserTable = () => {
const columns: ColumnProps[] = [
{
key: 'id',
title: 'Id',
width: 10,
},
{
key: 'name',
title: 'Name',
width: 35,
},
{
key: 'phone',
title: 'Phone',
width: 30,
},
{
key: 'isBooked',
title: 'Is Booked',
width: 20,
},
];
const { isLoading, users, count } = useUsers();
const tempUsers = users?.map((user) => ({
...user,
isBooked: user.isBooked
? 'Yes'
: 'No',
onClick: () => console.log(user),
}));
return (
<>
<Direction>
<StyledOperationTable>
<OrderBy options={ORDERBY_OPTIONS} />
<SortBy options={USER_PAGE.SORTBY_OPTIONS} />
<Search setPlaceHolder="Search by phone..." />
</StyledOperationTable>
{isLoading && <Spinner />}
{tempUsers && tempUsers.length ? (
<Table<IUserTable> columns={columns} rows={tempUsers} count={count}/>
) : (
!isLoading && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default UserTable;
@@ -1,28 +0,0 @@
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Types
import { IUser } from '@src/types/user';
// Components
import UserRow from '../UserRow';
describe('UserRow', () => {
const tempUser: IUser = {
id: 1,
name: 'Temp Room',
phone: '0324421232',
isBooked: false,
isDelete: true,
};
const queryClient = new QueryClient();
const wrapper = render(
<QueryClientProvider client={queryClient}>
<UserRow user={tempUser} />
</QueryClientProvider>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
@@ -1,22 +0,0 @@
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import UserTable from '@src/pages/User/UserTable.tsx';
// Components
describe('UserTable', () => {
const queryClient = new QueryClient();
const wrapper = render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<UserTable />
</BrowserRouter>
</QueryClientProvider>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
+159 -16
View File
@@ -1,45 +1,188 @@
// Components
import UserTable from './UserTable';
import Modal from '@src/components/Modal';
import UserForm from './UserForm';
import Modal from '@src/components/Modal';
import ButtonIcon from '@src/components/ButtonIcon';
import Direction from '@src/commons/styles/Direction';
import {FiEdit} from 'react-icons/fi';
import {FaRegTrashAlt} from 'react-icons/fa';
import {MdOutlineAddCircleOutline} from 'react-icons/md';
import Table from '@src/components/Table';
// Styled
import Button from '@src/commons/styles/Button';
import Direction from '@src/commons/styles/Direction';
import { StyledUser, Title } from './styled';
import {ActionTable, StyledUser, Title} from './styled';
import Spinner from '@src/commons/styles/Spinner.ts';
// Constants
import { FORM } from '@src/constants/commons';
// Types
import {ColumnProps} from '@src/types/common.ts';
import {IUser} from '@src/types/user.ts';
// Hooks
import {useUsers} from '@src/hooks/users/useUsers.ts';
import {useEffect, useState} from 'react';
import {FORM, USER_PAGE} from '@src/constants/commons.ts';
import {findItemInListById} from '@src/helpers/helper.ts';
import ConfirmMessage from '@src/components/ConfirmMessage';
import {useIsDeleteUser} from '@src/hooks/users/useSetIsDeleteUser.ts';
import Message from "@src/components/Message";
interface IUserTable extends Omit<IUser, 'isBooked'> {
isBooked: string;
}
const User = () => {
const TITLE = 'Add user';
const {setIsDeleteUser} = useIsDeleteUser();
const [itemSelected, setItemSelected] = useState<IUserTable>();
let userSelected: IUser | undefined;
const columns: ColumnProps[] = [
{
key: 'id',
title: 'Id',
width: 10
},
{
key: 'name',
title: 'Name',
width: 40
},
{
key: 'phone',
title: 'Phone',
width: 20
},
{
key: 'isBooked',
title: 'Is Booked',
width: 20
}
];
const {isLoading, users, count} = useUsers();
// Reset value when users changed.
useEffect(() => {
setItemSelected(undefined);
}, [users]);
const tempUsers = users?.map((user) => (
{
...user,
isBooked: user.isBooked
? 'Yes'
: 'No'
}
));
if (itemSelected && users) {
userSelected = findItemInListById<IUser>(itemSelected.id, users);
}
return (
<>
<StyledUser>
<Direction type="horizontal">
<Title>List User</Title>
<Modal>
<ActionTable>
<Modal.Open
modalName={FORM.USER}
modalName={FORM.ROOM}
renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add user</Button>
<ButtonIcon
icon={<MdOutlineAddCircleOutline/>}
text={'Add User'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'primary'}
iconColor={'white'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<FiEdit/>}
text={'Edit'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'success'}
iconColor={'white'}
disabled={!itemSelected}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.DELETE}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<FaRegTrashAlt/>}
text={'Delete'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'danger'}
iconColor={'white'}
disabled={!itemSelected || itemSelected?.isBooked === 'Yes'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Window
name={FORM.USER}
title={TITLE}
name={FORM.ROOM}
title="Add form"
renderChildren={(onCloseModal) => (
<UserForm onCloseModal={onCloseModal} />
<UserForm onCloseModal={onCloseModal}/>
)}
/>
<Modal.Window
name={FORM.EDIT}
title="Edit User"
renderChildren={(onCloseModal) =>
<UserForm user={userSelected} onCloseModal={onCloseModal}/>
}/>
<Modal.Window
name={FORM.DELETE}
title="Delete User"
renderChildren={(onCloseModal) =>
<ConfirmMessage
message={`Are you sure to delete ${userSelected!.name}?`}
onConfirm={() => setIsDeleteUser(userSelected!.id)}
onCloseModal={onCloseModal}
/>
}/>
</ActionTable>
</Modal>
</Direction>
<UserTable />
{isLoading && <Spinner/>}
{
tempUsers &&
Boolean(tempUsers.length) &&
<Table<IUserTable>
columns={columns}
rows={tempUsers}
count={count}
sortBy={USER_PAGE.SORTBY_OPTIONS}
enabledOrder={true}
searchPlaceHolder={'Search by phone...'}
stateSelected={{
itemSelected,
setItemSelected
}}
onRowClick={(data) => {
setItemSelected(data);
}}/>
}
{tempUsers && Boolean(!tempUsers.length) && <Message>No data to show here!</Message>}
</StyledUser>
</>
);
};
+8 -2
View File
@@ -14,14 +14,20 @@ const Title = styled.h2`
text-transform: capitalize;
`;
const StyledOperationTable = styled.div`
const StyledTableOption = styled.div`
display: flex;
gap: 30px;
justify-content: flex-end;
`;
const ActionTable = styled.div`
display: flex;
gap: 30px;
`
export {
StyledUser,
Title,
StyledOperationTable
StyledTableOption,
ActionTable
};
@@ -3,7 +3,8 @@
--primary-hover-color: #0010ba8c;
--header-table-color: #ebebeb;
--item-selected: #f5f5f5;
--item-hover: #f1f1f1;
--item-selected: #ececec;
--border-color: #8c8c8c;
--footer-table-color: #ebebeb;