Premium support for our pure JavaScript UI components


Post by gorakh.nath »

Hi,
Right now if we try to drop the task in non-working time of resource then it will automatically find the next available time of resource and it will assign the task at that available time.
But our use case is that for some resources we have to assign the task at there non-working time and that will be overtime for them.

How can we allow to drop the task and assign the task at the time where the task was dropped ?

Is there any config available to support this which will over-ride the non-working time of resource?

Is it possible to disable it to automatically assigning the task to working time if we drag to non-working time?

For reference you can check this example just drag the event to non-working time same scenario will be reproducible here : https://bryntum.com/examples/scheduler-pro/non-working-time/

Last edited by gorakh.nath on Tue Aug 31, 2021 1:26 pm, edited 2 times in total.

Post by mats »

Please describe your question thoroughly.


Post by gorakh.nath »

@mats I added the details.


Post by arcady »

There is no such option out of the box I'm afraid. I've made a feature request for this: https://github.com/bryntum/support/issues/3349

So far you can try implementing this yourself by overriding event model skipNonWorking time method. It's overridden on SchedulerProHasAssignmentsMixin class and looks like this:

        * skipNonWorkingTime (date : Date, isForward : boolean = true) : CalculationIterator<Date> {
            if (!date) return null

            const assignmentsByCalendar : this[ 'assignmentsByCalendar' ]   = yield this.$.assignmentsByCalendar

            if (assignmentsByCalendar.size > 0) {
                const options   = Object.assign(
                    yield* this.getBaseOptionsForDurationCalculations(),
                    isForward ? { startDate : date, isForward } : { endDate : date, isForward }
                )

                let workingDate : Date

                const skipRes = yield* this.forEachAvailabilityInterval(
                    options,
                    (startDate : Date, endDate : Date, calendarCacheIntervalMultiple : CalendarCacheIntervalMultiple) => {
                        workingDate         = isForward ? startDate : endDate

                        return false
                    }
                )

                if (skipRes === CalendarIteratorResult.MaxRangeReached || skipRes === CalendarIteratorResult.FullRangeIterated) {
                    yield Reject("Empty calendar")
                }

                return new Date(workingDate)
            } else {
                return yield* superProto.skipNonWorkingTime.call(this, date, isForward)
            }
        }

Post by gorakh.nath »

@arcady Where should I call skipNonWorkingTime method defined above? Do I need to call this on drag and drop event?
My drag and drop file has code:-

import { DragHelper, DomHelper, Rectangle, WidgetHelper, StringHelper, DateHelper } from '@bryntum/schedulerpro/schedulerpro.umd';
import { moment } from '../../../utilities/moment';
import {DRAG_PAST_DATE_ERROR_MSG, WORKFLOW_UPDATE_ERROR, TECHNICIAN_OVERLAP_TASK_ERROR, CUSTOM_ERROR_KEY} from "../utills";
import { getLodash } from '../../../utilities/lodash';
import { ActionDispatcher } from '../../../uibuilder/Actions/ActionDispatcher';
import { SetPageContext }
  from '../../../uibuilder/ActionType/pageConfig/SetPageContext';

export default class Drag extends DragHelper {
  static get defaultConfig () {
    return {
      // Don't drag the actual row element, clone it
      cloneTarget: true,
      mode: 'translateXY',
      // 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)'
    };
  }

  construct (config) {
    const me = this;

super.construct(config);

me.on({
  dragstart: me.onTaskDragStart,
  drag: me.onTaskDrag,
  drop: me.onTaskDrop,
  thisObj: me
});
  }

  /**
   * 
   * @param {object} schedulerObj : contains the scheduler object
   * @param {string} key : contains the key
   * @returns {string}: based on the key
   */
  getErrorMsg = (schedulerObj, key)=> {
    const customError= getLodash(schedulerObj.jsonDef, 'customErrorMessage');
    let errorMsg='';
    if(key){
      switch (key) {
        case CUSTOM_ERROR_KEY.TASK_OVERLAPPING_ERROR:
          errorMsg= customError?.taskOverlappingErrorMsg || TECHNICIAN_OVERLAP_TASK_ERROR;
          break;
        case CUSTOM_ERROR_KEY.WORKFLOW_UPDATE_ERROR:
          errorMsg= customError?.workflowUpdateErrorMsg || WORKFLOW_UPDATE_ERROR;
          break;
        case CUSTOM_ERROR_KEY.PAST_DATE_ERROR:
          errorMsg= customError?.pastDateErrorMsg || DRAG_PAST_DATE_ERROR_MSG;
          break;
        default:
          break;
      }
    }
    return errorMsg;
  }
  
/** * * @param {object} schedulerObj : contains scheduler object * @param {string} errorMsgKey : contains key based on that error message will come */ showErrorMsg = (schedulerObj, errorMsgKey)=> { const errorMsg= this.getErrorMsg(schedulerObj,errorMsgKey); schedulerObj.setShowAlert(true); schedulerObj.setErrorMessage(errorMsg); } /** * * @param {object} obj : contains the object value use to set the invalid of scheduler * @returns {boolean} : if valid true else false */ isContextValid = (obj)=> { let isValidContext = false; if(obj && obj.valid && Boolean(obj.date && obj.resource)){ isValidContext= true; //Now check for overlap task if allowOverlap false defined in the config if(obj && obj.schedule && !obj.schedule.allowOverlap && !obj.allowTechnicianOverlap && !obj.allowEventOverlap){ isValidContext= obj.schedule.isDateRangeAvailable(obj.startDate, obj.endDate, null, obj.resource); !isValidContext && this.showErrorMsg(obj.me, CUSTOM_ERROR_KEY.TASK_OVERLAPPING_ERROR); } } return isValidContext; } onTaskDragStart ({ context }) { const me = this, { schedule } = me, mouseX = context.clientX, proxy = context.element, task = me.grid.getRecordFromElement(context.grabbed), newSize = me.schedule.timeAxisViewModel.getDistanceForDuration(task.durationMS); // save a reference to the task so we can access it later context.task = task; // Mutate dragged element (grid row) into an event bar proxy.classList.remove('b-grid-row'); proxy.classList.add('b-sch-event-wrap'); proxy.classList.add('b-unassigned-class'); proxy.classList.add(`b-${schedule.mode}`); // proxy.innerHTML = `<i class="${task.iconCls}"></i> ${task.name}`; proxy.innerHTML = StringHelper.xss` <div class="b-sch-event b-has-content"> <div class="b-sch-event-content"> <div>${task.name}</div> <span>Duration: ${task.duration} ${task.durationUnit}</span> </div> </div> `; me.schedule.enableScrollingCloseToEdges(me.schedule.timeAxisSubGrid); if (schedule.isHorizontal) { // If the new width is narrower than the grabbed element... if (context.grabbed.offsetWidth > newSize) { const proxyRect = Rectangle.from(context.grabbed); // If the mouse is off (nearly or) the end, centre the element on the mouse if (mouseX > proxyRect.x + newSize - 20) { context.newX = context.elementStartX = context.elementX = mouseX - newSize / 2; DomHelper.setTranslateX(proxy, context.newX); } } proxy.style.width = `${newSize}px`; } else { const width = schedule.resourceColumns.columnWidth; // Always center horizontal under mouse for vertical mode context.newX = context.elementStartX = context.elementX = mouseX - width / 2; DomHelper.setTranslateX(proxy, context.newX); proxy.style.width = `${width}px`; proxy.style.height = `${newSize}px`; } // Prevent tooltips from showing while dragging schedule.element.classList.add('b-dragging-event'); } onTaskDrag ({ event, context }) { const me = this, { schedule } = me, { task } = context, coordinate = DomHelper[`getTranslate${me.schedule.isHorizontal ? 'X' : 'Y'}`](context.element), date = me.schedule.getDateFromCoordinate(coordinate, 'round', false), // Coordinates required when used in vertical mode, since it does not use actual columns resource = context.target && schedule.resolveResourceRecord(context.target, [event.offsetX, event.offsetY]), // Don't allow drops anywhere, only allow drops if the drop is on the timeaxis and on top of a Resource paramObj= { valid: context.valid, date,resource}, isValid= this.isContextValid(paramObj); context.valid = isValid; // Save reference to resource so we can use it in onTaskDrop context.resource = resource; } // Drop callback after a mouse up, take action and transfer the unplanned task to the real SchedulerEventStore (if it's valid) async onTaskDrop ({ context, event }) {
const me = this, { task, target, resource } = context, listCount = me.grid.data.length, { schedule } = me, coordinate = DomHelper[`getTranslate${schedule.isHorizontal ? 'X' : 'Y'}`](context.element), date = me.schedule.getDateFromCoordinate(coordinate, 'round', false), startDate = schedule.getDateFromCoordinate(coordinate, 'round', false), endDate = startDate && DateHelper.add(startDate, task.duration, task.durationUnit), paramObj= { valid: context.valid, date,resource,schedule, startDate, endDate , me, allowTechnicianOverlap: resource?.originalData?.allowTechnicianOverlap, alloweEventOverlap: task?.originalData?.alloweEventOverlap } schedule.disableScrollingCloseToEdges(me.schedule.timeAxisSubGrid); const isValid = this.isContextValid(paramObj); // If drop was done in a valid location, set the startDate and transfer the task to the Scheduler event store if (isValid && context.valid && target) { const // Try resolving event record from target element, to determine if drop was on another event targetEventRecord = me.schedule.resolveEventRecord(context.target); const finalize = me.context.finalize; if(moment().isAfter(date, 'second')) { this.showErrorMsg(me,CUSTOM_ERROR_KEY.PAST_DATE_ERROR) return false; } if (date) { me.setShowAlert(false); me.setErrorMessage(''); me.showLoader(); const pageContext = new SetPageContext({taskDetail: task.data, resourceId: context.resource.data.id }); debugger; this.dispatch(pageContext.plainAction()); const ah = new ActionDispatcher(this); ah.doAction(this.jsonDef.dropGridTaskAction); //const response = await me.submitTaskDetails({ ...task.data, resourceId: context.resource.data.id }); me.hideLoader(); // if(response === null || (response && response.errorCode)) { // this.showErrorMsg(me,CUSTOM_ERROR_KEY.WORKFLOW_UPDATE_ERROR) // return false; // } // // Remove from grid first so that the data change // // below does not fire events into the grid. // me.setTaskListCount(listCount-1); // me.grid.store.remove(task); // // task.setStartDate(date, true); // task.startDate = date; // task.resource = context.resource; // me.schedule.eventStore.add(task); } // Dropped on a scheduled event, display toast if (targetEventRecord) { WidgetHelper.toast(`Dropped on ${targetEventRecord.name}`); } finalize(); // me.context.finalize(); } else { me.abort(); } me.schedule.element.classList.remove('b-dragging-event'); } }

My scheduler code is :-

<BryntumSchedulerPro
          ref = {scheduler}
          autoHeight = {false}
          allowOverlap = {isAllowOverlap}
          zoomOnMouseWheel={false}
          zoomOnTimeAxisDoubleClick={false}
          viewPreset = {getViewPreset(timelineView, entityConfig)}
          tbar = {
            [
              {
                type: 'combo',
                value: timelineView,
                editable: false,
                ref: 'viewPresetCombo',
                cls: 'select-view',
                listCls: 'list-items',
                listeners: { change: viewPresetHandler },
                items: getTimeLineViewList()
              },
              {
                type: 'dateField',
                editable: false,
                cls: 'date-field',
                onChange ({ value, userAction }) {
                  if(hasSchedulerInstance() && userAction) {
                    limitMonthRange(value);
                    const diff = DateHelper.diff(
                      DateHelper.clearTime(getSchedulerInstance().startDate),
                      value,
                      'days'
                    );
                    getSchedulerInstance().startDate = DateHelper.add(
                      getSchedulerInstance().startDate,
                      diff,
                      'days'
                    );
                    getSchedulerInstance().scrollToDate(value, {
                      block: getViewPosition()
                    });
                    checkForDataFetch();
                  }
                },
                listeners: {
                  trigger: onDatePickerLoad
                },
                ref: 'selectDate'
              },
              {
                type: 'button',
                text: 'L{todayText}',
                onClick () {
                  const today = DateHelper.clearTime(new Date());
                  if(hasSchedulerInstance()) {
                    limitMonthRange(today);
                    const diff = DateHelper.diff(
                      DateHelper.clearTime(getSchedulerInstance().startDate),
                      today,
                      'days'
                    );
                    getSchedulerInstance().startDate = DateHelper.add(
                      getSchedulerInstance().startDate,
                      diff,
                      'days'
                    );
                    getSchedulerInstance().scrollToDate(today, {
                      block: getViewPosition()
                    });
                    checkForDataFetch();
                  }
                },
                cls: 'btn-today'
              },
              {
                type: 'button',
                icon: 'b-fa-angle-left',
                cls: 'btn-prev',
                onClick () {
                  if(hasSchedulerInstance()) {
                    getSchedulerInstance().shiftPrevious();
                    checkForDataFetch(true);
                  }
                }
              },
              {
                type: 'button',
                icon: 'b-fa-angle-right',
                cls: 'btn-next',
                onClick () {
                  if(hasSchedulerInstance()) {
                    getSchedulerInstance().shiftNext();
                    checkForDataFetch(true);
                  }
                }
              },
              '->',
              {
                type: 'textfield',
                ref: 'filterByName',
                placeholder: 'L{searchPlaceholder}',
                cls: 'input-search',
                clearable: true,
                keyStrokeChangeDelay: 100,
                triggers: {
                  filter: {
                    align: 'start',
                    cls: 'b-fa b-fa-search'
                  }
                },
                listeners: { change: searchHandler }
              }
            ]
          }
          resources = {data.resources || []}
          timeRanges={data.timeRanges || []}
          project ={ {
            timeRangeStore: timeRangeStores,
            resourceTimeRangeStore: resourceTimeRangeStore
            // calendar: 'Weekend',
            // calendarsData: data.calendars

      }}
      resourceTimeRanges={data.resourceTimeRanges || []}
      eventsVersion = {eventsVersion.current}
      resourceTimeRangesFeature={true}
      resourceNonWorkingTimeFeature = {true}
      nonWorkingTimeFeature={ true }
      startDate = {startDate}
      visibleDate = {new Date()}
      endDate = {endDate}
      columns = {getColumnsList(getLodash(jsonDef, 'columns', []))}
      filterBarFeature = {true} // to enable search on columns
      listeners = {{
        presetChange,
        eventClick: ({ eventRecord, eventElement, event }) => {
          eventRecord.data.elementType = TASK;
          const selectedResource = event?.resourceRecord?.originalData;
          setPageContext({ selectedResource: selectedResource });
          openEventPopup(eventRecord, eventElement, event.x, event.y, TASK);
        },
        scheduleDblClick: ({ resourceRecord, tickStartDate,
          tickEndDate, event }) => {
          const selectedResource= resourceRecord?.originalData;
          setPageContext({ selectedResource: selectedResource });
          onScheduleClick(
            resourceRecord, tickStartDate, tickEndDate, event);
        },
        async beforeEventDropFinalize ({ context }) {
          context.async = true;
          // update the task with the dropped dates and resource id
          const task = {
            ...context.eventRecords[FIRST_INDEX].data,
            startDate: context.startDate,
            endDate: context.endDate,
            resourceId: context.newResource.id
          };
            // check again if the date is less than today
          if(moment().isBefore(task.startDate, 'second')) {
            // show progress bar
            showLoader();
            const response = await submitTaskDetails(task);
            if(response === null) {
              setShowAlert(true);
              setErrorMessage(WORKFLOW_UPDATE_ERROR);
              context.finalize(false);
            } else {
              refreshScheduler();
              context.finalize(true);
            }
            if(props.dragCompleted) {
              props?.dragCompleted(task);
            }
            // hide progress bar
            hideLoader();
          }
        }
      }}
      events = {data.events || []}
      eventStyle = {'colored'}
      eventLayout = {eventLayout || 'stack'}
      eventBodyTemplate = {(data) => getEventTemplate(data)}
      eventRenderer = {(eventRecord) => eventRenderer(eventRecord)}
      // disable contextmenu on right click of events
      eventMenuFeature = {false}
      scheduleMenuFeature = {false}
      eventEditFeature = {{
        disabled: true // disable event edit popup on double click of events
      }}
      eventDragCreateFeature = {{
        disabled: true // disable event creation on drag
      }}
      eventDragFeature = {{
        disabled: !jsonDef?.dragAllow,
        validatorFn ({ startDate }) {
          if(moment().isAfter(startDate, 'second')) {
            return {
              valid: false,
              message: DRAG_PAST_DATE_ERROR_MSG
            };
          }
          return {
            valid: true
          };
        }
      }}
      eventTooltipFeature = {{
        template: (data) => getPopupContent(data.eventRecord)
      }}
      eventResizeFeature = {{
        disabled: true
      }}
      stripeFeature={isStripFeature}
      timeRangesFeature = {{
        showCurrentTimeLine: true,
        showHeaderElements: true,
        enableResizing: true
      }}
    />

Post by arcady »

You don't need to call it. It's already called each time you drag a task so the task shifts to the nearest working period of time.
As far as I understood you wanted to customize that logic so I pointed you to the method that should be overridden.


Post by gorakh.nath »

@arcady can I override this method from my code ? if yes then where , which file should I write this method to override above logic? Please provide some work around to override this method.


Post by chrisb »

Why not have two calendars, one for standard time and then another for overtime? And then depending on which time it's in assign the event to the appropriate calendar?


Post by gorakh.nath »

@chrisb How can we assign two calendars, please provide some example or share some link for reference.


Post by saki »

This demo: https://bryntum.com/examples/scheduler-pro/resource-non-working-time/ assigns various calendars to resources but you can assign these calendars to events as well.

The scheduling will be done per rules described here:
https://bryntum.com/docs/scheduler-pro/#SchedulerPro/guides/basics/calendars.md and here:
https://bryntum.com/docs/scheduler-pro/#../engine/schedulerpro_events_scheduling.md


Post Reply