mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 13:38:51 +00:00
Fix sort, order, search function
This commit is contained in:
@@ -57,7 +57,7 @@ const AppLayout = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable}}>
|
<UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable, dispatch}}>
|
||||||
<StyledAppLayout>
|
<StyledAppLayout>
|
||||||
<Header accountName={account?.user_metadata.fullName} />
|
<Header accountName={account?.user_metadata.fullName} />
|
||||||
<Sidebar heading="Hotel Management" />
|
<Sidebar heading="Hotel Management" />
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ interface ISearch {
|
|||||||
const Search = ({ setPlaceHolder }: ISearch) => {
|
const Search = ({ setPlaceHolder }: ISearch) => {
|
||||||
const field = 'search';
|
const field = 'search';
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
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);
|
const debounceValue = useDebounce<Nullable<string>>(query, 700);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,6 +37,7 @@ const Search = ({ setPlaceHolder }: ISearch) => {
|
|||||||
<StyledSearch
|
<StyledSearch
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder={setPlaceHolder}
|
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +1,38 @@
|
|||||||
// Types
|
// Types
|
||||||
import { ColumnProps } from '@src/types/common';
|
import {ColumnProps} from '@src/types/common';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import {
|
import {
|
||||||
StyledBody, StyledFooter, StyledHeader, StyledRow, StyledTable
|
StyledBody,
|
||||||
|
StyledFooter,
|
||||||
|
StyledHeader,
|
||||||
|
StyledRow,
|
||||||
|
StyledTable,
|
||||||
} from './styled';
|
} from './styled';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Pagination from '../Pagination';
|
import Pagination from '../Pagination';
|
||||||
import { TbArrowNarrowRight } from 'react-icons/tb';
|
import {TbArrowNarrowRight} from 'react-icons/tb';
|
||||||
import { StyledTableOption } from '@src/pages/Room/styled.ts';
|
import {StyledTableOption} from '@src/pages/Room/styled.ts';
|
||||||
import OrderBy from '@src/components/OrderBy';
|
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 SortBy from '@src/components/SortBy';
|
||||||
import Search from '@src/components/Search';
|
import Search from '@src/components/Search';
|
||||||
import Message from '@src/components/Message';
|
|
||||||
import Direction from '@src/commons/styles/Direction.ts';
|
import Direction from '@src/commons/styles/Direction.ts';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import {Dispatch, SetStateAction} from 'react';
|
||||||
|
|
||||||
const parseWidthString = (columnsWidth: number[]): string => {
|
const parseWidthString = (columnsWidth: number[]): string => {
|
||||||
let result: 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) => {
|
columnsWidth.forEach((width) => {
|
||||||
result += Math.round((width / totalWidth) * 100).toString() + '% ';
|
result += Math.round((
|
||||||
|
width / totalWidth
|
||||||
|
) * 100)
|
||||||
|
.toString() + '% ';
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -34,13 +43,16 @@ type Props<T> = {
|
|||||||
rows: T[];
|
rows: T[];
|
||||||
stateSelected: {
|
stateSelected: {
|
||||||
itemSelected: T | undefined;
|
itemSelected: T | undefined;
|
||||||
setItemSelected: Dispatch<SetStateAction<T | undefined>>
|
setItemSelected: Dispatch<SetStateAction<T | undefined>>;
|
||||||
};
|
};
|
||||||
count?: number | null;
|
count?: number | null;
|
||||||
onRowClick?: (rowData: T) => void;
|
onRowClick?: (rowData: T) => void;
|
||||||
enabledOrder?: boolean;
|
enabledOrder?: boolean;
|
||||||
enabledSort?: boolean;
|
sortBy?: {
|
||||||
enabledSearch?: boolean;
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
searchPlaceHolder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Table = <T, >({
|
const Table = <T, >({
|
||||||
@@ -49,18 +61,24 @@ const Table = <T, >({
|
|||||||
count,
|
count,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
stateSelected,
|
stateSelected,
|
||||||
enabledSort = false,
|
sortBy,
|
||||||
enabledSearch = false,
|
searchPlaceHolder,
|
||||||
enabledOrder = false
|
enabledOrder = false,
|
||||||
}: Props<T>) => {
|
}: Props<T>) => {
|
||||||
const width = parseWidthString(columns.map((item) => item.width));
|
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>;
|
return <div key={`headCell-${index}`}>{column.title}</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderRows = rows.map((row, rowIndex) => {
|
const renderRows = rows.map((
|
||||||
|
row,
|
||||||
|
rowIndex
|
||||||
|
) => {
|
||||||
const handleRowClick = () => {
|
const handleRowClick = () => {
|
||||||
if (onRowClick) {
|
if (onRowClick) {
|
||||||
onRowClick(row);
|
onRowClick(row);
|
||||||
@@ -73,45 +91,61 @@ const Table = <T, >({
|
|||||||
key={`row-${rowIndex}`}
|
key={`row-${rowIndex}`}
|
||||||
width={width}
|
width={width}
|
||||||
onDoubleClick={handleRowClick}
|
onDoubleClick={handleRowClick}
|
||||||
className={JSON.stringify(itemSelected) === JSON.stringify(row)
|
className={
|
||||||
? 'selected'
|
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;
|
const value = row[column.key as keyof typeof row] as string;
|
||||||
|
|
||||||
if (column.isDateValue) {
|
if (column.isDateValue) {
|
||||||
return (<div
|
return (
|
||||||
style={{ display: 'flex', alignItems: 'center' }}
|
<div
|
||||||
key={`cell-${columnIndex}`}
|
style={{display: 'flex', alignItems: 'center'}}
|
||||||
>
|
key={`cell-${columnIndex}`}
|
||||||
{value[0]} <TbArrowNarrowRight />
|
>
|
||||||
{value[1]}
|
{value[0]} <TbArrowNarrowRight/>
|
||||||
</div>);
|
{value[1]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div key={`cell-${columnIndex}`}>{value}</div>;
|
return <div key={`cell-${columnIndex}`}>{value}</div>;
|
||||||
})}
|
})}
|
||||||
</StyledRow>);
|
</StyledRow>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (<Direction>
|
return (
|
||||||
<StyledTableOption>
|
<Direction>
|
||||||
{enabledOrder && <OrderBy options={ORDERBY_OPTIONS} />}
|
<StyledTableOption>
|
||||||
{enabledSort && <SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />}
|
{enabledOrder && <OrderBy options={ORDERBY_OPTIONS}/>}
|
||||||
{enabledSearch && <Search setPlaceHolder="Search by name..." />}
|
{Boolean(sortBy?.length) && <SortBy options={sortBy!}/>}
|
||||||
</StyledTableOption>
|
{Boolean(searchPlaceHolder) && <Search setPlaceHolder={searchPlaceHolder!}/>}
|
||||||
|
</StyledTableOption>
|
||||||
|
|
||||||
{rows && rows.length ? (<StyledTable>
|
{rows && rows.length &&
|
||||||
<StyledHeader width={width}>{headers}</StyledHeader>
|
(
|
||||||
|
<StyledTable>
|
||||||
|
<StyledHeader width={width}>{headers}</StyledHeader>
|
||||||
|
|
||||||
<StyledBody>{renderRows}</StyledBody>
|
<StyledBody>{renderRows}</StyledBody>
|
||||||
|
|
||||||
{Boolean(count) && (<StyledFooter>
|
{Boolean(count) && (
|
||||||
<Pagination count={count!} />
|
<StyledFooter>
|
||||||
</StyledFooter>)}
|
<Pagination count={count!}/>
|
||||||
</StyledTable>) : (<Message>No data to show here!</Message>)}
|
</StyledFooter>
|
||||||
</Direction>);
|
)}
|
||||||
};
|
</StyledTable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Direction>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Table;
|
export default Table;
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ const StyledBody = styled.div`
|
|||||||
|
|
||||||
const StyledRow = styled(CommonRow)`
|
const StyledRow = styled(CommonRow)`
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--item-hover);
|
||||||
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: var(--item-selected);
|
background-color: var(--item-selected);
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ const getDayDiff = (startDate: Date, endDate: Date): number => {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findItemInListById = <T,>(id: number, listItem: T[]): T | undefined => {
|
||||||
|
return listItem.find((item) => item["id" as keyof typeof item] === id);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
getDayDiff,
|
getDayDiff,
|
||||||
|
findItemInListById,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => {
|
|||||||
usersAvailable,
|
usersAvailable,
|
||||||
dispatch
|
dispatch
|
||||||
} = useUserRoomAvailable();
|
} = useUserRoomAvailable();
|
||||||
|
|
||||||
const { isCreating, createBooking } = useCreateBooking();
|
const { isCreating, createBooking } = useCreateBooking();
|
||||||
const { isUpdating, updateBooking } = useUpdateBooking();
|
const { isUpdating, updateBooking } = useUpdateBooking();
|
||||||
const isLoading = isCreating || isUpdating;
|
const isLoading = isCreating || isUpdating;
|
||||||
@@ -83,7 +84,7 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => {
|
|||||||
type: 'updateStatusRoom',
|
type: 'updateStatusRoom',
|
||||||
payload: [{ id: newBooking.roomId, status: true }],
|
payload: [{ id: newBooking.roomId, status: true }],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update room, user available in server
|
// Update room, user available in server
|
||||||
await updateRoomStatus(newBooking.roomId, true);
|
await updateRoomStatus(newBooking.roomId, true);
|
||||||
await updateUserBookedStatus(newBooking.userId, true);
|
await updateUserBookedStatus(newBooking.userId, true);
|
||||||
|
|||||||
@@ -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} <TbArrowNarrowRight />
|
|
||||||
{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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
// Components
|
||||||
import Modal from '@src/components/Modal';
|
|
||||||
import BookingTable from './BookingTable';
|
|
||||||
import BookingForm from './BookingForm';
|
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
|
// Styled
|
||||||
import { FORM } from '@src/constants/commons';
|
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 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 (
|
return (
|
||||||
<StyledBooking>
|
<StyledBooking>
|
||||||
<Direction type="horizontal">
|
<Direction type="horizontal">
|
||||||
<Title>List Booking</Title>
|
<Title>List Booking</Title>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '15px' }}>
|
<Modal>
|
||||||
<Modal>
|
<ActionTable>
|
||||||
<Modal.Open
|
<Modal.Open
|
||||||
modalName={FORM.BOOKING}
|
modalName={FORM.ROOM}
|
||||||
renderChildren={(onCloseModal) => (
|
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
|
<Modal.Window
|
||||||
name={FORM.BOOKING}
|
name={FORM.ROOM}
|
||||||
title="Add form"
|
title="Add form"
|
||||||
renderChildren={(onCloseModal) => (
|
renderChildren={(onCloseModal) => (
|
||||||
<BookingForm onCloseModal={onCloseModal} />
|
<BookingForm onCloseModal={onCloseModal}/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
|
||||||
</div>
|
<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>
|
||||||
</Direction>
|
</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>
|
</StyledBooking>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,14 +14,20 @@ const Title = styled.h2`
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledOperationTable = styled.div`
|
const StyledTableOption = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ActionTable = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
export {
|
export {
|
||||||
StyledBooking,
|
StyledBooking,
|
||||||
Title,
|
Title,
|
||||||
StyledOperationTable
|
StyledTableOption,
|
||||||
|
ActionTable
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,13 +19,20 @@ import { IRoom } from '@src/types/room.ts';
|
|||||||
// Hooks
|
// Hooks
|
||||||
import { useRooms } from '@src/hooks/rooms/useRooms.ts';
|
import { useRooms } from '@src/hooks/rooms/useRooms.ts';
|
||||||
import { useEffect, useState } from 'react';
|
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'> {
|
interface IRoomTable extends Omit<IRoom, 'status'> {
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Room = () => {
|
const Room = () => {
|
||||||
|
const { setIsDeleteRoom } = useSetIsDeleteRoom();
|
||||||
const [itemSelected, setItemSelected] = useState<IRoomTable>();
|
const [itemSelected, setItemSelected] = useState<IRoomTable>();
|
||||||
|
let roomSelected: IRoom | undefined;
|
||||||
|
|
||||||
const columns: ColumnProps[] = [
|
const columns: ColumnProps[] = [
|
||||||
{
|
{
|
||||||
@@ -50,7 +57,7 @@ const Room = () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const { isLoading, rooms, count } = useRooms();
|
const { isLoading, rooms, count } = useRooms();
|
||||||
|
|
||||||
// Reset value when rooms changed.
|
// Reset value when rooms changed.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setItemSelected(undefined);
|
setItemSelected(undefined);
|
||||||
@@ -63,6 +70,10 @@ const Room = () => {
|
|||||||
: 'Available'
|
: 'Available'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (itemSelected && rooms) {
|
||||||
|
roomSelected = findItemInListById<IRoom>(itemSelected.id, rooms);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRoom>
|
<StyledRoom>
|
||||||
<Direction type="horizontal">
|
<Direction type="horizontal">
|
||||||
@@ -71,7 +82,7 @@ const Room = () => {
|
|||||||
<Modal>
|
<Modal>
|
||||||
<ActionTable>
|
<ActionTable>
|
||||||
<Modal.Open
|
<Modal.Open
|
||||||
modalName="room-form"
|
modalName={FORM.ROOM}
|
||||||
renderChildren={(onCloseModal) => (
|
renderChildren={(onCloseModal) => (
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
icon={<MdOutlineAddCircleOutline />}
|
icon={<MdOutlineAddCircleOutline />}
|
||||||
@@ -84,32 +95,65 @@ const Room = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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?.status === 'Available'}
|
||||||
|
onClick={onCloseModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal.Window
|
<Modal.Window
|
||||||
name="room-form"
|
name={FORM.ROOM}
|
||||||
title="Add form"
|
title="Add form"
|
||||||
renderChildren={(onCloseModal) => (
|
renderChildren={(onCloseModal) => (
|
||||||
<RoomForm onCloseModal={onCloseModal} />
|
<RoomForm onCloseModal={onCloseModal} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ButtonIcon
|
<Modal.Window
|
||||||
icon={<FiEdit />}
|
name={FORM.EDIT}
|
||||||
text={'Edit'}
|
title="Edit Room"
|
||||||
iconSize={'18px'}
|
renderChildren={(onCloseModal) =>
|
||||||
fontSize={'var(--fs-sm)'}
|
<RoomForm room={roomSelected} onCloseModal={onCloseModal} />
|
||||||
variations={'success'}
|
} />
|
||||||
iconColor={'white'}
|
|
||||||
disabled={Boolean(!itemSelected)}
|
<Modal.Window
|
||||||
/>
|
name={FORM.DELETE}
|
||||||
<ButtonIcon
|
title="Delete Room"
|
||||||
icon={<FaRegTrashAlt />}
|
renderChildren={(onCloseModal) =>
|
||||||
text={'Delete'}
|
<ConfirmMessage
|
||||||
iconSize={'18px'}
|
message={`Are you sure to delete ${roomSelected!.name}?`}
|
||||||
fontSize={'var(--fs-sm)'}
|
onConfirm={() => setIsDeleteRoom(roomSelected!.id)}
|
||||||
variations={'danger'}
|
onCloseModal={onCloseModal}
|
||||||
iconColor={'white'}
|
/>
|
||||||
disabled={Boolean(!itemSelected)}
|
} />
|
||||||
/>
|
|
||||||
</ActionTable>
|
</ActionTable>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Direction>
|
</Direction>
|
||||||
@@ -123,16 +167,19 @@ const Room = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
rows={tempRooms}
|
rows={tempRooms}
|
||||||
count={count}
|
count={count}
|
||||||
enabledSort={true}
|
sortBy={ROOM_PAGE.SORTBY_OPTIONS}
|
||||||
enabledOrder={true}
|
enabledOrder={true}
|
||||||
enabledSearch={true}
|
searchPlaceHolder={'Search by name...'}
|
||||||
stateSelected={{
|
stateSelected={{
|
||||||
itemSelected,
|
itemSelected,
|
||||||
setItemSelected,
|
setItemSelected
|
||||||
}}
|
}}
|
||||||
onRowClick={(data) => {
|
onRowClick={(data) => {
|
||||||
setItemSelected(data);
|
setItemSelected(data);
|
||||||
}} />}
|
}} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{tempRooms && Boolean(!tempRooms.length) && <Message>No data to show here!</Message>}
|
||||||
</StyledRoom>
|
</StyledRoom>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +1,188 @@
|
|||||||
// Components
|
// Components
|
||||||
import UserTable from './UserTable';
|
|
||||||
import Modal from '@src/components/Modal';
|
|
||||||
import UserForm from './UserForm';
|
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
|
// Styled
|
||||||
import Button from '@src/commons/styles/Button';
|
import {ActionTable, StyledUser, Title} from './styled';
|
||||||
import Direction from '@src/commons/styles/Direction';
|
import Spinner from '@src/commons/styles/Spinner.ts';
|
||||||
import { StyledUser, Title } from './styled';
|
|
||||||
|
|
||||||
// Constants
|
// Types
|
||||||
import { FORM } from '@src/constants/commons';
|
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 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 (
|
return (
|
||||||
<>
|
<StyledUser>
|
||||||
<StyledUser>
|
<Direction type="horizontal">
|
||||||
<Direction type="horizontal">
|
<Title>List User</Title>
|
||||||
<Title>List User</Title>
|
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
<ActionTable>
|
||||||
<Modal.Open
|
<Modal.Open
|
||||||
modalName={FORM.USER}
|
modalName={FORM.ROOM}
|
||||||
renderChildren={(onCloseModal) => (
|
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.Window
|
|
||||||
name={FORM.USER}
|
|
||||||
title={TITLE}
|
|
||||||
renderChildren={(onCloseModal) => (
|
|
||||||
<UserForm onCloseModal={onCloseModal} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</Direction>
|
|
||||||
|
|
||||||
<UserTable />
|
<Modal.Open
|
||||||
</StyledUser>
|
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.ROOM}
|
||||||
|
title="Add form"
|
||||||
|
renderChildren={(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>
|
||||||
|
|
||||||
|
{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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,20 @@ const Title = styled.h2`
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledOperationTable = styled.div`
|
const StyledTableOption = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ActionTable = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
export {
|
export {
|
||||||
StyledUser,
|
StyledUser,
|
||||||
Title,
|
Title,
|
||||||
StyledOperationTable
|
StyledTableOption,
|
||||||
|
ActionTable
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
--primary-hover-color: #0010ba8c;
|
--primary-hover-color: #0010ba8c;
|
||||||
|
|
||||||
--header-table-color: #ebebeb;
|
--header-table-color: #ebebeb;
|
||||||
--item-selected: #f5f5f5;
|
--item-hover: #f1f1f1;
|
||||||
|
--item-selected: #ececec;
|
||||||
--border-color: #8c8c8c;
|
--border-color: #8c8c8c;
|
||||||
--footer-table-color: #ebebeb;
|
--footer-table-color: #ebebeb;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user