import { Calendar, dateFnsLocalizer } from 'react-big-calendar' import { format, parse, startOfWeek as dateFnsStartOfWeek, getDay, fromUnixTime, getUnixTime, addMinutes, startOfMonth, endOfWeek, endOfMonth, } from 'date-fns' import * as locales from 'date-fns/locale' import 'react-big-calendar/lib/css/react-big-calendar.css' import './CalendarScreen.scss' import { useCallback, useMemo, useState } from 'react' import { createFormFromModel } from '../../components/Form/lib/createForm' import { useSidebar } from '../../components/Sidebar/SidebarContext' import { Form } from '../../components/Form/Form' import { dispatch, select, useDispatch, useSelect } from '@wordpress/data' import { store_name } from '../../../store/backend' import { createFormMenuSectionsFromModel } from '../../components/Form/utils/utils' import { removePrefixesFromModelFields } from '../../components/WebbaDataTable/utils' import BookingsModel from '../../../schemas/appointments.json' import { stripIncompleteBookingExtrasForSubmit } from '../../components/Form/Fields/ExtrasSelectorField/ExtrasSelectorField' import { FilterForm } from '../../components/Filter/FilterForm' import { filterFields } from './FilterConfigs' import { __ } from '@wordpress/i18n' import { toZonedTime } from 'date-fns-tz' import { formatWbkDate } from '../../components/Filter/utils' import { Button } from '../../components/Button/Button' import metadata from '../../../schemas/appointments.json' import { TAllowedFilterValue } from '../../components/Filter/types' import { increaseOpacity } from '../../components/Form/Fields/ColorField/utils' import classNames from 'classnames' import { weekDaysSlugs } from '../../components/Form/utils/dateTime' const bookingsModel = removePrefixesFromModelFields( BookingsModel, 'appointment_' ) const form = createFormFromModel(bookingsModel) const sanitizeBookingFormPayload = (data: Record) => { const payload = { ...data } if (typeof payload.booking_extra === 'string') { payload.booking_extra = stripIncompleteBookingExtrasForSubmit(payload.booking_extra) } return payload } const menuSections = createFormMenuSectionsFromModel({ model: bookingsModel, form, modelName: 'appointments', }) export const CalendarScreen = () => { const sidebar = useSidebar() const { filterItems } = useDispatch(store_name) const [currentView, setCurrentView] = useState('month') const allStatus: Record = useMemo( () => metadata.properties?.appointment_status.misc?.options, [] ) const { settings, admin_url } = useSelect( // @ts-ignore (select) => select(store_name).getPreset(), [] ) const weekStart = useMemo(() => { const weekStartValue = settings?.week_start if (weekStartValue === undefined || weekStartValue === null) { return 0 } const valueStr = String(weekStartValue).toLowerCase() if (weekDaysSlugs.hasOwnProperty(valueStr)) { return weekDaysSlugs[valueStr] } const parsed = parseInt(String(weekStartValue), 10) const result = isNaN(parsed) ? 0 : parsed return result }, [settings?.week_start]) const customStartOfWeek = useCallback( (date: Date, locale?: any) => { return dateFnsStartOfWeek(date, { weekStartsOn: weekStart as 0 | 1 | 2 | 3 | 4 | 5 | 6, locale, }) }, [weekStart] ) const customEndOfWeek = useCallback( (date: Date, locale?: any) => { return endOfWeek(date, { weekStartsOn: weekStart as 0 | 1 | 2 | 3 | 4 | 5 | 6, locale, }) }, [weekStart] ) const localizer = useMemo( () => dateFnsLocalizer({ format, parse, startOfWeek: customStartOfWeek, getDay, locales, }), [customStartOfWeek] ) const bookings = useSelect( (select) => // @ts-ignore select(store_name).getItems('appointments', [ { name: 'appointment_day', value: formatWbkDate( customStartOfWeek(startOfMonth(new Date())) ), }, { name: 'appointment_day', value: formatWbkDate( customEndOfWeek(endOfMonth(new Date())) ), }, { name: 'appointment_status', value: Object.keys(allStatus).filter( (status) => status === 'approved' ), }, ]), [customStartOfWeek, customEndOfWeek, allStatus] ) const services = useSelect( // @ts-ignore (select) => select(store_name).getItems('services'), [] ) const { deleteItems, setItem, addItem }: any = dispatch(store_name) const getBookingFromEvent = useCallback( (event: IEvent) => { return bookings.find((booking: any) => booking.id == event.id) }, [bookings] ) const onDelete = useCallback(async (id: number) => { await deleteItems('appointments', [id]) sidebar.close() }, []) const onSubmit = useCallback(async (update: any, id: number) => { const cleaned = sanitizeBookingFormPayload(update) await setItem('appointments', { ...cleaned, id }) }, []) const onDuplicate = useCallback(async (data: any) => { const newId = Number(data.id) + 1 const update = { ...data, name: `Copy of ${data.name}`, id: String(newId), } await addItem('appointments', update) sidebar.close() }, []) const handleEventClick = useCallback( (event: IEvent) => { sidebar.open(
onSubmit(data, event.id)} onDelete={() => onDelete(event.id)} onDuplicate={() => onDuplicate(getBookingFromEvent(event))} /> ) }, [bookings] ) const addBooking = async (data: any) => { try { return await addItem('appointments', data) } catch (e) { console.error(e) } } const handleAddBookingClick = () => { sidebar.open( { return await addBooking(sanitizeBookingFormPayload(data)) }} /> ) } const messages = useMemo(() => { return { allDay: __('All Day', 'webba-booking-lite'), previous: '<', next: '>', today: __('Today', 'webba-booking-lite'), month: __('Month', 'webba-booking-lite'), week: __('Week', 'webba-booking-lite'), day: __('Day', 'webba-booking-lite'), agenda: __('Agenda', 'webba-booking-lite'), date: __('Date', 'webba-booking-lite'), time: __('Time', 'webba-booking-lite'), event: __('Event', 'webba-booking-lite'), showMore: (total: number) => __(`+ (${total}) Events`, 'webba-booking-lite'), } }, []) const events = useMemo(() => { return bookings.map((booking: any) => { let calculatedEnd = null if (!booking?.end) { const duration = select(store_name) .getItems('services', []) .find( (service: any) => service.id === booking.service_id )?.duration calculatedEnd = getUnixTime( addMinutes(fromUnixTime(booking.time), duration) ) } return { id: booking?.id.toString(), title: booking?.extra_data?.dynamic_title || booking.name, start: toZonedTime( fromUnixTime(booking.time), settings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone ), end: toZonedTime( fromUnixTime(booking?.end ? booking?.end : calculatedEnd), settings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone ), status: booking?.status, color: services.find( (service: any) => service?.id == booking?.service_id )?.color || 'transparent', } }) }, [bookings, services]) const [customFilter, setCustomFilter] = useState< TAllowedFilterValue[] >([ { name: 'appointment_day', value: formatWbkDate(customStartOfWeek(startOfMonth(new Date()))), }, { name: 'appointment_day', value: formatWbkDate(customEndOfWeek(endOfMonth(new Date()))), }, ]) const updateRange = useCallback( (fullRange: Date[] | Record<'start' | 'end', Date>) => { const formattedRange: [Date, Date] = Array.isArray(fullRange) ? [fullRange[0], fullRange[fullRange.length - 1]] : [fullRange.start, fullRange.end] const query = generateFilterFromDateRange(formattedRange) setCustomFilter(query) filterItems('appointments', query) }, [customFilter] ) const generateFilterFromDateRange = useCallback( (formattedRange: [Date, Date]) => { return [ { name: 'appointment_day', value: formatWbkDate(formattedRange[0]), }, { name: 'appointment_day', value: formatWbkDate(formattedRange[1]), }, ...customFilter.filter( (filter: TAllowedFilterValue) => filter.name !== 'appointment_day' ), ] }, [customFilter] ) const EventWrapper = useCallback( ({ event, children }: any) => (
{children}
), [] ) return (
{ updateRange(fullRange) if (view) { setCurrentView(view as string) } }} components={{ eventWrapper: EventWrapper, event: (props: any) => { // Only customize for day view if (props.isAllDay || props.view !== 'day') { // Use default rendering for other views return {props.title} } // Use the event color with opacity if available, fallback to default const bgColor = props.event.color ? increaseOpacity(props.event.color, 0.5) : undefined return (
{props.event.title}
) }, }} dayLayoutAlgorithm="no-overlap" // Track current view to help with event rendering view={currentView as any} onView={(view) => setCurrentView(view)} />
) }