Unfortunately, the code you suggested is what I already have.
I have attached a video of the issue here and a dump of my code.
My code is a Frankenstein as I have used bits and pieces from multiple examples learning how to interact with schedulePro:
import * as Module from '../../build/schedulerpro.module.js';
Object.assign(window, Module);
CSSHelper.insertRule('.weekend { background: transparent repeating-linear-gradient(-55deg, #dddddd99, #dddddd99 10px, #eeeeee99 5px, #eeeeee99 20px); }');
const Now = DateHelper.format(new Date(), 'MM/DD/YY HH:mm');
const Numberformatter = new NumberFormat('9,999.##');
const zoomLevels = {
minuteAndHour : 1,
hourAndDay : 1
};
// Handles dragging unscheduled appointment from the grid onto the schedule
class Drag extends DragHelper {
static get defaultConfig() {
return {
callOnFunctions : true,
autoSizeClonedTarget : false,
unifiedProxy : true,
// Prevent removing proxy on drop, we adopt it for usage in the Schedule
removeProxyAfterDrop : false,
// Don't drag the actual row element, clone it
cloneTarget : true,
// Only allow drops on the schedule area
dropTargetSelector : '.b-timeline-subgrid',
// Only allow drag of row elements inside on the unplanned grid
targetSelector : '.b-grid-row:not(.b-group-row)'
};
}
afterConstruct(config) {
// Configure DragHelper with schedule's scrollManager to allow scrolling while dragging
this.scrollManager = this.schedule.scrollManager;
}
createProxy(grabbedElement, initialXY) {
const
{ context, schedule, grid } = this,
draggedAppointment = grid.getRecordFromElement(grabbedElement),
durationInPixels = schedule.timeAxisViewModel.getDistanceForDuration(draggedAppointment.durationMS),
proxy = document.createElement('div');
proxy.style.cssText = '';
proxy.style.width = `${durationInPixels}px`;
proxy.style.height = schedule.rowHeight - (2 * schedule.resourceMargin) + 'px';
// Fake an event bar
proxy.classList.add('b-sch-event-wrap', 'b-sch-style-border', 'b-unassigned-class', 'b-sch-horizontal');
proxy.innerHTML = StringHelper.xss`
<div class="b-sch-event b-has-content b-sch-event-withicon">
<div class="b-sch-event-content">
<div>
<div>${draggedAppointment.name}</div>
</div>
</div>
</div>
`;
let totalDuration = 0;
grid.selectedRecords.forEach(appointment => totalDuration += appointment.duration);
context.totalDuration = totalDuration;
return proxy;
}
onDragStart({ context }) {
const
me = this,
{ schedule, grid } = me,
{ selectedRecords, store } = grid,
appointment = grid.getRecordFromElement(context.grabbed);
// save a reference to the appointments being dragged so we can access them later
context.appointments = selectedRecords.slice();
context.relatedElements = selectedRecords.sort((r1, r2) => store.indexOf(r1) - store.indexOf(r2)).map(rec => rec !== appointment && grid.rowManager.getRowFor(rec).element).filter(el => el);
schedule.enableScrollingCloseToEdges(schedule.timeAxisSubGrid);
// Prevent tooltips from showing while dragging
schedule.features.eventTooltip.disabled = true;
}
onDrag({ context }) {
const
{ schedule } = this,
{ appointments, totalDuration } = context,
newStartDate = schedule.getDateFromCoordinate(context.newX, 'round', false),
lastAppointmentEndDate = newStartDate && DateHelper.add(newStartDate, totalDuration, appointments[0].durationUnit),
doctor = context.target && schedule.resolveResourceRecord(context.target),
calendar = doctor?.calendar;
// Save reference to the doctor so we can use it in onAppointmentDrop
context.doctor = doctor;
}
// Drop callback after a mouse up, take action and transfer the unplanned appointment to the real EventStore (if it's valid)
async onDrop({ context }) {
const
me = this,
{ schedule } = me;
// If drop was done in a valid location, set the startDate and transfer the task to the Scheduler event store
if (context.valid) {
const
{ appointments, element, relatedElements = [], doctor } = context,
coordinate = DomHelper.getTranslateX(element),
dropDate = schedule.getDateFromCoordinate(coordinate, 'round', false),
eventBarEls = [element, ...relatedElements];
for (let i = 0; i < appointments.length; i++) {
// We hand over the data + existing element to the Scheduler so it do the scheduling
// await is used so that we have a reliable end date in the case of multiple event drag
await schedule.scheduleEvent({
eventRecord : appointments[i],
// When dragging multiple appointments, add them back to back + assign to resource
startDate : i === 0 ? dropDate : appointments[i - 1].endDate,
// Assign to the doctor (resource) it was dropped on
resourceRecord : doctor,
element : eventBarEls[i]
});
}
}
schedule.disableScrollingCloseToEdges(schedule.timeAxisSubGrid);
schedule.features.eventTooltip.disabled = false;
}
};
// Customized scheduler displaying hospital appointments
class Schedule extends SchedulerPro {
static get $name() {
return 'Schedule';
}
static get configurable() {
return {
features : {
columnLines : true,
},
//rowHeight : 80,
barMargin : 10,
eventStyle : 'border',
eventColor : 'indigo',
//allowOverlap : false,
//useInitialAnimation : false,
columns : [
{
type : 'resourceInfo',
text : 'Cell',
field : 'name',
showImage : false,
filterable : {
filterField : {
triggers : {
search : {
cls : 'b-icon b-fa-filter'
}
},
placeholder : 'Filter Cells'
}
}
},
]
}
};
// Return a DOM config object for what to show inside the event bar (you can return an HTML string too)
eventRenderer({ eventRecord }) {
return [
{
children : [
{
class : 'b-event-name',
text : 'Job:' + eventRecord.name + ' Qty:' + Numberformatter.format(eventRecord.qty)
},
{
class : 'b-event-name',
text : eventRecord.partno
}
]
}
];
}
};
// Custom grid that holds unassigned appointments
class UnplannedGrid extends Grid {
static get configurable() {
return {
selectionMode : {
cell : false
},
features : {
filterBar : {
compactMode : true
},
/*
group : {
field : 'dept',
renderer({ groupRowFor, column }) {
if (column.parentIndex === 0) {
return `Dept:${groupRowFor} Jobs`;
}
}
}
*/
},
columns : [
{
type : 'template',
text : 'Jobs',
flex : 2,
cellCls : 'unscheduledNameCell',
template : ({ record: appointment }) => `
<div class="name-container">
<span>${StringHelper.encodeHtml(appointment.name)}</span>
<span class="patient-name">${appointment.partno}</span>
</div>
`
},
{
type : 'template',
text : 'Hours',
flex : 1,
cellCls : 'unscheduledNameCell',
template : ({ record: appointment }) => `
<div class="name-container">
<span>${StringHelper.encodeHtml(appointment.duration)}</span>
</div>
`
},
{
type : 'template',
text : 'Quantity',
flex : 1,
cellCls : 'unscheduledNameCell',
template : ({ record: appointment }) => `
<div class="name-container">
<span>${StringHelper.encodeHtml(Numberformatter.format(appointment.qty))}</span>
</div>
`
}
],
tbar : [
{
type : 'widget',
tag : 'strong',
html : 'Unplanned Jobs',
flex : 1
},
{
icon : 'b-fa b-fa-angle-double-down',
cls : 'b-transparent',
tooltip : 'Expand all groups',
onAction : 'up.expandAll'
},
{
icon : 'b-fa b-fa-angle-double-up',
cls : 'b-transparent',
tooltip : 'Collapse all groups',
onAction : 'up.collapseAll'
},
],
rowHeight : 65,
disableGridRowModelWarning : true
};
}
static get $name() {
return 'UnplannedGrid';
}
set project(project) {
// Create a chained version of the event store as our store. It will be filtered to only display events that
// lack assignments
this.store = project.eventStore.chain(eventRecord => !eventRecord.assignments.length);
// When assignments change, update our chained store to reflect the changes
project.assignmentStore.on({
change() {
this.store.fillFromMaster();
},
thisObj : this
});
}
};
class Task extends EventModel {
static $name = 'Task';
static get defaults() {
return {
// In this demo, default duration for tasks will be hours (instead of days)
durationUnit : 'h',
// Use a default name, for better look in the grid if unassigning a new event
name : 'New event',
// Use a default icon also
iconCls : 'b-fa b-fa-asterisk'
};
}
}
class TaskStore extends EventStore {
static $name = 'TaskStore';
static get defaultConfig() {
return {
modelClass : Task
};
}
// Override add to reschedule any overlapping events caused by the add
add(records, silent = false) {
const me = this;
if (me.autoRescheduleTasks) {
// Flag to avoid rescheduling during rescheduling
me.isRescheduling = true;
me.beginBatch();
}
if (!Array.isArray(records)) {
records = [records];
}
super.add(records, silent);
if (me.autoRescheduleTasks) {
me.endBatch();
me.isRescheduling = false;
}
}
// Auto called when triggering the update event.
// Reschedule if the update caused the event to overlap any others.
onUpdate({ record }) {
this.rescheduleOverlappingTasks(record);
/*
if (this.autoRescheduleTasks && !this.isRescheduling) {
alert('123');
this.rescheduleOverlappingTasks(record);
}
*/
}
rescheduleOverlappingTasks(eventRecord) {
if (eventRecord.resource) {
const
futureEvents = [],
earlierEvents = [];
// Split tasks into future and earlier tasks
eventRecord.resource.events.forEach(event => {
if (event !== eventRecord) {
if (event.startDate >= eventRecord.startDate) {
futureEvents.push(event);
}
else {
earlierEvents.push(event);
}
}
});
if (futureEvents.length || earlierEvents.length) {
futureEvents.sort((a, b) => a.startDate > b.startDate ? 1 : -1);
earlierEvents.sort((a, b) => a.startDate > b.startDate ? -1 : 1);
futureEvents.forEach((ev, i) => {
const prev = futureEvents[i - 1] || eventRecord;
ev.startDate = DateHelper.max(prev.endDate, ev.startDate);
});
// mjm 3/29/23 Removed this code as I dont want previous events rescheduling
// Walk backwards and remove any overlap
/*
[eventRecord, ...earlierEvents].forEach((ev, i, all) => {
const prev = all[i - 1];
if (ev.endDate > Date.now() && ev !== eventRecord && prev) {
ev.setEndDate(DateHelper.min(prev.startDate, ev.endDate), true);
}
});
*/
this.isRescheduling = false;
}
}
}
};
// Displays planned sessions
const schedule = new Schedule({
ref : 'schedule',
appendTo : 'main',
durationUnit : 'h',
startDate : DateHelper.add(Now, -6, 'hours'),
endDate : DateHelper.add(Now, 7, 'days'),
//allowOverlap : true,
autoRescheduleTasks : true,
flex : 1,
autoHeight:true,
eventStyle : 'border',
presets : PresetManager.records.filter(preset => zoomLevels[preset.id]),
//useInitialAnimation : 'slide-from-left',
features : {
filterBar : {
compactMode : true
},
timeRanges : {
showCurrentTimeLine :true,
showHeaderElements : true,
//enableResizing : true
},
pan : true,
eventDragCreate : false,
dependencies : false,
// headerZoom : true,
percentBar : true,
resourceNonWorkingTime : true,
resourceNonWorkingTime : {
maxTimeAxisUnit : 'week'
},
},
viewPreset : {
base : 'hourAndDay',
columnLinesFor : 1,
timeResolution : {
unit : 'hour',
increment : 1
},
headers : [
{
unit : 'day',
increment : 1,
dateFormat : 'ddd DD MMM YY'
},
{
unit : 'hour',
increment : 2,
dateFormat : 'hA'
}
]
},
/*
columns : [
{ text : 'Cell', field : 'name', width : 160 },
],
*/
project : {
resourceStore : {
createUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=RESOURCE_CREATEDATA',
readUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=RESOURCE_READDATA',
updateUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=RESOURCE_UPDATEDATA',
autoLoad : true, // Load upon creation
autoCommit : true,
sendAsFormData : true
},
eventStore : {
// Unassigned events should remain in store
//removeUnassignedEvent : false,
storeClass : TaskStore,
createUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=EVENT_CREATEDATA',
readUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=EVENT_READDATA',
updateUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=EVENT_UPDATEDATA',
autoLoad : true, // Load upon creation
autoCommit : true,
sendAsFormData : true
},
assignmentStore : {
createUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=ASSIGNMENT_CREATEDATA',
readUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=ASSIGNMENT_READDATA',
updateUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=ASSIGNMENT_UPDATEDATA',
deleteUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=ASSIGNMENT_DELETEDATA',
autoLoad : true, // Load upon creation
autoCommit : true,
sendAsFormData : true
}
,
calendarManagerStore : {
readUrl : 'http://mmcsherry/poem/Prod/Schedule/Schedule.php?LOC=CALANDAR_READDATA',
autoLoad : true, // Load upon creation
sendAsFormData : true
}
,
/*
calendarsData : [
{
id : 'X',
name : 'X',
unspecifiedTimeIsWorking : true,
intervals : [
{
recurrentStartDate : 'on Sat at 0:00',
recurrentEndDate : 'on Sun at 22:45',
isWorking : false,
cls : 'weekend'
},
]
},
{
id : 'weekends',
name : 'Weekends',
unspecifiedTimeIsWorking : true,
intervals : [
{
recurrentStartDate : 'on Sat at 0:00',
recurrentEndDate : 'on Sun at 22:00',
isWorking : false,
cls : 'weekend'
}
]
},
{
id : 'weekdays',
name : 'Weekdays',
unspecifiedTimeIsWorking : true,
intervals : [
{
recurrentStartDate : 'on mon at 0:00',
recurrentEndDate : 'on sat at 0:00',
isWorking : false,
cls : 'weekend'
}
,
{
startDate : "2023-03-29T02:00",
endDate : "2023-03-29T08:00",
isWorking : true,
}
]
}
],
*/
},
timeRanges : [
{ id : 1, name : '@', startDate : Now, iconCls : "b-fa b-fa-arrows-rotate"}
],
tbar : [
/*
{
type : 'button',
toggleable : true,
icon : 'b-fa-calendar',
pressedIcon : 'b-fa-calendar-check',
text : 'Automatic rescheduling',
tooltip : 'Toggles whether to automatically reschedule overlapping tasks',
cls : 'reschedule-button',
onToggle({ pressed }) {
schedule.autoRescheduleTasks = pressed;
}
},
*/
{
type : 'button',
ref : 'zoomInButton',
icon : 'b-icon-search-plus',
text : 'Zoom in',
onClick() {
schedule.zoomIn();
}
},
{
type : 'button',
ref : 'zoomOutButton',
icon : 'b-icon-search-minus',
text : 'Zoom out',
onClick() {
schedule.zoomOut();
}
},
{
icon : 'b-icon b-fa-caret-left',
onClick() {
schedule.shiftPrevious();
}
},
{
type : 'button',
text : 'Today',
onClick() {
const startDate = DateHelper.clearTime(new Date());
schedule.setTimeSpan(DateHelper.add(startDate, schedule.startDate, 'd'), DateHelper.add(startDate, schedule.endDate, 'd'));
}
},
{
icon : 'b-icon b-fa-caret-right',
onClick() {
schedule.shiftNext();
}
},
{
type : 'textfield',
ref : 'filterByName',
icon : 'b-fa b-fa-filter',
placeholder : 'Find Scheduled Jobs',
clearable : true,
keyStrokeChangeDelay : 100,
width : 200,
triggers : {
filter : {
align : 'start',
cls : 'b-fa b-fa-filter'
}
},
onChange({ value }) {
value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Replace all previous filters and set a new filter
schedule.eventStore.filter({
filters : event => event.name.match(new RegExp(value, 'i')),
replace : true
});
}
},
{
type : 'textfield',
ref : 'filterByPartno',
icon : 'b-fa b-fa-filter',
placeholder : 'Find Scheduled Parts',
clearable : true,
keyStrokeChangeDelay : 100,
width : 200,
triggers : {
filter : {
align : 'start',
cls : 'b-fa b-fa-filter'
}
},
onChange({ value }) {
value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Replace all previous filters and set a new filter
schedule.eventStore.filter({
filters : event => event.partno.match(new RegExp(value, 'i')),
replace : true
});
}
},
],
updateAutoRescheduleTasks(autoRescheduleTasks) {
this.eventStore.autoRescheduleTasks = autoRescheduleTasks;
}
});
const splitter = new Splitter({
appendTo : 'main'
});
// Holds unplanned sessions, that can be dragged to the schedule
const unplannedGrid = new UnplannedGrid({
ref : 'unplanned',
flex : '0 1 300px',
appendTo : 'main',
project : schedule.project,
});
// Handles dragging
const drag = new Drag({
grid : unplannedGrid,
schedule,
constrain : false,
outerElement : unplannedGrid.element
});
//mjm 3/30/23 The way the stores are used currently in this code (grid store is a chained copy of schedule store) this kinda works but it duplicates the record[attachment=0]Demo.mp4[/attachment]
/*
schedule.assignmentStore.on({
// When a task is unassigned move it back to the unplanned tasks grid
remove({ records }) {
records.forEach(({ event }) => {
schedule.eventStore.remove(event);
unplannedGrid.store.add(event);
});
},
thisObj : this
});
*/