24-Hour Rotating Crew Shift Rota
Usage
theme="ios" You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant="light" You can turn auto dark mode on based on system settings.
Usage
clickToCreate={true}
// click to create is on dragToCreate={true}
// drag to create is on dragToMove={true}
// reordering is on dragToResize={true}
// resizing is on eventDelete={true}
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale={localeEn} Selecting the locale loads the localized settings and the translated UI text.
24-Hour Rotating Crew Shift Rota
The scheduler can be configured for industrial or service environments requiring 24/7 coverage, such as manufacturing lines or critical infrastructure maintenance. This setup uses three resources (Crew A, Crew B, Crew C) to demonstrate a cyclical, rotating roster, like the 3-Crew, 3-Shift Rota.
To visualize the continuous operation and manage shifts that span midnight, the scheduler view is configured with a custom, 24-hour cycle, starting at 06:00 and ending at 06:00 the following day. This is achieved using the custom day-offset feature added to the startTime (e.g., '20:00-1') and endTime (e.g., '08:00+1') properties under the view option, which initiates the view relative to the previous or next calendar day. Furthermore, to ensure compliance with 8-hour shift lengths and prevent invalid scheduling, the drag-and-drop functionality is constrained by setting the dragTimeStep to 8 hours, requiring all events to be moved only in 8-hour increments and anchoring the alignment with the dragTimeStepBase: 'viewStart' option. The drag-and-drop logic is implemented for automatic conflict resolution: when an event is dragged to a different time or resource column within the same day, it automatically updates and shifts any conflicting events (those sharing the same time slot or resource column) to maintain scheduling integrity and prevent overlaps.
Demo

Code
View code on GitHub
import { Eventcalendar, setOptions, Toast } from '@mobiscroll/react';
import { useCallback, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const morningColor = '#4a8c4d';
const afternoonColor = '#f87c6b';
const nightColor = '#8567AD';
const shifts = {
morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: morningColor },
afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: afternoonColor },
night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: nightColor },
};
const getShiftByHour = (hour) => {
if (hour >= 6 && hour < 14) {
return shifts.morning;
}
if (hour >= 14 && hour < 22) {
return shifts.afternoon;
}
if (hour >= 22 || hour < 6) {
return shifts.night;
}
return shifts.afternoon;
};
const calculateStart = (start) => {
const d = new Date(start);
const originalHour = d.getHours();
const shift = getShiftByHour(originalHour);
d.setHours(shift.startHour, 0, 0, 0);
if (shift.startHour === 22 && originalHour < 6) {
d.setDate(d.getDate() - 1);
}
return d;
};
const calculateEnd = (start) => {
const d = new Date(start);
const startHour = d.getHours();
const shift = getShiftByHour(d.getHours());
d.setHours(shift.endHour, 0, 0, 0);
if (shift.nextDay && startHour === 22) {
d.setDate(d.getDate() + 1);
}
return d;
};
const getTitle = (startHours) => getShiftByHour(startHours).title;
const getColor = (startHours) => getShiftByHour(startHours).color;
const initialEvents = [
{ id: 1, resource: 'A', title: 'Morning Shift', start: '2026-10-23T06:00', end: '2026-10-23T14:00', color: morningColor },
{
id: 2,
resource: 'B',
title: 'Afternoon Shift',
start: '2026-10-23T14:00',
end: '2026-10-23T22:00',
color: afternoonColor,
},
{ id: 3, resource: 'C', title: 'Night Shift', start: '2026-10-23T22:00', end: '2026-10-24T06:00', color: nightColor },
{ id: 4, resource: 'A', title: 'Morning Shift', start: '2026-10-24T06:00', end: '2026-10-24T14:00', color: morningColor },
{
id: 5,
resource: 'B',
title: 'Afternoon Shift',
start: '2026-10-24T14:00',
end: '2026-10-24T22:00',
color: afternoonColor,
},
{ id: 6, resource: 'C', title: 'Night Shift', start: '2026-10-24T22:00', end: '2026-10-25T06:00', color: nightColor },
// Showing partial data. Download full source.
];
const resources = [
{ id: 'A', name: 'Crew A' },
{ id: 'B', name: 'Crew B' },
{ id: 'C', name: 'Crew C' },
];
const view = {
scheduler: {
type: 'week',
allDay: false,
startTime: '06:00',
endTime: '06:00+1',
timeCellStep: 120,
timeLabelStep: 120,
},
};
function App() {
const [myEvents, setMyEvents] = useState(initialEvents);
const [colors, setColors] = useState([]);
const [redResources, setRedResources] = useState({});
const [draggedEventStart, setDraggedEventStart] = useState(null);
const [draggedEventEnd, setDraggedEventEnd] = useState(null);
const [draggedEventResource, setDraggedEventResource] = useState(null);
const [availableSlotOnHover, setAvailableSlotOnHover] = useState(null);
const [isToastOpen, setToastOpen] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const getAvailableSlots = useCallback(
(resourceId, dayStart) => {
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
dayEnd.setHours(6, 0, 0, 0);
const dayEvents = myEvents.filter((e) => {
const eStart = new Date(e.start);
return eStart >= dayStart && eStart < dayEnd;
});
const slots = { morning: true, afternoon: true, night: true };
dayEvents.forEach((e) => {
if (e.resource === resourceId) {
slots.morning = slots.afternoon = slots.night = false;
} else {
const startHour = new Date(e.start).getHours();
if (startHour === 6) {
slots.morning = false;
} else if (startHour === 14) {
slots.afternoon = false;
} else if (startHour === 22) {
slots.night = false;
}
}
});
return slots;
},
[myEvents],
);
const clearColorsForResource = useCallback((currentColors, resourceId, date) => {
const base = new Date(date);
base.setHours(0, 0, 0, 0);
const baseTime = +base;
return currentColors.filter((c) => {
if (c.resource === resourceId) {
const cDate = new Date(c.start);
cDate.setHours(0, 0, 0, 0);
if (+cDate === baseTime) {
const hour = new Date(c.start).getHours();
if (hour === 6 || hour === 14 || hour === 22) {
return false;
}
}
}
return true;
});
}, []);
const handleCellHoverIn = useCallback(
(args) => {
const hoveredDate = new Date(args.date);
const shift = getShiftByHour(hoveredDate.getHours());
if (!shift) return;
const dayStart = new Date(args.date);
dayStart.setHours(6, 0, 0, 0);
const availableSlots = getAvailableSlots(args.resource.id, dayStart);
const slotKey = Object.keys(availableSlots).find((key) => availableSlots[key] && shifts[key].startHour === shift.startHour);
if (slotKey) {
const startTime = new Date(dayStart);
startTime.setHours(shifts[slotKey].startHour, 0, 0, 0);
const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1);
const slot = {
background: '#c1ffe180',
cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
start: +startTime + 1,
end: endTime,
resource: args.resource.id,
};
setAvailableSlotOnHover(slot);
setColors((prev) => [...prev, slot]);
}
},
[getAvailableSlots],
);
const handleCellHoverOut = useCallback(() => {
if (availableSlotOnHover) {
setColors((prev) => prev.filter((c) => c !== availableSlotOnHover));
setAvailableSlotOnHover(null);
}
}, [availableSlotOnHover]);
const handleEventCreate = useCallback(
(args) => {
const event = { ...args.event, id: Date.now() };
const dayStart = new Date(event.start);
dayStart.setHours(6, 0, 0, 0);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
dayEnd.setHours(6, 0, 0, 0);
const dayEvents = myEvents.filter((e) => {
const eStart = new Date(e.start);
return eStart >= dayStart && eStart < dayEnd;
});
const conflict = dayEvents.find((ev) => ev.resource === event.resource || +new Date(ev.start) === +new Date(event.start));
if (conflict) {
setToastMessage('Already assigned');
setToastOpen(true);
return false;
}
setColors((prev) => clearColorsForResource(prev, event.resource, event.start));
const day = new Date(event.start);
day.setHours(0, 0, 0, 0);
setRedResources((prev) => ({ ...prev, [event.resource + day.toISOString()]: false }));
setAvailableSlotOnHover(null);
setMyEvents((prev) => [...prev, event]);
},
[myEvents, clearColorsForResource],
);
const handleEventClick = useCallback(() => {
setToastMessage('Already assigned');
setToastOpen(true);
}, []);
const handleEventDelete = useCallback((args) => {
setMyEvents((prev) => prev.filter((e) => e.id !== args.event.id));
const colorStart = new Date(args.event.start);
colorStart.setHours(6, 0, 0, 0);
const colorEnd = new Date(args.event.start);
colorEnd.setDate(colorEnd.getDate() + 1);
colorEnd.setHours(6, 0, 0, 0);
setColors((prev) => [...prev, { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: args.event.resource }]);
const resource = args.event.resource;
const day = new Date(args.event.start);
day.setHours(0, 0, 0, 0);
setRedResources((prev) => ({ ...prev, [resource + day.toISOString()]: true }));
setToastMessage(args.event.title + ' deleted');
setToastOpen(true);
}, []);
const handleEventDragStart = useCallback((args) => {
setDraggedEventStart(args.event.start);
setDraggedEventEnd(args.event.end);
setDraggedEventResource(args.event.resource);
}, []);
const handleEventUpdateFailed = useCallback(() => {
setToastMessage('Already assigned');
setToastOpen(true);
}, []);
const handleEventUpdate = useCallback(
(args) => {
const event = args.event;
const dayStart = new Date(event.start);
dayStart.setHours(6, 0, 0, 0);
const dragStartDay = new Date(draggedEventStart);
dragStartDay.setHours(6, 0, 0, 0);
if (+dayStart !== +dragStartDay) {
return false;
}
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
dayEnd.setHours(6, 0, 0, 0);
const dayEvents = myEvents.filter((e) => {
const eStart = new Date(e.start);
return eStart >= dayStart && eStart < dayEnd;
});
const conflicts = dayEvents.filter(
(ev) => ev.id !== event.id && (ev.resource === event.resource || +new Date(ev.start) === +new Date(event.start)),
);
const evStart = new Date(event.start);
event.title = getTitle(evStart.getHours());
event.color = getColor(evStart.getHours());
if (conflicts.length) {
let shouldReturn = false;
conflicts.forEach((sh, i) => {
if (sh.resource === event.resource) {
if (+event.start === +new Date(sh.start)) {
shouldReturn = true;
}
sh.resource = draggedEventResource;
conflicts[i] = sh;
} else {
sh.start = draggedEventStart;
sh.end = draggedEventEnd;
const start = new Date(draggedEventStart);
sh.title = getTitle(start.getHours());
sh.color = getColor(start.getHours());
conflicts[i] = sh;
}
});
if (shouldReturn) {
return false;
}
setMyEvents((prev) =>
prev.map((e) => {
const updated = [...conflicts, event].find((u) => u.id === e.id);
return updated || e;
}),
);
} else {
if (dayEvents.some((ev) => ev.id === event.id)) {
setMyEvents((prev) => prev.map((e) => (e.id === event.id ? event : e)));
}
}
setColors((prev) => clearColorsForResource(prev, event.resource, event.start));
const day = new Date(event.start);
day.setHours(0, 0, 0, 0);
setRedResources((prev) => ({ ...prev, [event.resource + day.toISOString()]: false }));
if (!conflicts.length && event.resource !== draggedEventResource) {
const colorEnd = new Date(dayStart);
colorEnd.setDate(colorEnd.getDate() + 1);
colorEnd.setHours(6, 0, 0, 0);
setColors((prev) => [...prev, { start: dayStart, resource: args.oldEvent.resource, background: '#ffdbd280', end: colorEnd }]);
setRedResources((prev) => ({ ...prev, [args.oldEvent.resource + day.toISOString()]: true }));
}
setDraggedEventStart(null);
setDraggedEventEnd(null);
setDraggedEventResource(null);
},
[myEvents, draggedEventStart, draggedEventEnd, draggedEventResource, clearColorsForResource],
);
const extendDefaultEvent = useCallback((args) => {
const start = new Date(args.start);
const newStart = calculateStart(start);
const newEnd = calculateEnd(start);
const title = getTitle(newStart.getHours());
const color = getColor(newStart.getHours());
return {
title: title,
start: newStart,
end: newEnd,
color: color,
};
}, []);
const customResource = useCallback(
(res, day) => {
const style = redResources[res.id + day.toISOString()]
? { color: '#53000080', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
: {};
return <div style={style}>{res.name}</div>;
},
[redResources],
);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
return (
<>
<Eventcalendar
cssClass="mds-24-hour-manufacturing-calendar"
data={myEvents}
colors={colors}
dragToMove={true}
dragToCreate={false}
clickToCreate="single"
dragToResize={false}
dragTimeStep={480}
dragTimeStepBase="viewStart"
eventDelete={true}
eventOverlap={false}
extendDefaultEvent={extendDefaultEvent}
onCellHoverIn={handleCellHoverIn}
onCellHoverOut={handleCellHoverOut}
onEventCreate={handleEventCreate}
onEventClick={handleEventClick}
onEventDelete={handleEventDelete}
onEventDragStart={handleEventDragStart}
onEventUpdateFailed={handleEventUpdateFailed}
onEventUpdate={handleEventUpdate}
groupBy="date"
resources={resources}
renderResource={customResource}
view={view}
/>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</>
);
}
export default App;
import {
Eventcalendar,
MbscCalendarColor,
MbscCalendarEvent,
MbscCellHoverEvent,
MbscDateType,
MbscEventcalendarView,
MbscEventCreateEvent,
MbscEventDeleteEvent,
MbscEventDragEvent,
MbscEventUpdateEvent,
MbscResource,
setOptions,
Toast ,
} from '@mobiscroll/react';
import { FC, useCallback, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
interface Shift {
startHour: number;
endHour: number;
title: string;
color: string;
nextDay?: boolean;
}
const morningColor = '#4a8c4d';
const afternoonColor = '#f87c6b';
const nightColor = '#8567AD';
const shifts: Record<string, Shift> = {
morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: morningColor },
afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: afternoonColor },
night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: nightColor },
};
const getShiftByHour = (hour: number): Shift => {
if (hour >= 6 && hour < 14) {
return shifts.morning;
}
if (hour >= 14 && hour < 22) {
return shifts.afternoon;
}
if (hour >= 22 || hour < 6) {
return shifts.night;
}
return shifts.afternoon;
};
const calculateStart = (start: Date): Date => {
const d = new Date(start);
const originalHour = d.getHours();
const shift = getShiftByHour(originalHour);
d.setHours(shift.startHour, 0, 0, 0);
if (shift.startHour === 22 && originalHour < 6) {
d.setDate(d.getDate() - 1);
}
return d;
};
const calculateEnd = (start: Date): Date => {
const d = new Date(start);
const startHour = d.getHours();
const shift = getShiftByHour(d.getHours());
d.setHours(shift.endHour, 0, 0, 0);
if (shift.nextDay && startHour === 22) {
d.setDate(d.getDate() + 1);
}
return d;
};
const getTitle = (startHours: number): string => getShiftByHour(startHours).title;
const getColor = (startHours: number): string => getShiftByHour(startHours).color;
const initialEvents: MbscCalendarEvent[] = [
{ id: 1, resource: 'A', title: 'Morning Shift', start: '2026-10-23T06:00', end: '2026-10-23T14:00', color: morningColor },
{
id: 2,
resource: 'B',
title: 'Afternoon Shift',
start: '2026-10-23T14:00',
end: '2026-10-23T22:00',
color: afternoonColor,
},
{ id: 3, resource: 'C', title: 'Night Shift', start: '2026-10-23T22:00', end: '2026-10-24T06:00', color: nightColor },
{ id: 4, resource: 'A', title: 'Morning Shift', start: '2026-10-24T06:00', end: '2026-10-24T14:00', color: morningColor },
{
id: 5,
resource: 'B',
title: 'Afternoon Shift',
start: '2026-10-24T14:00',
end: '2026-10-24T22:00',
color: afternoonColor,
},
{ id: 6, resource: 'C', title: 'Night Shift', start: '2026-10-24T22:00', end: '2026-10-25T06:00', color: nightColor },
// Showing partial data. Download full source.
];
const resources: MbscResource[] = [
{ id: 'A', name: 'Crew A' },
{ id: 'B', name: 'Crew B' },
{ id: 'C', name: 'Crew C' },
];
const view: MbscEventcalendarView = {
scheduler: {
type: 'week',
allDay: false,
startTime: '06:00',
endTime: '06:00+1',
timeCellStep: 120,
timeLabelStep: 120,
},
};
const App: FC = () => {
const [myEvents, setMyEvents] = useState<MbscCalendarEvent[]>(initialEvents);
const [colors, setColors] = useState<MbscCalendarColor[]>([]);
const [redResources, setRedResources] = useState<Record<string, boolean>>({});
const [draggedEventStart, setDraggedEventStart] = useState<Date | null>(null);
const [draggedEventEnd, setDraggedEventEnd] = useState<Date | null>(null);
const [draggedEventResource, setDraggedEventResource] = useState<string | null>(null);
const [availableSlotOnHover, setAvailableSlotOnHover] = useState<MbscCalendarColor | null>(null);
const [isToastOpen, setToastOpen] = useState<boolean>(false);
const [toastMessage, setToastMessage] = useState<string>('');
const getAvailableSlots = useCallback(
(resourceId: string, dayStart: Date): Record<string, boolean> => {
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
dayEnd.setHours(6, 0, 0, 0);
const dayEvents = myEvents.filter((e) => {
const eStart = new Date(e.start as Date);
return eStart >= dayStart && eStart < dayEnd;
});
const slots: Record<string, boolean> = { morning: true, afternoon: true, night: true };
dayEvents.forEach((e) => {
if (e.resource === resourceId) {
slots.morning = slots.afternoon = slots.night = false;
} else {
const startHour = new Date(e.start as Date).getHours();
if (startHour === 6) {
slots.morning = false;
} else if (startHour === 14) {
slots.afternoon = false;
} else if (startHour === 22) {
slots.night = false;
}
}
});
return slots;
},
[myEvents],
);
const clearColorsForResource = useCallback((currentColors: MbscCalendarColor[], resourceId: string, date: Date): MbscCalendarColor[] => {
const base = new Date(date);
base.setHours(0, 0, 0, 0);
const baseTime = +base;
return currentColors.filter((c) => {
if (c.resource === resourceId) {
const cDate = new Date(c.start as Date);
cDate.setHours(0, 0, 0, 0);
if (+cDate === baseTime) {
const hour = new Date(c.start as Date).getHours();
if (hour === 6 || hour === 14 || hour === 22) {
return false;
}
}
}
return true;
});
}, []);
const handleCellHoverIn = useCallback(
(args: MbscCellHoverEvent) => {
const hoveredDate = new Date(args.date);
const shift = getShiftByHour(hoveredDate.getHours());
if (!shift) return;
const dayStart = new Date(args.date);
dayStart.setHours(6, 0, 0, 0);
const availableSlots = getAvailableSlots(String(args.resource.id), dayStart);
const slotKey = Object.keys(availableSlots).find((key) => availableSlots[key] && shifts[key].startHour === shift.startHour);
if (slotKey) {
const startTime = new Date(dayStart);
startTime.setHours(shifts[slotKey].startHour, 0, 0, 0);
const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1);
const slot: MbscCalendarColor = {
background: '#c1ffe180',
cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
start: startTime as MbscDateType,
end: endTime,
resource: String(args.resource.id),
};
setAvailableSlotOnHover(slot);
setColors((prev) => [...prev, slot]);
}
},
[getAvailableSlots],
);
const handleCellHoverOut = useCallback(() => {
if (availableSlotOnHover) {
setColors((prev) => prev.filter((c) => c !== availableSlotOnHover));
setAvailableSlotOnHover(null);
}
}, [availableSlotOnHover]);
const handleEventCreate = useCallback(
(args: MbscEventCreateEvent) => {
const event = { ...args.event, id: Date.now() };
const dayStart = new Date(event.start as Date);
dayStart.setHours(6, 0, 0, 0);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
dayEnd.setHours(6, 0, 0, 0);
const dayEvents = myEvents.filter((e) => {
const eStart = new Date(e.start as Date);
return eStart >= dayStart && eStart < dayEnd;
});
const conflict = dayEvents.find(
(ev) => ev.resource === event.resource || +new Date(ev.start as Date) === +new Date(event.start as Date),
);
if (conflict) {
setToastMessage('Already assigned');
setToastOpen(true);
return false;
}
setColors((prev) => clearColorsForResource(prev, String(event.resource), event.start as Date));
const day = new Date(event.start as Date);
day.setHours(0, 0, 0, 0);
setRedResources((prev) => ({ ...prev, [String(event.resource) + day.toISOString()]: false }));
setAvailableSlotOnHover(null);
setMyEvents((prev) => [...prev, event]);
},
[myEvents, clearColorsForResource],
);
const handleEventClick = useCallback(() => {
setToastMessage('Already assigned');
setToastOpen(true);
}, []);
const handleEventDelete = useCallback((args: MbscEventDeleteEvent) => {
setMyEvents((prev) => prev.filter((e) => e.id !== args.event.id));
const colorStart = new Date(args.event.start as Date);
colorStart.setHours(6, 0, 0, 0);
const colorEnd = new Date(args.event.start as Date);
colorEnd.setDate(colorEnd.getDate() + 1);
colorEnd.setHours(6, 0, 0, 0);
setColors((prev) => [...prev, { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: String(args.event.resource) }]);
const resource = String(args.event.resource);
const day = new Date(args.event.start as Date);
day.setHours(0, 0, 0, 0);
setRedResources((prev) => ({ ...prev, [resource + day.toISOString()]: true }));
setToastMessage(args.event.title + ' deleted');
setToastOpen(true);
}, []);
const handleEventDragStart = useCallback((args: MbscEventDragEvent) => {
setDraggedEventStart(args.event.start as Date);
setDraggedEventEnd(args.event.end as Date);
setDraggedEventResource(String(args.event.resource));
}, []);
const handleEventUpdateFailed = useCallback(() => {
setToastMessage('Already assigned');
setToastOpen(true);
}, []);
const handleEventUpdate = useCallback(
(args: MbscEventUpdateEvent) => {
const event = args.event;
const dayStart = new Date(event.start as Date);
dayStart.setHours(6, 0, 0, 0);
const dragStartDay = new Date(draggedEventStart!);
dragStartDay.setHours(6, 0, 0, 0);
if (+dayStart !== +dragStartDay) {
return false;
}
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
dayEnd.setHours(6, 0, 0, 0);
const dayEvents = myEvents.filter((e) => {
const eStart = new Date(e.start as Date);
return eStart >= dayStart && eStart < dayEnd;
});
const conflicts = dayEvents.filter(
(ev) => ev.id !== event.id && (ev.resource === event.resource || +new Date(ev.start as Date) === +new Date(event.start as Date)),
);
const evStart = new Date(event.start as Date);
event.title = getTitle(evStart.getHours());
event.color = getColor(evStart.getHours());
if (conflicts.length) {
let shouldReturn = false;
conflicts.forEach((sh, i) => {
if (sh.resource === event.resource) {
if (+(event.start as Date) === +new Date(sh.start as Date)) {
shouldReturn = true;
}
sh.resource = draggedEventResource!;
conflicts[i] = sh;
} else {
sh.start = draggedEventStart!;
sh.end = draggedEventEnd!;
const start = new Date(draggedEventStart!);
sh.title = getTitle(start.getHours());
sh.color = getColor(start.getHours());
conflicts[i] = sh;
}
});
if (shouldReturn) {
return false;
}
setMyEvents((prev) =>
prev.map((e) => {
const updated = [...conflicts, event].find((u) => u.id === e.id);
return updated || e;
}),
);
} else {
if (dayEvents.some((ev) => ev.id === event.id)) {
setMyEvents((prev) => prev.map((e) => (e.id === event.id ? event : e)));
}
}
setColors((prev) => clearColorsForResource(prev, String(event.resource), event.start as Date));
const day = new Date(event.start as Date);
day.setHours(0, 0, 0, 0);
setRedResources((prev) => ({ ...prev, [String(event.resource) + day.toISOString()]: false }));
if (!conflicts.length && event.resource !== draggedEventResource) {
const colorEnd = new Date(dayStart);
colorEnd.setDate(colorEnd.getDate() + 1);
colorEnd.setHours(6, 0, 0, 0);
setColors((prev) => [
...prev,
{ start: dayStart, resource: String(args.oldEvent!.resource), background: '#ffdbd280', end: colorEnd },
]);
setRedResources((prev) => ({ ...prev, [String(args.oldEvent!.resource) + day.toISOString()]: true }));
}
setDraggedEventStart(null);
setDraggedEventEnd(null);
setDraggedEventResource(null);
},
[myEvents, draggedEventStart, draggedEventEnd, draggedEventResource, clearColorsForResource],
);
const extendDefaultEvent = useCallback((args: { start: Date }): MbscCalendarEvent => {
const start = new Date(args.start);
const newStart = calculateStart(start);
const newEnd = calculateEnd(start);
const title = getTitle(newStart.getHours());
const color = getColor(newStart.getHours());
return {
title: title,
start: newStart,
end: newEnd,
color: color,
};
}, []);
const customResource = useCallback(
(res: MbscResource, day: Date) => {
const style = redResources[res.id + day.toISOString()]
? { color: '#53000080', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
: {};
return <div style={style}>{res.name}</div>;
},
[redResources],
);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
return (
<>
<Eventcalendar
cssClass="mds-24-hour-manufacturing-calendar"
data={myEvents}
colors={colors}
dragToMove={true}
dragToCreate={false}
clickToCreate="single"
dragToResize={false}
dragTimeStep={480}
dragTimeStepBase="viewStart"
eventDelete={true}
eventOverlap={false}
extendDefaultEvent={extendDefaultEvent}
onCellHoverIn={handleCellHoverIn}
onCellHoverOut={handleCellHoverOut}
onEventCreate={handleEventCreate}
onEventClick={handleEventClick}
onEventDelete={handleEventDelete}
onEventDragStart={handleEventDragStart}
onEventUpdateFailed={handleEventUpdateFailed}
onEventUpdate={handleEventUpdate}
groupBy="date"
resources={resources}
renderResource={customResource}
view={view}
/>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</>
);
};
export default App;
.mds-24-hour-manufacturing-calendar .mbsc-schedule-events {
left: 0;
right: 0;
}
.mds-24-hour-manufacturing-calendar .mbsc-schedule-time-wrapper {
height: 2.5em;
}
.mds-24-hour-manufacturing-calendar .mbsc-schedule-item {
height: 2.5em;
}
.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot {
z-index: auto;
}
.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot .mbsc-schedule-color-text {
position: absolute;
z-index: 1;
pointer-events: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 35px;
height: 35px;
border: none;
border-radius: 50%;
background: linear-gradient(135deg, #33333380, #00000080);
box-sizing: border-box;
}
.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot.mbsc-font-icon:before {
position: absolute;
z-index: 2;
pointer-events: none;
top: 50%;
left: 50%;
color: #e0fff0;
font-size: 12px;
opacity: 0.8;
transform: translate(-50%, -50%);
}