mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 13:38:51 +00:00
Update code, create booking services
This commit is contained in:
@@ -3,14 +3,12 @@ import renderer from 'react-test-renderer';
|
||||
// Components
|
||||
import ConfirmMessage from '.';
|
||||
|
||||
describe('ConfirmMessage', () => {
|
||||
const isDisable = true;
|
||||
describe('ConfirmMessage testing snapshot', () => {
|
||||
const message = 'Hello World!';
|
||||
const handleOnConfirm = jest.fn();
|
||||
|
||||
const wrapper = renderer.create(
|
||||
<ConfirmMessage
|
||||
disabled={isDisable}
|
||||
message={message}
|
||||
onConfirm={handleOnConfirm}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
// Styled
|
||||
import { StyledConfirmDelete } from './styled';
|
||||
@@ -8,27 +8,27 @@ import Button from '@commonStyle/Button';
|
||||
|
||||
interface IConfirmMessage {
|
||||
message: string;
|
||||
disabled: boolean;
|
||||
onConfirm: () => void;
|
||||
onCloseModal?: () => void;
|
||||
}
|
||||
|
||||
const ConfirmMessage = memo(
|
||||
({ message, disabled, onConfirm, onCloseModal }: IConfirmMessage) => {
|
||||
({ message, onConfirm, onCloseModal }: IConfirmMessage) => {
|
||||
const handleConfirmBtn = useCallback(() => {
|
||||
onConfirm();
|
||||
onCloseModal!();
|
||||
}, [onCloseModal, onConfirm]);
|
||||
|
||||
return (
|
||||
<StyledConfirmDelete>
|
||||
<p>{message}</p>
|
||||
|
||||
<div>
|
||||
<Button variations="danger" disabled={disabled} onClick={onConfirm}>
|
||||
Delete
|
||||
<Button variations="danger" onClick={handleConfirmBtn}>
|
||||
Yes
|
||||
</Button>
|
||||
<Button
|
||||
variations="secondary"
|
||||
disabled={disabled}
|
||||
onClick={onCloseModal}
|
||||
>
|
||||
Cancel
|
||||
<Button variations="secondary" onClick={onCloseModal}>
|
||||
No
|
||||
</Button>
|
||||
</div>
|
||||
</StyledConfirmDelete>
|
||||
|
||||
@@ -13,7 +13,7 @@ const StyledConfirmDelete = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
padding-inline: 50px;
|
||||
padding-inline: 100px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ const Error = styled.p`
|
||||
|
||||
margin-top: 5px;
|
||||
padding-inline: 5px;
|
||||
|
||||
width: 220px;
|
||||
`;
|
||||
|
||||
const FormBtn = styled(Button)`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Components
|
||||
import { MdSpaceDashboard } from 'react-icons/md';
|
||||
import { BiSolidCalendarAlt } from 'react-icons/bi';
|
||||
import { HiUsers } from 'react-icons/hi2';
|
||||
import { BsHouseDoorFill } from 'react-icons/bs';
|
||||
|
||||
@@ -7,14 +7,14 @@ import { BsHouseDoorFill } from 'react-icons/bs';
|
||||
import { StyledNav, StyledNavLink } from './styled';
|
||||
|
||||
// Constants
|
||||
import { DASHBOARD, ROOM, USER } from '@constant/path';
|
||||
import { BOOKING, ROOM, USER } from '@constant/path';
|
||||
|
||||
const Nav = () => {
|
||||
return (
|
||||
<StyledNav>
|
||||
<StyledNavLink to={DASHBOARD}>
|
||||
<MdSpaceDashboard />
|
||||
<span>Dashboard</span>
|
||||
<StyledNavLink to={BOOKING}>
|
||||
<BiSolidCalendarAlt />
|
||||
<span>Booking</span>
|
||||
</StyledNavLink>
|
||||
<StyledNavLink to={USER}>
|
||||
<HiUsers />
|
||||
|
||||
@@ -19,6 +19,7 @@ interface ISelect {
|
||||
onChange?: ChangeEventHandler<HTMLSelectElement> | undefined;
|
||||
id?: string;
|
||||
ariaLabel: string;
|
||||
disable?: boolean;
|
||||
}
|
||||
|
||||
const Select = ({
|
||||
@@ -28,6 +29,7 @@ const Select = ({
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
disable = false,
|
||||
}: ISelect) => {
|
||||
const { register } = useFormContext() ?? {};
|
||||
|
||||
@@ -38,6 +40,7 @@ const Select = ({
|
||||
aria-label={ariaLabel}
|
||||
id={id}
|
||||
value={value}
|
||||
disabled={disable}
|
||||
{...(register
|
||||
? { ...register(id!, optionsConfigForm) }
|
||||
: { onChange: onChange })}
|
||||
|
||||
@@ -6,6 +6,8 @@ const StyledSelect = styled.select`
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
export { StyledSelect };
|
||||
|
||||
@@ -48,6 +48,7 @@ const FORM = {
|
||||
DELETE: 'delete',
|
||||
USER: 'user-form',
|
||||
ROOM: 'room-form',
|
||||
CHECKOUT: 'checkout',
|
||||
};
|
||||
|
||||
const REGEX = {
|
||||
|
||||
@@ -10,4 +10,16 @@ const formatCurrency = (value: number): string => {
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
export { formatCurrency };
|
||||
const getDayDiff = (startDate: Date, endDate: Date): number => {
|
||||
const msInDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
return Math.round(
|
||||
Math.abs(Number(endDate) - Number(startDate)) / msInDay
|
||||
);
|
||||
}
|
||||
|
||||
const convertCurrencyToNumber = (currency: string) => {
|
||||
return Number(currency.replace(/[^0-9.-]+/g,""))
|
||||
}
|
||||
|
||||
export { formatCurrency, getDayDiff, convertCurrencyToNumber };
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Types
|
||||
import { IBooking, TBookingResponse } from '@type/booking';
|
||||
|
||||
// Services
|
||||
import supabase from './supabaseService';
|
||||
|
||||
// Constants
|
||||
import { DEFAULT_PAGE_SIZE } from '@constant/config';
|
||||
import {
|
||||
BOOKINGS_TABLE,
|
||||
ERROR_CHECKOUT,
|
||||
ERROR_CREATE_BOOKING,
|
||||
ERROR_DELETE_BOOKING,
|
||||
ERROR_FETCHING_BOOKING,
|
||||
ERROR_UPDATE_BOOKING,
|
||||
} from '@constant/messages';
|
||||
import { updateRoomStatus } from './roomServices';
|
||||
import { updateUserBookedStatus } from './userServices';
|
||||
|
||||
interface ICheckOutBooking {
|
||||
idBooking: number;
|
||||
roomId: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
interface IGetAllBookings {
|
||||
userNameSearch: string;
|
||||
page: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bookings from database
|
||||
* @returns Return all bookings in database
|
||||
*/
|
||||
const getAllBookings = async ({
|
||||
userNameSearch,
|
||||
page,
|
||||
}: IGetAllBookings): Promise<{
|
||||
data: TBookingResponse[];
|
||||
count: number | null;
|
||||
}> => {
|
||||
const from = (page - 1) * DEFAULT_PAGE_SIZE;
|
||||
const to = from + DEFAULT_PAGE_SIZE - 1;
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
.from(BOOKINGS_TABLE)
|
||||
.select(
|
||||
'id, startDate, endDate, status, amount, rooms(id, name), users!inner(id, name)',
|
||||
{
|
||||
count: 'exact',
|
||||
}
|
||||
)
|
||||
.ilike('users.name', `%${userNameSearch}%`)
|
||||
.range(from, to);
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_FETCHING_BOOKING);
|
||||
}
|
||||
|
||||
return { data, count };
|
||||
};
|
||||
|
||||
/**
|
||||
* Update booking into database
|
||||
* @param booking Booking object need to be updated
|
||||
*/
|
||||
const updateBooking = async (booking: IBooking): Promise<IBooking> => {
|
||||
const { data, error } = await supabase
|
||||
.from(BOOKINGS_TABLE)
|
||||
.update(booking)
|
||||
.eq('id', booking.id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_UPDATE_BOOKING);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add room to database
|
||||
* @param room The room object need to be add
|
||||
*/
|
||||
const createBooking = async (booking: IBooking): Promise<IBooking> => {
|
||||
const { data, error: createBookingError } = await supabase
|
||||
.from(BOOKINGS_TABLE)
|
||||
.insert(booking)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createBookingError) {
|
||||
console.error(createBookingError.message);
|
||||
throw new Error(ERROR_CREATE_BOOKING);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete room in database
|
||||
* @param idRoom The id of room need to delete
|
||||
*/
|
||||
const deleteBooking = async (idBooking: number) => {
|
||||
const { error } = await supabase
|
||||
.from(BOOKINGS_TABLE)
|
||||
.delete()
|
||||
.eq('id', idBooking);
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_DELETE_BOOKING);
|
||||
}
|
||||
};
|
||||
|
||||
const checkOutBooking = async ({
|
||||
idBooking,
|
||||
roomId,
|
||||
userId,
|
||||
}: ICheckOutBooking) => {
|
||||
// Update status booking
|
||||
const { data, error } = await supabase
|
||||
.from(BOOKINGS_TABLE)
|
||||
.update({ status: false })
|
||||
.eq('id', idBooking)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_CHECKOUT);
|
||||
}
|
||||
|
||||
// Update status room, user
|
||||
await updateRoomStatus(roomId, false);
|
||||
await updateUserBookedStatus(userId, false);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export {
|
||||
createBooking,
|
||||
getAllBookings,
|
||||
updateBooking,
|
||||
deleteBooking,
|
||||
checkOutBooking,
|
||||
};
|
||||
@@ -7,17 +7,18 @@ import supabase from './supabaseService';
|
||||
|
||||
// Constants
|
||||
import { DEFAULT_PAGE_SIZE } from '@constant/config';
|
||||
|
||||
const ROOMS_TABLE = 'rooms';
|
||||
const ERROR_FETCHING = "Can't fetch room data!";
|
||||
const ERROR_UPDATE_ROOM = "Can't update room!";
|
||||
const ERROR_CREATE_ROOM = "Can't create room!";
|
||||
const ERROR_DELETE_ROOM = "Can't delete room!";
|
||||
import {
|
||||
ERROR_CREATE_ROOM,
|
||||
ERROR_DELETE_ROOM,
|
||||
ERROR_FETCHING_ROOM,
|
||||
ERROR_UPDATE_ROOM,
|
||||
ROOMS_TABLE,
|
||||
} from '@constant/messages';
|
||||
|
||||
interface IGetAllRooms {
|
||||
sortBy: string;
|
||||
orderBy: string;
|
||||
roomName: string;
|
||||
roomSearch: string;
|
||||
page: number;
|
||||
}
|
||||
|
||||
@@ -28,22 +29,27 @@ interface IGetAllRooms {
|
||||
const getAllRooms = async ({
|
||||
sortBy,
|
||||
orderBy,
|
||||
roomName,
|
||||
roomSearch,
|
||||
page,
|
||||
}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => {
|
||||
const from = (page - 1) * DEFAULT_PAGE_SIZE;
|
||||
const to = from + DEFAULT_PAGE_SIZE - 1;
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
let query = supabase
|
||||
.from(ROOMS_TABLE)
|
||||
.select('*', { count: 'exact' })
|
||||
.range(from, to)
|
||||
.order(sortBy, { ascending: orderBy === 'asc' })
|
||||
.ilike('name', `%${roomName}%`);
|
||||
.like('name', `%${roomSearch}%`);
|
||||
|
||||
if(page) {
|
||||
query = query.range(from, to);
|
||||
}
|
||||
|
||||
const { data, error, count } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_FETCHING);
|
||||
throw new Error(ERROR_FETCHING_ROOM);
|
||||
}
|
||||
|
||||
return { data, count };
|
||||
@@ -101,18 +107,57 @@ const deleteRoom = async (idRoom: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getRoomsAvailable = async (): Promise<IDataState[]> => {
|
||||
const getRoomById = async (idRoom: string): Promise<IRoom> => {
|
||||
const { data, error } = await supabase
|
||||
.from(ROOMS_TABLE)
|
||||
.select('id, name')
|
||||
.eq('status', false);
|
||||
.select('*')
|
||||
.eq('id', idRoom)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_FETCHING);
|
||||
throw new Error(ERROR_FETCHING_ROOM);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export { getAllRooms, updateRoom, createRoom, deleteRoom, getRoomsAvailable };
|
||||
const getRoomsAvailable = async (): Promise<IDataState[]> => {
|
||||
const { data, error } = await supabase
|
||||
.from(ROOMS_TABLE)
|
||||
.select('id, name, status');
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_FETCHING_ROOM);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateRoomStatus = async (
|
||||
id: number,
|
||||
status: boolean
|
||||
): Promise<number> => {
|
||||
const { error, status: statusConnection } = await supabase
|
||||
.from(ROOMS_TABLE)
|
||||
.update({ status })
|
||||
.eq('id', id);
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_UPDATE_ROOM);
|
||||
}
|
||||
|
||||
return statusConnection;
|
||||
};
|
||||
|
||||
export {
|
||||
getAllRooms,
|
||||
updateRoom,
|
||||
createRoom,
|
||||
deleteRoom,
|
||||
getRoomsAvailable,
|
||||
getRoomById,
|
||||
updateRoomStatus,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Types
|
||||
import { IUser } from '@type/users';
|
||||
import { IDataState } from '@type/common';
|
||||
|
||||
// Services
|
||||
import supabase from './supabaseService';
|
||||
|
||||
// Constants
|
||||
import { IDataState } from '@type/common';
|
||||
import { DEFAULT_PAGE_SIZE } from '@constant/config';
|
||||
|
||||
const USERS_TABLE = 'users';
|
||||
const ERROR_FETCHING = "Users can't be loaded!";
|
||||
const ERROR_CREATE_USER = "Can't create user!";
|
||||
const ERROR_UPDATE_USER = "Can't update user!";
|
||||
import {
|
||||
ERROR_CREATE_USER,
|
||||
ERROR_DELETE_USER,
|
||||
ERROR_FETCHING_USER,
|
||||
ERROR_UPDATE_USER,
|
||||
USERS_TABLE,
|
||||
} from '@constant/messages';
|
||||
|
||||
/**
|
||||
* Create user to the database
|
||||
@@ -24,6 +24,8 @@ const createUser = async (user: IUser): Promise<IUser> => {
|
||||
.select()
|
||||
.single();
|
||||
|
||||
console.log('Create');
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_CREATE_USER);
|
||||
@@ -52,6 +54,19 @@ const updateUser = async (user: IUser): Promise<IUser> => {
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete user in database
|
||||
* @param idUser The id of user need to delete
|
||||
*/
|
||||
const deleteUser = async (idUser: number) => {
|
||||
const { error } = await supabase.from('users').delete().eq('id', idUser);
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_DELETE_USER);
|
||||
}
|
||||
};
|
||||
|
||||
interface IGetAllUsers {
|
||||
sortBy: string;
|
||||
orderBy: string;
|
||||
@@ -75,16 +90,21 @@ const getAllUsers = async ({
|
||||
const from = (page - 1) * DEFAULT_PAGE_SIZE;
|
||||
const to = from + DEFAULT_PAGE_SIZE - 1;
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
let query = supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('*', { count: 'exact' })
|
||||
.range(from, to)
|
||||
.order(sortBy, { ascending: orderBy === 'asc' })
|
||||
.ilike('phone', `%${phoneSearch}%`);
|
||||
.like('phone', `%${phoneSearch}%`);
|
||||
|
||||
if (page) {
|
||||
query = query.range(from, to);
|
||||
}
|
||||
|
||||
const { data, error, count } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_FETCHING);
|
||||
throw new Error(ERROR_FETCHING_USER);
|
||||
}
|
||||
|
||||
return { data, count };
|
||||
@@ -93,15 +113,38 @@ const getAllUsers = async ({
|
||||
const getUserNotBooked = async (): Promise<IDataState[]> => {
|
||||
const { data, error } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.select('id, name')
|
||||
.eq('isBooked', false);
|
||||
.select('id, name, isBooked');
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_FETCHING);
|
||||
throw new Error(ERROR_FETCHING_USER);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export { updateUser, createUser, getAllUsers, getUserNotBooked };
|
||||
const updateUserBookedStatus = async (
|
||||
id: number,
|
||||
isBooked: boolean
|
||||
): Promise<number> => {
|
||||
const { error, status } = await supabase
|
||||
.from(USERS_TABLE)
|
||||
.update({ isBooked })
|
||||
.eq('id', id);
|
||||
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
throw new Error(ERROR_UPDATE_USER);
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
export {
|
||||
updateUser,
|
||||
createUser,
|
||||
getAllUsers,
|
||||
deleteUser,
|
||||
getUserNotBooked,
|
||||
updateUserBookedStatus,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user