I load my stores using PHP without CRUD (not using CRUD I think is my issue). When I Right click an Event and unassign it on the Schedule, how can I get the event to show up in the Unplanned grid without refreshing the page?
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
});
Re: Refresh unplanned Grid on un-Assigning Events
Posted: Thu Mar 30, 2023 4:02 pm
by marcio
That's the one mikemcs, glad that you found and figure it out!
Feel free to reach us when you need.
Re: Refresh unplanned Grid on un-Assigning Events
Posted: Thu Mar 30, 2023 4:14 pm
by mikemcs
This has an adverse effect, it seems to duplicate the record not move it.
// 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;
}
};
In the drag-unplanned-tasks example, the schedule and the grid share the same store but are filtered by if the event is scheduled or not.
// 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,
});
and
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);
I think what I need to do is just refresh the unplanned grid not add a record to it, but I am not sure?
Re: Refresh unplanned Grid on un-Assigning Events
Posted: Thu Mar 30, 2023 10:28 pm
by marcio
Hey mikemcs,
That sounds correct, you could use the following snippet to listen for changes and then update the Grid (retrieved from the same demo)
// When assignments change, update our chained store to reflect the changes
project.assignmentStore.on({
change() {
this.store.fillFromMaster();
},
thisObj : this
});
so the full project configuration would be like this
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
});
}
Re: Refresh unplanned Grid on un-Assigning Events
Posted: Fri Mar 31, 2023 4:09 pm
by mikemcs
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
});
*/
Re: Refresh unplanned Grid on un-Assigning Events
Posted: Fri Mar 31, 2023 9:21 pm
by marcio
Hey mikemcs,
Could you provide some data samples for us to use with your project sample?? We're looking into the code, but we need some samples of data to test it.