609 lines
24 KiB
TypeScript
609 lines
24 KiB
TypeScript
import React, { useEffect, useState, useMemo } from 'react'
|
|
import { createPortal } from 'react-dom';
|
|
import { ToastContainer, toast } from 'react-toastify';
|
|
import BaseButton from '../BaseButton'
|
|
import CardBoxModal from '../CardBoxModal'
|
|
import CardBox from "../CardBox";
|
|
import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/reservations/reservationsSlice'
|
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
|
import { useRouter } from 'next/router'
|
|
import { Field, Form, Formik } from "formik";
|
|
import {
|
|
DataGrid,
|
|
GridColDef,
|
|
} from '@mui/x-data-grid';
|
|
import {loadColumns} from "./configureReservationsCols";
|
|
import _ from 'lodash';
|
|
import dataFormatter from '../../helpers/dataFormatter'
|
|
import { getIncompleteFilterHint, isFilterItemIncomplete, pruneHiddenFilterItems } from '../../helpers/entityVisibility'
|
|
import {dataGridStyles} from "../../styles";
|
|
|
|
|
|
import BigCalendar from "../BigCalendar";
|
|
import { SlotInfo } from 'react-big-calendar';
|
|
|
|
|
|
const perPage = 25
|
|
|
|
const compactColumnVisibilityModel = {
|
|
tenant: false,
|
|
organization: false,
|
|
booking_request: false,
|
|
unit_type: false,
|
|
actual_check_in_at: false,
|
|
actual_check_out_at: false,
|
|
early_check_in: false,
|
|
late_check_out: false,
|
|
monthly_rate: false,
|
|
currency: false,
|
|
internal_notes: false,
|
|
external_notes: false,
|
|
guests: false,
|
|
service_requests: false,
|
|
invoices: false,
|
|
documents: false,
|
|
comments: false,
|
|
}
|
|
|
|
|
|
const TableSampleReservations = ({ filterItems, setFilterItems, filters, showGrid }) => {
|
|
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
|
|
|
|
const dispatch = useAppDispatch();
|
|
const router = useRouter();
|
|
|
|
const pagesList = [];
|
|
const [id, setId] = useState(null);
|
|
const [currentPage, setCurrentPage] = useState(0);
|
|
const [filterRequest, setFilterRequest] = React.useState('');
|
|
const [columns, setColumns] = useState<GridColDef[]>([]);
|
|
const [selectedRows, setSelectedRows] = useState([]);
|
|
const [sortModel, setSortModel] = useState([
|
|
{
|
|
field: '',
|
|
sort: 'desc',
|
|
},
|
|
]);
|
|
|
|
const { reservations, loading, count, notify: reservationsNotify, refetch } = useAppSelector((state) => state.reservations)
|
|
const { currentUser } = useAppSelector((state) => state.auth);
|
|
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
|
const corners = useAppSelector((state) => state.style.corners);
|
|
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
|
|
for (let i = 0; i < numPages; i++) {
|
|
pagesList.push(i);
|
|
}
|
|
|
|
const loadData = async (page = currentPage, request = filterRequest) => {
|
|
if (page !== currentPage) setCurrentPage(page);
|
|
if (request !== filterRequest) setFilterRequest(request);
|
|
const { sort, field } = sortModel[0];
|
|
|
|
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
|
dispatch(fetch({ limit: perPage, page, query }));
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (reservationsNotify.showNotification) {
|
|
notify(reservationsNotify.typeNotification, reservationsNotify.textNotification);
|
|
}
|
|
}, [reservationsNotify.showNotification]);
|
|
|
|
useEffect(() => {
|
|
if (!currentUser) return;
|
|
loadData();
|
|
}, [sortModel, currentUser]);
|
|
|
|
useEffect(() => {
|
|
if (refetch) {
|
|
loadData(0);
|
|
dispatch(setRefetch(false));
|
|
}
|
|
}, [refetch, dispatch]);
|
|
|
|
const validFilterItems = useMemo(() => pruneHiddenFilterItems(filterItems, filters), [filterItems, filters]);
|
|
|
|
useEffect(() => {
|
|
if (validFilterItems !== filterItems) {
|
|
setFilterItems(validFilterItems);
|
|
}
|
|
}, [filterItems, setFilterItems, validFilterItems]);
|
|
|
|
const [isModalInfoActive, setIsModalInfoActive] = useState(false)
|
|
const [isModalTrashActive, setIsModalTrashActive] = useState(false)
|
|
|
|
const handleModalAction = () => {
|
|
setIsModalInfoActive(false)
|
|
setIsModalTrashActive(false)
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCreateEventAction = ({ start, end }: SlotInfo) => {
|
|
router.push(
|
|
`/reservations/reservations-new?dateRangeStart=${start.toISOString()}&dateRangeEnd=${end.toISOString()}`,
|
|
);
|
|
};
|
|
|
|
|
|
const handleDeleteModalAction = (id: string) => {
|
|
setId(id)
|
|
setIsModalTrashActive(true)
|
|
}
|
|
const handleDeleteAction = async () => {
|
|
if (id) {
|
|
await dispatch(deleteItem(id));
|
|
await loadData(0);
|
|
setIsModalTrashActive(false);
|
|
}
|
|
};
|
|
|
|
const reservationCalendarOverview = useMemo(() => {
|
|
const today = new Date();
|
|
const nextSevenDays = new Date(today);
|
|
nextSevenDays.setDate(nextSevenDays.getDate() + 7);
|
|
|
|
const totals = reservations.reduce(
|
|
(accumulator, item) => {
|
|
const checkIn = item?.check_in_at ? new Date(item.check_in_at) : null;
|
|
const checkOut = item?.check_out_at ? new Date(item.check_out_at) : null;
|
|
const status = item?.status || 'quoted';
|
|
|
|
accumulator.total += 1;
|
|
accumulator[status] = (accumulator[status] || 0) + 1;
|
|
|
|
if (checkIn && checkIn >= today && checkIn <= nextSevenDays) {
|
|
accumulator.upcomingArrivals += 1;
|
|
}
|
|
|
|
if (checkOut && checkOut >= today && checkOut <= nextSevenDays) {
|
|
accumulator.upcomingDepartures += 1;
|
|
}
|
|
|
|
return accumulator;
|
|
},
|
|
{
|
|
total: 0,
|
|
confirmed: 0,
|
|
checked_in: 0,
|
|
upcomingArrivals: 0,
|
|
upcomingDepartures: 0,
|
|
},
|
|
);
|
|
|
|
return [
|
|
{
|
|
label: 'Visible stays',
|
|
value: totals.total,
|
|
hint: 'Loaded in this calendar range',
|
|
},
|
|
{
|
|
label: 'Confirmed',
|
|
value: totals.confirmed,
|
|
hint: 'Booked and upcoming',
|
|
},
|
|
{
|
|
label: 'In house',
|
|
value: totals.checked_in,
|
|
hint: 'Currently checked in',
|
|
},
|
|
{
|
|
label: 'Departures soon',
|
|
value: totals.upcomingDepartures,
|
|
hint: 'Next 7 days',
|
|
},
|
|
];
|
|
}, [reservations]);
|
|
|
|
const generateFilterRequests = useMemo(() => {
|
|
let request = '&';
|
|
validFilterItems.forEach((item) => {
|
|
const isRangeFilter = filters.find(
|
|
(filter) =>
|
|
filter.title === item.fields.selectedField &&
|
|
(filter.number || filter.date),
|
|
);
|
|
|
|
if (isRangeFilter) {
|
|
const from = item.fields.filterValueFrom;
|
|
const to = item.fields.filterValueTo;
|
|
if (from) {
|
|
request += `${item.fields.selectedField}Range=${from}&`;
|
|
}
|
|
if (to) {
|
|
request += `${item.fields.selectedField}Range=${to}&`;
|
|
}
|
|
} else {
|
|
const value = item.fields.filterValue;
|
|
if (value) {
|
|
request += `${item.fields.selectedField}=${value}&`;
|
|
}
|
|
}
|
|
});
|
|
return request;
|
|
}, [filters, validFilterItems]);
|
|
|
|
const deleteFilter = (value) => {
|
|
const newItems = validFilterItems.filter((item) => item.id !== value);
|
|
|
|
if (newItems.length) {
|
|
setFilterItems(newItems);
|
|
} else {
|
|
loadData(0, '');
|
|
|
|
setFilterItems(newItems);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
loadData(0, generateFilterRequests);
|
|
|
|
};
|
|
|
|
const handleChange = (id) => (e) => {
|
|
const value = e.target.value;
|
|
const name = e.target.name;
|
|
|
|
setFilterItems(
|
|
validFilterItems.map((item) => {
|
|
if (item.id !== id) return item;
|
|
if (name === 'selectedField') return { id, fields: { [name]: value } };
|
|
|
|
return { id, fields: { ...item.fields, [name]: value } }
|
|
}),
|
|
);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setFilterItems([]);
|
|
loadData(0, '');
|
|
|
|
};
|
|
|
|
const onPageChange = (page: number) => {
|
|
loadData(page);
|
|
setCurrentPage(page);
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
if (!currentUser) return;
|
|
|
|
loadColumns(
|
|
handleDeleteModalAction,
|
|
`reservations`,
|
|
currentUser,
|
|
).then((newCols) => setColumns(newCols));
|
|
}, [currentUser]);
|
|
|
|
|
|
|
|
const handleTableSubmit = async (id: string, data) => {
|
|
|
|
if (!_.isEmpty(data)) {
|
|
await dispatch(update({ id, data }))
|
|
.unwrap()
|
|
.then((res) => res)
|
|
.catch((err) => {
|
|
throw new Error(err);
|
|
});
|
|
}
|
|
};
|
|
|
|
const onDeleteRows = async (selectedRows) => {
|
|
await dispatch(deleteItemsByIds(selectedRows));
|
|
await loadData(0);
|
|
};
|
|
|
|
const controlClasses =
|
|
'w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-slate-100 placeholder:text-slate-400 ' +
|
|
' ' + bgColor + ' ' + focusRing + ' ' + corners + ' ' +
|
|
'dark:bg-slate-800/80 my-1';
|
|
|
|
|
|
const dataGrid = (
|
|
<div className='relative overflow-x-auto'>
|
|
<DataGrid
|
|
autoHeight
|
|
rowHeight={56}
|
|
sx={dataGridStyles}
|
|
className={'datagrid--table'}
|
|
getRowClassName={() => `datagrid--row`}
|
|
rows={reservations ?? []}
|
|
columns={columns}
|
|
initialState={{
|
|
pagination: {
|
|
paginationModel: {
|
|
pageSize: 25,
|
|
},
|
|
},
|
|
columns: {
|
|
columnVisibilityModel: compactColumnVisibilityModel,
|
|
},
|
|
}}
|
|
disableRowSelectionOnClick
|
|
onProcessRowUpdateError={(params) => {
|
|
console.log('Error', params);
|
|
}}
|
|
processRowUpdate={async (newRow, oldRow) => {
|
|
const data = dataFormatter.dataGridEditFormatter(newRow);
|
|
|
|
try {
|
|
await handleTableSubmit(newRow.id, data);
|
|
return newRow;
|
|
} catch {
|
|
return oldRow;
|
|
}
|
|
}}
|
|
sortingMode={'server'}
|
|
checkboxSelection
|
|
onRowSelectionModelChange={(ids) => {
|
|
setSelectedRows(ids)
|
|
}}
|
|
onSortModelChange={(params) => {
|
|
params.length
|
|
? setSortModel(params)
|
|
: setSortModel([{ field: '', sort: 'desc' }]);
|
|
}}
|
|
rowCount={count}
|
|
pageSizeOptions={[25]}
|
|
paginationMode={'server'}
|
|
loading={loading}
|
|
onPaginationModelChange={(params) => {
|
|
onPageChange(params.page);
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{validFilterItems && Array.isArray(validFilterItems) && validFilterItems.length ?
|
|
<CardBox className='mb-6 border border-white/10 shadow-none'>
|
|
<Formik
|
|
initialValues={{
|
|
checkboxes: ['lorem'],
|
|
switches: ['lorem'],
|
|
radio: 'lorem',
|
|
}}
|
|
onSubmit={() => null}
|
|
>
|
|
<Form>
|
|
<>
|
|
{validFilterItems && validFilterItems.map((filterItem) => {
|
|
const showIncompleteHint = isFilterItemIncomplete(filterItem, filters)
|
|
const incompleteHint = showIncompleteHint ? getIncompleteFilterHint(filterItem, filters) : null
|
|
|
|
return (
|
|
<div key={filterItem.id} className="mb-3 grid gap-3 rounded-2xl border border-white/10 bg-white/5 p-4 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)_minmax(0,14rem)]">
|
|
<div className="flex min-w-0 flex-col">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Filter</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name='selectedField'
|
|
id='selectedField'
|
|
component='select'
|
|
value={filterItem?.fields?.selectedField || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
>
|
|
{filters.map((selectOption) => (
|
|
<option
|
|
key={selectOption.title}
|
|
value={`${selectOption.title}`}
|
|
>
|
|
{selectOption.label}
|
|
</option>
|
|
))}
|
|
</Field>
|
|
</div>
|
|
{filters.find((filter) =>
|
|
filter.title === filterItem?.fields?.selectedField
|
|
)?.type === 'enum' ? (
|
|
<div className="flex min-w-0 flex-col">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
|
Value
|
|
</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name="filterValue"
|
|
id='filterValue'
|
|
component="select"
|
|
value={filterItem?.fields?.filterValue || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
>
|
|
<option value="">Select Value</option>
|
|
{filters.find((filter) =>
|
|
filter.title === filterItem?.fields?.selectedField
|
|
)?.options?.map((option) => (
|
|
<option key={option} value={option}>
|
|
{option}
|
|
</option>
|
|
))}
|
|
</Field>
|
|
</div>
|
|
) : filters.find((filter) =>
|
|
filter.title === filterItem?.fields?.selectedField
|
|
)?.number ? (
|
|
<div className="grid min-w-0 gap-3 md:grid-cols-2">
|
|
<div className="flex min-w-0 flex-col">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">From</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name='filterValueFrom'
|
|
placeholder='From'
|
|
id='filterValueFrom'
|
|
value={filterItem?.fields?.filterValueFrom || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col w-full">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">To</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name='filterValueTo'
|
|
placeholder='to'
|
|
id='filterValueTo'
|
|
value={filterItem?.fields?.filterValueTo || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : filters.find(
|
|
(filter) =>
|
|
filter.title ===
|
|
filterItem?.fields?.selectedField
|
|
)?.date ? (
|
|
<div className='grid min-w-0 gap-3 md:grid-cols-2'>
|
|
<div className='flex flex-col w-full mr-3'>
|
|
<div className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400'>
|
|
From
|
|
</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name='filterValueFrom'
|
|
placeholder='From'
|
|
id='filterValueFrom'
|
|
type='datetime-local'
|
|
value={filterItem?.fields?.filterValueFrom || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
/>
|
|
</div>
|
|
<div className='flex flex-col w-full'>
|
|
<div className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400'>To</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name='filterValueTo'
|
|
placeholder='to'
|
|
id='filterValueTo'
|
|
type='datetime-local'
|
|
value={filterItem?.fields?.filterValueTo || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex min-w-0 flex-col">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Contains</div>
|
|
<Field
|
|
className={controlClasses}
|
|
name='filterValue'
|
|
placeholder='Contained'
|
|
id='filterValue'
|
|
value={filterItem?.fields?.filterValue || ''}
|
|
onChange={handleChange(filterItem.id)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col gap-2">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Action</div>
|
|
<BaseButton
|
|
className="my-1 w-full md:w-auto"
|
|
type='reset'
|
|
color='whiteDark'
|
|
outline
|
|
label='Delete'
|
|
onClick={() => {
|
|
deleteFilter(filterItem.id)
|
|
}}
|
|
/>
|
|
{incompleteHint ? (
|
|
<p className='rounded-xl border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-xs leading-5 text-amber-100'>
|
|
{incompleteHint}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<BaseButton
|
|
className="my-1 mr-0"
|
|
type='submit' color='info'
|
|
label='Apply'
|
|
onClick={handleSubmit}
|
|
/>
|
|
<BaseButton
|
|
className="my-1"
|
|
type='reset' color='whiteDark' outline
|
|
label='Cancel'
|
|
onClick={handleReset}
|
|
/>
|
|
</div>
|
|
</>
|
|
</Form>
|
|
</Formik>
|
|
</CardBox> : null
|
|
}
|
|
<CardBoxModal
|
|
title="Please confirm"
|
|
buttonColor="info"
|
|
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
|
|
isActive={isModalTrashActive}
|
|
onConfirm={handleDeleteAction}
|
|
onCancel={handleModalAction}
|
|
>
|
|
<p>Are you sure you want to delete this item?</p>
|
|
</CardBoxModal>
|
|
|
|
|
|
{!showGrid && (
|
|
<>
|
|
<div className='mb-6 grid gap-3 md:grid-cols-4'>
|
|
{reservationCalendarOverview.map((item) => (
|
|
<CardBox key={item.label} className='rounded-2xl border border-gray-200/80 shadow-none dark:border-dark-700'>
|
|
<div className='p-4'>
|
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-gray-400 dark:text-gray-500'>
|
|
{item.label}
|
|
</p>
|
|
<p className='mt-2 text-2xl font-semibold text-gray-900 dark:text-white'>
|
|
{item.value}
|
|
</p>
|
|
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>{item.hint}</p>
|
|
</div>
|
|
</CardBox>
|
|
))}
|
|
</div>
|
|
<BigCalendar
|
|
events={reservations}
|
|
showField={'reservation_code'}
|
|
start-data-key={'check_in_at'}
|
|
end-data-key={'check_out_at'}
|
|
handleDeleteAction={handleDeleteModalAction}
|
|
pathEdit={`/reservations/reservations-edit/?id=`}
|
|
pathView={`/reservations/reservations-view/?id=`}
|
|
handleCreateEventAction={handleCreateEventAction}
|
|
onDateRangeChange={(range) => {
|
|
loadData(0,`&calendarStart=${range.start}&calendarEnd=${range.end}`);
|
|
}}
|
|
isLoading={loading}
|
|
emptyTitle='No reservations in this range'
|
|
emptyDescription='Adjust the calendar window or add a reservation to start filling the schedule.'
|
|
entityName={'reservations'}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
|
|
|
|
{showGrid && dataGrid}
|
|
|
|
|
|
{selectedRows.length > 0 &&
|
|
createPortal(
|
|
<BaseButton
|
|
className='me-4'
|
|
color='danger'
|
|
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
|
|
onClick={() => onDeleteRows(selectedRows)}
|
|
/>,
|
|
document.getElementById('delete-rows-button'),
|
|
)}
|
|
<ToastContainer />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default TableSampleReservations
|