---
title: "React Scheduler Appointment Scheduling Example | Mobiscroll"
component: "Scheduler"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/scheduler/doctors-appointment"
---

# Appointment Scheduling - React Scheduler demo

Build a React appointment scheduler — assign doctors or dentists, drag from an unscheduled queue, and prevent double-booking. For React and Next.JS.

- [Live demo](https://demo.mobiscroll.com/react/scheduler/doctors-appointment)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/scheduler/doctors-appointment)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/scheduler/doctors-appointment)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/scheduler/doctors-appointment.md), [jQuery](https://demo.mobiscroll.com/jquery/scheduler/doctors-appointment.md), [Angular](https://demo.mobiscroll.com/angular/scheduler/doctors-appointment.md), [Vue](https://demo.mobiscroll.com/vue/scheduler/doctors-appointment.md)

## Description

The scheduler can be set up for scheduling/unscheduling booked appointments. Simply set up a daily schedule view with multiple resources representing dentists or doctors. Next to the calendar you can render a list of unscheduled appointments.

Enabling [advanced validation](https://demo.mobiscroll.com/react/scheduler/time-off-blocked-ranges) and combining that with the [drag & drop between the calendar and external list](https://demo.mobiscroll.com/react/scheduler/external-drag-drop-sortable-dragula) on a resource to resource basis you got a pretty nice working base for appointment scheduling that can be further customized.

## What this demo shows

- A desktop daily scheduler for doctor or dentist appointments, with date navigation, multiple resources, a scrollable time grid, and an external list of unscheduled appointments.
- **Header navigation** The month and year label in the top left opens date navigation, while the blue previous and next arrows and the Today button on the right make it easy to move between dates and jump back to the current day.
- **Day header** The curent or selected date and short weekday name are shown below the main navigation.
- **Resource strip** Multiple resources are shown below the date header, representing dentists or doctors.
- **Time grid** The scheduler displays a vertical day grid from 8AM to 8PM for the selected day.
- **Current time** A blue horizontal line marks the current time across the scheduler.
- **Past hours** Time ranges before the current time are disabled and shown with a gray background.
- **Disabled past interactions** Disabled past hours block event creation and drag-and-drop interactions in those time ranges.
- **Past events** Events scheduled in past hours remain visible, but they cannot be moved or repositioned.
- **Failed past updates** When a user tries to create or move an event into a past time range, a bottom-center toast appears with the message: `Can't add event in the past`.
- **Hover feedback** Hovering the time grid shows a time indicator that follows the pointer in 15-minute increments.
- **External task list** A right-side panel titled `Unscheduled appointments` shows four unscheduled appointments by default with different colors: `Winfred Lesley - Teeth whitening 1.5 hours` - yellow, `Rosaline Delice - Crown and bridge 2 hours` - green, `Macy Steven - Root canal treatment` - red,`Lavern Cameron / Rartar removal 1 hour` - purple.
- **External drag and drop** Appointments can be dragged from the external list onto the scheduler to add appointments.
- **Unscheduling events** Scheduled appointments can be dragged from the scheduler and dropped back onto the external list.
- **Confirmation messages** When an appointment is scheduled from the external list, a bottom-center toast confirms that the client was added. When an appointment is moved back to the external list, a toast confirms that the client was unscheduled.
- **External task list state** When a scheduled event is dragged over the external list, the panel background changes to light green.
- **Event rendering** Events appear as colored cards with a colored stripe on the left, the event time above the title which is the patient name shown in bold.
- **Event overlapping** Events cannot overlap. If a user tries to create, move, or resize an event and it overlaps another scheduled event, a bottom-center toast appears with the message: `Make sure not to double book`.
- **Event interactions** Hovering over a future event highlights it and shows drag and resize handles, indicating that it can be moved or resized.
- **Event creation** On future hours, double-clicking the time grid or clicking and dragging on it creates a new event and shows a bottom-center toast with the message: `New event added`.
- **Event selection** Clicking an event selects and highlights it.

## Best for

- **Dental clinics and dental practices** Schedule patients with specific dentists or hygienists while keeping new or unconfirmed appointments in an unscheduled queue until a resource becomes available.
- **Medical offices and healthcare providers** Manage consultations, treatments, examinations, and follow-up visits across multiple doctors, nurses, or specialists.
- **Veterinary clinics** Coordinate appointments across veterinarians, treatment rooms, and support staff while tracking patients waiting to be scheduled.
- **Beauty salons and spas** Assign customers to stylists, therapists, or treatment rooms and drag unscheduled bookings into available time slots.
- **Physical therapy and rehabilitation centers** Schedule therapists, treatment sessions, and equipment while maintaining visibility into pending appointments.
- **Automotive service centers** Organize repair jobs, inspections, and maintenance appointments across service technicians and service bays.
- **Professional service businesses** Manage consultations, assessments, interviews, or client meetings across multiple specialists or advisors.
- **Home services and field operations** Schedule technicians, installers, inspectors, or maintenance crews and assign incoming jobs from a backlog.
- **Call centers and customer appointment teams** Coordinate customer appointments across available agents while tracking unassigned requests.
- **Educational and tutoring services** Match students with instructors, classrooms, or training resources and schedule sessions as availability opens up.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`invalid`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-invalid) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`eventOverlap`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventOverlap) - option
- [`externalDrop`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalDrop) - option
- [`externalDrag`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-externalDrag) - option
- [`colors`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-colors) - option
- [`onEventCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreate) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventCreateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreateFailed) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdateFailed) - event
- [`onEventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDelete) - event
- [`onEventDragEnter`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDragEnter) - event
- [`onEventDragLeave`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDragLeave) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

```jsx
import { Draggable, Dropcontainer, Eventcalendar, setOptions, Toast } from '@mobiscroll/react';
import PropTypes from 'prop-types';
import { useCallback, useMemo, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

const now = new Date();
const today = new Date(now.setMinutes(59));
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);

const Appointment = (props) => {
  const [draggable, setDraggable] = useState();

  const setDragElm = useCallback((elm) => {
    setDraggable(elm);
  }, []);

  const event = props.data;
  const eventLength = Math.abs(new Date(event.end).getTime() - new Date(event.start).getTime()) / (60 * 60 * 1000);

  return (
    <div>
      {!event.hide && (
        <div ref={setDragElm} className="docs-appointment-task" style={{ background: event.color }}>
          <div>{event.title}</div>
          <div>{eventLength + ' hour' + (eventLength > 1 ? 's' : '')}</div>
          <Draggable dragData={event} element={draggable} />
        </div>
      )}
    </div>
  );
};

Appointment.propTypes = {
  data: PropTypes.object.isRequired,
};

function App() {
  const doctors = useMemo(
    () => [
      { id: 1, name: 'Dr. Keila Delores' },
      { id: 2, name: 'Dr. Gene Cortez' },
      { id: 3, name: 'Dr. Paula Bush' },
      { id: 4, name: 'Dr. Pete Nichols' },
      { id: 5, name: 'Dr. Jean Pearson' },
      { id: 6, name: 'Dr. Thelma Cain' },
    ],
    [],
  );

  const myView = useMemo(
    () => ({
      scheduler: {
        type: 'day',
        startTime: '08:00',
        endTime: '20:00',
        allDay: false,
      },
    }),
    [],
  );

  const myInvalid = useMemo(() => [{ recurring: { repeat: 'daily', until: yesterday } }, { start: yesterday, end: today }], []);

  const [myEvents, setEvents] = useState(
    [
      {
        id: 'job1',
        start: '2026-10-24T14:00',
        end: '2026-10-24T16:00',
        resource: 1,
        title: 'Myla Bennett',
        job: 'Wisdom tooth removal',
        color: '#334ab9',
      },
      {
        id: 'job2',
        start: '2026-10-24T17:00',
        end: '2026-10-24T18:30',
        resource: 1,
        title: 'Beatrix Foley',
        job: 'Braces',
        color: '#177e70',
      },
      {
        id: 'job3',
        start: '2026-10-24T08:00',
        end: '2026-10-24T09:30',
        resource: 3,
        title: 'Frank Watson',
        job: 'Teeth whitening',
        color: '#d1891f',
      },
      {
        id: 'job4',
        start: '2026-10-24T10:00',
        end: '2026-10-24T12:30',
        resource: 3,
        title: 'Jaime Joyce',
        job: 'Root canal treatment',
        color: '#cb3939',
      },
      {
        id: 'job5',
        start: '2026-10-24T13:00',
        end: '2026-10-24T14:00',
        resource: 3,
        title: 'Corey Shepard',
        job: 'Tooth extraction',
        color: '#aba343',
      },
      {
        id: 'job6',
        start: '2026-10-24T14:00',
        end: '2026-10-24T16:00',
        resource: 4,
        title: 'Callie Leonard',
        job: 'Crown and bridge',
        color: '#1ca11a',
      },
      {
        id: 'job7',
        start: '2026-10-24T17:00',
        end: '2026-10-24T18:00',
        resource: 4,
        title: 'Harley Thomson',
        job: 'Tartar removal',
        color: '#a446b5',
      },
      {
        id: 'job8',
        start: '2026-10-24T09:00',
        end: '2026-10-24T11:00',
        resource: 6,
        title: 'Ricky Welch',
        job: 'Wisdom tooth removal',
        color: '#334ab9',
      },
    ].map((event) => {
      const start = event.start ? new Date(event.start) : event.start;
      const end = event.end ? new Date(event.end) : event.end;
      return {
        ...event,
        start,
        end,
        // Mark past events as fixed by setting the event.editable property to false
        editable: !!(start && today < start),
      };
    }),
  );

  const [appointments, setAppointments] = useState([
    {
      id: 'd1',
      title: 'Winfred Lesley',
      job: 'Teeth whitening',
      color: '#d1891f',
      start: '2026-10-24T08:00',
      end: '2026-10-24T09:30',
      unscheduled: true,
    },
    {
      id: 'd2',
      title: 'Rosalin Delice',
      job: 'Crown and bridge',
      color: '#1ca11a',
      start: '2026-10-24T08:00',
      end: '2026-10-24T10:00',
      unscheduled: true,
    },
    {
      id: 'd3',
      title: 'Macy Steven',
      job: 'Root canal treatment',
      color: '#cb3939',
      start: '2026-10-24T10:00',
      end: '2026-10-24T12:30',
      unscheduled: true,
    },
    {
      id: 'd4',
      title: 'Lavern Cameron',
      job: 'Tartar removal',
      color: '#a446b5',
      start: '2026-10-24T12:00',
      end: '2026-10-24T13:00',
      unscheduled: true,
    },
  ]);

  const [contBg, setContBg] = useState('');
  const [myColors, setColors] = useState([]);
  const [dropCont, setDropCont] = useState();
  const [toastMessage, setToastMessage] = useState('');
  const [isToastOpen, setToastOpen] = useState(false);

  const setDropElm = useCallback((elm) => {
    setDropCont(elm);
  }, []);

  const handleEventCreate = useCallback((args) => {
    const event = args.event;
    event.unscheduled = false;
    setColors([]);
  }, []);

  const handleEventCreated = useCallback((args) => {
    setToastMessage(args.event.title + ' added');
    setToastOpen(true);
    setEvents((prevEvents) => [...prevEvents, args.event]);
    setAppointments((prevAppointments) => prevAppointments.filter((item) => item.id !== args.event.id));
  }, []);

  const handleFailed = useCallback((event) => {
    if (event.start <= today) {
      setToastMessage("Can't add event in the past");
    } else {
      setToastMessage('Make sure not to double book');
    }
    setToastOpen(true);
  }, []);

  const handleEventCreateFailed = useCallback(
    (args) => {
      handleFailed(args.event);
    },
    [handleFailed],
  );

  const handleEventUpdateFailed = useCallback(
    (args) => {
      handleFailed(args.event);
    },
    [handleFailed],
  );

  const handleEventDelete = useCallback((args) => {
    setToastMessage(args.event.title + ' unscheduled');
    setToastOpen(true);
    setEvents((prevEvents) => prevEvents.filter((item) => item.id !== args.event.id));
  }, []);

  const handleEventDragEnter = useCallback(() => {
    setColors([
      {
        background: '#f1fff24d',
        start: '08:00',
        end: '20:00',
        recurring: {
          repeat: 'daily',
        },
      },
    ]);
  }, []);

  const handleEventDragLeave = useCallback(() => {
    setColors([]);
  }, []);

  const handleItemDrop = useCallback((args) => {
    if (args.data) {
      args.data.unscheduled = true;
      setAppointments((prevAppointments) => [...prevAppointments, args.data]);
    }
    setContBg('');
  }, []);

  const handleItemDragEnter = useCallback((args) => {
    if (!(args.data && args.data.unscheduled)) {
      setContBg('#d0e7d2cc');
    }
  }, []);

  const handleItemDragLeave = useCallback(() => {
    setContBg('');
  }, []);

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  return (
    <div className="mbsc-grid mbsc-no-padding">
      <div className="mbsc-row">
        <div className="mbsc-col-sm-9 docs-appointment-calendar">
          <Eventcalendar
            data={myEvents}
            view={myView}
            resources={doctors}
            invalid={myInvalid}
            dragToMove={true}
            dragToCreate={true}
            eventOverlap={false}
            externalDrop={true}
            externalDrag={true}
            colors={myColors}
            onEventCreate={handleEventCreate}
            onEventCreated={handleEventCreated}
            onEventCreateFailed={handleEventCreateFailed}
            onEventUpdateFailed={handleEventUpdateFailed}
            onEventDelete={handleEventDelete}
            onEventDragEnter={handleEventDragEnter}
            onEventDragLeave={handleEventDragLeave}
          />
          <Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
        </div>
        <div className="mbsc-col-sm-3 docs-appointment-cont" ref={setDropElm} style={{ backgroundColor: contBg }}>
          <Dropcontainer
            onItemDrop={handleItemDrop}
            onItemDragEnter={handleItemDragEnter}
            onItemDragLeave={handleItemDragLeave}
            element={dropCont}
          >
            <div className="mbsc-form-group-title">Unscheduled appointments</div>
            {appointments.map((app) => (
              <Appointment key={app.id} data={app} />
            ))}
          </Dropcontainer>
        </div>
      </div>
    </div>
  );
}

export default App;
```

### TSX

```tsx
import {
  Draggable,
  Dropcontainer,
  Eventcalendar,
  MbscCalendarColor,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscEventCreatedEvent,
  MbscEventCreateEvent,
  MbscEventCreateFailedEvent,
  MbscEventDeleteEvent,
  MbscEventUpdateFailedEvent,
  MbscItemDragEvent,
  MbscResource,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

const now = new Date();
const today = new Date(now.setMinutes(59));
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);

const Appointment: FC<{ data: MbscCalendarEvent }> = (props) => {
  const [draggable, setDraggable] = useState<HTMLElement | null>();

  const setDragElm = useCallback((elm: HTMLElement | null) => {
    setDraggable(elm);
  }, []);

  const event = props.data;
  const eventLength = Math.abs(new Date(event.end as string).getTime() - new Date(event.start as string).getTime()) / (60 * 60 * 1000);

  return (
    <div>
      {!event.hide && (
        <div ref={setDragElm} className="docs-appointment-task" style={{ background: event.color }}>
          <div>{event.title}</div>
          <div>{eventLength + ' hour' + (eventLength > 1 ? 's' : '')}</div>
          <Draggable dragData={event} element={draggable} />
        </div>
      )}
    </div>
  );
};

const App: FC = () => {
  const doctors = useMemo<MbscResource[]>(
    () => [
      { id: 1, name: 'Dr. Keila Delores' },
      { id: 2, name: 'Dr. Gene Cortez' },
      { id: 3, name: 'Dr. Paula Bush' },
      { id: 4, name: 'Dr. Pete Nichols' },
      { id: 5, name: 'Dr. Jean Pearson' },
      { id: 6, name: 'Dr. Thelma Cain' },
    ],
    [],
  );

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      scheduler: {
        type: 'day',
        startTime: '08:00',
        endTime: '20:00',
        allDay: false,
      },
    }),
    [],
  );

  const myInvalid = useMemo(() => [{ recurring: { repeat: 'daily', until: yesterday } }, { start: yesterday, end: today }], []);

  const [myEvents, setEvents] = useState<MbscCalendarEvent[]>([
    {
      id: 'job1',
      start: '2026-10-24T14:00',
      end: '2026-10-24T16:00',
      resource: 1,
      title: 'Myla Bennett',
      job: 'Wisdom tooth removal',
      color: '#334ab9',
    },
    {
      id: 'job2',
      start: '2026-10-24T17:00',
      end: '2026-10-24T18:30',
      resource: 1,
      title: 'Beatrix Foley',
      job: 'Braces',
      color: '#177e70',
    },
    {
      id: 'job3',
      start: '2026-10-24T08:00',
      end: '2026-10-24T09:30',
      resource: 3,
      title: 'Frank Watson',
      job: 'Teeth whitening',
      color: '#d1891f',
    },
    {
      id: 'job4',
      start: '2026-10-24T10:00',
      end: '2026-10-24T12:30',
      resource: 3,
      title: 'Jaime Joyce',
      job: 'Root canal treatment',
      color: '#cb3939',
    },
    {
      id: 'job5',
      start: '2026-10-24T13:00',
      end: '2026-10-24T14:00',
      resource: 3,
      title: 'Corey Shepard',
      job: 'Tooth extraction',
      color: '#aba343',
    },
    {
      id: 'job6',
      start: '2026-10-24T14:00',
      end: '2026-10-24T16:00',
      resource: 4,
      title: 'Callie Leonard',
      job: 'Crown and bridge',
      color: '#1ca11a',
    },
    {
      id: 'job7',
      start: '2026-10-24T17:00',
      end: '2026-10-24T18:00',
      resource: 4,
      title: 'Harley Thomson',
      job: 'Tartar removal',
      color: '#a446b5',
    },
    {
      id: 'job8',
      start: '2026-10-24T09:00',
      end: '2026-10-24T11:00',
      resource: 6,
      title: 'Ricky Welch',
      job: 'Wisdom tooth removal',
      color: '#334ab9',
    },
  ]);

  const [appointments, setAppointments] = useState<MbscCalendarEvent[]>(
    [
      {
        id: 'd1',
        title: 'Winfred Lesley',
        job: 'Teeth whitening',
        color: '#d1891f',
        start: '2026-10-24T08:00',
        end: '2026-10-24T09:30',
        unscheduled: true,
      },
      {
        id: 'd2',
        title: 'Rosalin Delice',
        job: 'Crown and bridge',
        color: '#1ca11a',
        start: '2026-10-24T08:00',
        end: '2026-10-24T10:00',
        unscheduled: true,
      },
      {
        id: 'd3',
        title: 'Macy Steven',
        job: 'Root canal treatment',
        color: '#cb3939',
        start: '2026-10-24T10:00',
        end: '2026-10-24T12:30',
        unscheduled: true,
      },
      {
        id: 'd4',
        title: 'Lavern Cameron',
        job: 'Tartar removal',
        color: '#a446b5',
        start: '2026-10-24T12:00',
        end: '2026-10-24T13:00',
        unscheduled: true,
      },
    ].map((event) => {
      const start = event.start ? new Date(event.start as string) : event.start;
      const end = event.end ? new Date(event.end as string) : event.end;
      return {
        ...event,
        start,
        end,
        // Mark past events as fixed by setting the event.editable property to false
        editable: !!(start && today < start),
      };
    }),
  );

  const [contBg, setContBg] = useState('');
  const [myColors, setColors] = useState<MbscCalendarColor[]>([]);
  const [dropCont, setDropCont] = useState<HTMLElement | null>();
  const [toastMessage, setToastMessage] = useState('');
  const [isToastOpen, setToastOpen] = useState(false);

  const setDropElm = useCallback((elm: HTMLElement | null) => {
    setDropCont(elm);
  }, []);

  const handleEventCreate = useCallback((args: MbscEventCreateEvent) => {
    const event = args.event;
    event.unscheduled = false;
    setColors([]);
  }, []);

  const handleEventCreated = useCallback((args: MbscEventCreatedEvent) => {
    setToastMessage(args.event.title + ' added');
    setToastOpen(true);
    setEvents((prevEvents) => [...prevEvents, args.event]);
    setAppointments((prevAppointments) => prevAppointments.filter((item) => item.id !== args.event.id));
  }, []);

  const handleFailed = useCallback((event: MbscCalendarEvent) => {
    if (event.start! <= today) {
      setToastMessage("Can't add event in the past");
    } else {
      setToastMessage('Make sure not to double book');
    }
    setToastOpen(true);
  }, []);

  const handleEventCreateFailed = useCallback(
    (args: MbscEventCreateFailedEvent) => {
      handleFailed(args.event);
    },
    [handleFailed],
  );

  const handleEventUpdateFailed = useCallback(
    (args: MbscEventUpdateFailedEvent) => {
      handleFailed(args.event);
    },
    [handleFailed],
  );

  const handleEventDelete = useCallback((args: MbscEventDeleteEvent) => {
    setToastMessage(args.event.title + ' unscheduled');
    setToastOpen(true);
    setEvents((prevEvents) => prevEvents.filter((item) => item.id !== args.event.id));
  }, []);

  const handleEventDragEnter = useCallback(() => {
    setColors([
      {
        background: '#f1fff24d',
        start: '08:00',
        end: '20:00',
        recurring: {
          repeat: 'daily',
        },
      },
    ]);
  }, []);

  const handleEventDragLeave = useCallback(() => {
    setColors([]);
  }, []);

  const handleItemDrop = useCallback((args: MbscItemDragEvent) => {
    if (args.data) {
      args.data.unscheduled = true;
      setAppointments((prevAppointments) => [...prevAppointments, args.data]);
    }
    setContBg('');
  }, []);

  const handleItemDragEnter = useCallback((args: MbscItemDragEvent) => {
    if (!(args.data && args.data.unscheduled)) {
      setContBg('#d0e7d2cc');
    }
  }, []);

  const handleItemDragLeave = useCallback(() => {
    setContBg('');
  }, []);

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  return (
    <div className="mbsc-grid mbsc-no-padding">
      <div className="mbsc-row">
        <div className="mbsc-col-sm-9 docs-appointment-calendar">
          <Eventcalendar
            data={myEvents}
            view={myView}
            resources={doctors}
            invalid={myInvalid}
            dragToMove={true}
            dragToCreate={true}
            eventOverlap={false}
            externalDrop={true}
            externalDrag={true}
            colors={myColors}
            onEventCreate={handleEventCreate}
            onEventCreated={handleEventCreated}
            onEventCreateFailed={handleEventCreateFailed}
            onEventUpdateFailed={handleEventUpdateFailed}
            onEventDelete={handleEventDelete}
            onEventDragEnter={handleEventDragEnter}
            onEventDragLeave={handleEventDragLeave}
          />
          <Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
        </div>
        <div className="mbsc-col-sm-3 docs-appointment-cont" ref={setDropElm} style={{ backgroundColor: contBg }}>
          <Dropcontainer
            onItemDrop={handleItemDrop}
            onItemDragEnter={handleItemDragEnter}
            onItemDragLeave={handleItemDragLeave}
            element={dropCont}
          >
            <div className="mbsc-form-group-title">Unscheduled appointments</div>
            {appointments.map((app) => (
              <Appointment key={app.id} data={app} />
            ))}
          </Dropcontainer>
        </div>
      </div>
    </div>
  );
};
export default App;
```

### CSS

```css
.docs-appointment-calendar {
  border-right: 1px solid #ccc;
}

.docs-appointment-calendar .mbsc-readonly-event {
  opacity: 0.6;
}

.docs-appointment-cont {
  height: 100%;
  overflow: auto;
}

.docs-appointment-task {
  color: #fff;
  background: #999;
  padding: 10px;
  margin: 20px;
  border-radius: 8px;
  font-family:
    -apple-system,
    Segoe UI,
    Roboto,
    sans-serif;
}

.demo-doctors-appointment.demo-wrapper,
.demo-doctors-appointment .mbsc-grid,
.demo-doctors-appointment .mbsc-row,
.demo-doctors-appointment .docs-appointment-calendar {
  height: 100%;
}
```

## Implementation notes

- Use `type: 'day'` with `startTime: '08:00'`, `endTime: '20:00'`, and `allDay: false`. Define resources as a flat list of doctors, each with an `id` and `name`.
- Block past time with two `invalid` entries: a `recurring: { repeat: 'daily', until: yesterday }` entry for all days before today, and a `{ start: yesterday, end: today }` entry where `today` is the current `Date` with minutes set to `59` so the current hour is fully blocked. On mount, also set `editable: false` on every scheduled event whose `start` is before `today` to lock past appointments in place.
- Set `dragToMove`, `dragToCreate`, `eventOverlap={false}`, `externalDrop`, and `externalDrag` on the `Eventcalendar` to enable full bidirectional drag between the calendar and the external sidebar.
- Render each unscheduled appointment as a component that uses a ref-callback to obtain its DOM element and mounts a `Draggable` with `dragData={event}` and `element={elem}`. Conditionally render the card only when `!event.hide` - Mobiscroll sets `hide: true` during drag to prevent the source card and the drag ghost from both showing.
- Wrap the unscheduled appointments sidebar in a `Dropcontainer` connected via a ref-callback on the container div. In `onItemDrop`, set `unscheduled: true` on `args.data` and push it into the appointments list. In `onItemDragEnter`, apply a green background to the container only when `args.data.unscheduled` is false (calendar event being dragged back). Clear the background in `onItemDragLeave`.
- In `onEventCreate`, set `event.unscheduled = false` directly on the event object and clear the `colors` array. In `onEventCreated`, add the event to the scheduled list, remove it from the appointments list by ID, and show a `Toast`.
- In `onEventDragEnter` (fires when an external draggable enters the calendar), set a `colors` entry with a green tint spanning `'08:00'–'20:00'` with `recurring: { repeat: 'daily' }` to highlight valid drop targets. Clear it in `onEventDragLeave`.
- In `onEventCreateFailed` and `onEventUpdateFailed`, show a `Toast` whose text depends on whether `event.start <= today` ("Can't add event in the past") or not ("Make sure not to double book"). In `onEventDelete`, show an "unscheduled" `Toast` and remove the event from the scheduled list.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-react.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config react
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config react --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/react/getting-started/installation

## Related demos

- Previous: [Work Week](https://demo.mobiscroll.com/react/scheduler/work-week-hours.md) - Configure the React Scheduler with blocked break times — set up a business-hours scheduler in one view.
- Next: [Doctors Availability](https://demo.mobiscroll.com/react/scheduler/doctors-availability-calendar-planner.md) - Display per-doctor availability in React Scheduler — color-coded tracks mark open slots per doctor, automatically respecting working hours.
