Our powerful JS Calendar component


Post by peter@tjecco.com »

Hi,

I am using dayresource view to display planned events per resource. I am trying to set my coreHours and visibleStartTime like this:

 coreHours: {
          start: 8,
          end: 18
        },
visibleStartTime: 7,

but my visibleStartTime setting does not work, the calendar does not scroll to 7 am. What is the problem here?
When I am using normal day view it does work, but with dayresource doesn't :/

My code snippet:

 const calendar = new bryntum.calendar.Calendar({
      date: new Date(),
      cls: "executors-planned-tasks-calendar",
      height: 600,
      width: 1360,
      viewPreset: "hourAndDay",
      allowOverlap: false,
      shortEventDuration: "1 hour",
      mode: "dayresourceview",
      sidebar: {
        hidden: true,
        items: {
          resourceFilter: null
        }
      },

  project: {
    events,
    resources,
    listeners: {
      refresh({ source }) {
        const rf = calendar.widgetMap.resourceFilter;
        calendar.eventStore.addFilter({
          id: "custom-filter",
          filterBy: (event) => rf.valueCollection.includes(event.resourceId)
        });

        rf.store = source.resourceStore;
        rf.value = rf.store.records[0];
        window.rf = rf;
      },
      once: true
    }
  },

  tbar: {
    items: {
      resourceFilter: {
        type: "combo",
        width: "17rem",
        weight: 100,
        multiSelect: true,
        placeholder: "executors",
        listCls: "custom-resource-filter",
        // The value is the records selected
        valueField: null,
        displayField: "name",
        listItemTpl: (resource) => `
            <div class="resource-list-text">
                <div class="resource-name">${resource.name}</div>
            </div>
        `,
        listeners: {
          // "up." means resolve in ownership chain. Will call on the Calendar
          change: "up.onFilterCriteriaChange",
          // We have to filter after the Calendar has processed the change
          prio: -1000
        },
        // We want the ChipView to scroll horizontally with no wrapping.
        chipView: {
          itemTpl: (resource) => {
            if (resource.name.length > 15) {
              return `${resource.name.substring(0, 12) + "..."}`;
            } else {
              return `${resource.name}`;
            }
          },
          scrollable: {
            overflowX: "hidden-scroll",
            overflowY: false
          }
        }
      },
      todayButton: false,
      prevButton: {
        weight: 200
      },
      viewDescription: {
        weight: 300
      },
      nextButton: {
        weight: 400
      },
      button: {
        weight: 700,
        text: "week",
        cls: "week-mode-button",
        disabled: true,
        tooltip: "Week view is for now disabled",
        onClick: (event) => {}
      }
    }
  },

  // The subviews have a close tool which filters them out
  modeDefaults: {
    timeFormat: "HH:mm",
    coreHours: {
      start: 8,
      end: 18
    },
    view: {
      visibleStartTime: 7,
      // Show a close icon to filter out the resource
      tools: {
        close: {
          cls: "b-fa b-fa-times",
          tooltip: "Filter out this resource",

          // Will find the handler on the Calendar
          handler: "up.onSubviewCloseClick"
        }
      },
      strips: {
        // A simple widget showing total planned calendar events' hours for each resource
        resourceInfo: {
          type: "widget",
          dock: "header",
          cls: "b-total-events-duration-header",
          // This method gets called when the panel is created and we return some meta data about the
          // resource, like "8u". Will be found on the Calendar
          html: "up.getSubViewTotalHoursCountHeader"
        }
      }
    }
  },

  modes: {
    day: null,
    month: null,
    year: null,
    week: null,
    agenda: null,
    dayresource: {
      descriptionRenderer() {
        return `${Ext.Date.format(calendar.date, "j F Y")}`;
      },
      resourceWidth: "18em",
      weight: 500,
      view: {
        type: "dayview",
        showHeaderAvatars: false,
        allDayEvents: {
          fullWeek: false
        },
        eventRenderer: (event) => {
          const eventRecord = event.eventRecord.data,
            renderData = event.renderData;
          let eventCls = "";

          switch (eventRecord.type) {
            case "TASK":
              eventCls = "task-event";
              renderData.eventColor = me.taskEventColor;
              renderData.bodyColor = me.taskEventColor;
              break;
            case "TRAVEL":
              eventCls = "travel-event";
              renderData.eventColor = me.travelEventColor;
              renderData.bodyColor = me.travelEventColor;
              break;
            case "LEAVE":
              eventCls = "leave-event";
              renderData.eventColor = me.taskEventColor;
              renderData.bodyColor = me.taskEventColor;
              break;
            case "BREAK":
              eventCls = "break-event";
              renderData.eventColor = me.breakEventColor;
              renderData.bodyColor = me.breakEventColor;
              break;
            default:
              break;
          }

          let amountOfAssets = eventRecord.planningAssetAmount ? eventRecord.planningAssetAmount : "",
            productCode = eventRecord.productCode ? eventRecord.productCode : "",
            eventName = eventRecord.name ? eventRecord.name : "";

          if (eventRecord.type === "TASK") {
            amountOfAssets = `${amountOfAssets}x`;

            //show "..." when text is too long
            switch (eventRecord.duration) {
              case 0.5:
              case 0.75:
              case 1:
                if (eventName.length >= 14 && eventName.length < 55) {
                  eventName = eventName.substring(0, 14) + "...";
                }
                break;
              case 1.25:
                if (eventName.length >= 55 && eventName.length < 60) {
                  eventName = eventName.substring(0, 55) + "...";
                }
                break;
              case 1.5:
                if (eventName.length >= 60 && eventName.length < 75) {
                  eventName = eventName.substring(0, 60) + "...";
                }
                eventName = eventName.substring(0, 60) + "...";
                break;
              case 1.75:
                if (eventName.length >= 75 && eventName.length < 85) {
                  eventName = eventName.substring(0, 75) + "...";
                }
                break;
              case 2:
                if (eventName.length >= 85 && eventName.length < 115) {
                  eventName = eventName.substring(0, 85) + "...";
                }
                break;
              case 2.25:
                if (eventName.length >= 115 && eventName.length < 135) {
                  eventName = eventName.substring(0, 115) + "...";
                }
                break;
              case 2.5:
                if (eventName.length >= 135 && eventName.length < 155) {
                  eventName = eventName.substring(0, 135) + "...";
                }
                break;
              case 2.75:
                if (eventName.length >= 155) {
                  eventName = eventName.substring(0, 155) + "...";
                }
                break;
              default:
                break;
            }
          } else if (eventRecord.type === "TRAVEL") {
            eventName = UCare4.util.Translator.translate("travel_time");
          }

          const html = `<div id="custom-${eventCls}">
            <div class="custom-event-name-container">
              <span class="custom-event-name-text">${amountOfAssets}<span class="product-code-text">${productCode}</span> ${eventName}</span>
            </div>
          </div>`;

          return html;
        }
      },
      hideNonWorkingDays: false,
      range: "day",
      type: "resource",
      title: UCare4.util.Translator.translate("day"),
      // Demo uses more padding than default, switch to the short event duration "earlier" to fit contents
      shortEventDuration: "1 hour"
    },
    // Mode name can be anything if it contains a "type" property.
    weekResources: {
      weight: 600,
      disabled: true,
      // Type has the final say over which view type is created
      type: "resource",
      title: UCare4.util.Translator.translate("workweek"),
      // Specify how wide each resource panel should be
      resourceWidth: "4em",
      hideNonWorkingDays: true,
      fitHours: true,
      // Info to display below a resource name
      meta: (resource) => resource.name
    }
  },
  // Features named by the properties are included.
  // An object is used to configure the feature.
  features: {
    scheduleContextMenu: false,
    eventMenu: {
      items: {
        editEvent: false,
        duplicate: false,
        deleteEvent: {
          weight: 300,
          icon: "b-fa b-fa-fw b-fa-calendar-times",
          async onItem(event) {
            if (event && event.eventRecord && event.eventRecord.data) {
              const eventRecord = event.eventRecord;

              if (eventRecord.data.travelEventId) {
                const travelEventResponse = await UCare4.util.Client.delete(
                  `${Ext.manifest.calendar_service}/api/rest/v1/event/${eventRecord.data.travelEventId}`
                );

                if (!me.isSuccess(travelEventResponse.status)) {
                  me.showEventCouldNotBeDeletedError();
                  return;
                }
              }

              const taskEventResponse = await UCare4.util.Client.delete(
                `${Ext.manifest.calendar_service}/api/rest/v1/event/${eventRecord.data.realEventId}`
              );

              if (!me.isSuccess(taskEventResponse.status)) {
                me.showEventCouldNotBeDeletedError();
                return;
              }

              await me.refreshData(calendar, true);
              me.showEventIsSuccessfullyUnscheduled();

              if (eventRecord && eventRecord.data) {
                calendar.recalculateEventDurationWidgetForAllResources(eventRecord.data.resourceId);
              }
            }
          }
        }
      }
    },
    scheduleMenu: {
      items: {
        // Knocks out the predefined addEvent item
        addEvent: null
      }
    },
    externalEventSource: {
      dragRootElement: "tasks-to-plan-list",
      dragItemSelector: ".contract-planning-management-listitem",
      droppable: true,
      draggable: true,
      getRecordFromElement(element) {
        // Return an object from which an EventModel can be created.
        // Same format as loading an EventStore. { name : 'name', startDate: ''} etc
        return me.createRecordFromElement(element);
      }
    },
    drag: {
      // Each drag mode has a separate validation callback.
      // We route them all to one on the calendar instance
      validateCreateFn() {
        return calendar.validateCreate(...arguments);
        //do something
      }
    }
  },
  listeners: {
    async dropExternal(event) {
      let result = false;
      const dragTargetResource = event.event.target && 
                calendar.resolveResourceRecord(event.event.target);

      let startDate = calendar.getDateFromDomEvent(event.domEvent),
        endDate = me.calculateEndDate(startDate, event.eventRecord.data.duration);

      event.eventRecord.data["startDate"] = startDate;
      event.eventRecord.data["endDate"] = endDate;

      const dateIsAvailable = calendar.eventStore.isDateRangeAvailable(
        startDate,
        endDate,
        event.eventRecord,
        dragTargetResource
      );

      if (!dateIsAvailable) {
        return false;
      }

      event.eventRecord.data["resourceId"] = dragTargetResource.data.id;
      // Show MessageBox and wait for user response
      const userResponse = await new Promise((resolve) => {
        UCare4.ux.MessageBox.showConfirm(
          UCare4.util.Translator.translate("calculate_travel_time"),
          UCare4.util.Translator.translate("calculate_travel_time_message"),
          (choice) => {
            resolve(choice);
          }
        );
      });

      // Validate based on user response
      if (userResponse === "yes") {
        result = await me.createEventWithTravelTime(event, calendar);
      } else {
        result = await me.createEvent(event, calendar);
      }

      return result;
    },

    dragMoveEnd: async (event) => {
      if (event.drag && event.drag.source && event.drag.source.isExternalZone) {
        calendar.recalculateEventDurationWidget(event);
      }

      await me.refreshData(calendar, true);
    },

    beforeDragMoveEnd: async (event) => {
      if (event.drag && event.drag.source && event.drag.source.isExternalZone) return true;
      return await calendar.executeDragEnd(event, false);
    },

    beforeDragResizeEnd: async (event) => {
      if (event.drag && event.drag.source && event.drag.source.isExternalZone) return true;
      return await calendar.executeDragEnd(event, true);
    },

    paint: () => {
      calendar.addHoverTooltipToWeekButton();
    },

    eventClick: (event) => {
      if (event && event.eventRecord && event.eventRecord.data) {
        //set executor where event is planned - just for now
        const resourceRecord = event.resourceRecord.data;
        event.eventRecord.data["executorName"] = resourceRecord.name;
        event.eventRecord.data["executorId"] = resourceRecord.id;
        event.eventRecord.data["executorComplianceUserId"] = resourceRecord.compliance_user_id;
        event.eventRecord.data["executorAvatar"] = resourceRecord.avatar;

        if (event.eventRecord.type === "TASK") {
          //create calendar event edit/read detail popup
          Ext.create("UCare4.ux.popup.calendarevent.CalendarEventPopup", {
            eventRecord: event.eventRecord.data
          }).show();
        } else if (event.eventRecord.type === "LEAVE") {
          Ext.create("UCare4.ux.popup.customcalendarevent.CustomCalendarEventPopup", {
            eventRecord: event.eventRecord.data,
            calendar: calendar
          }).show();
        }
      }
    },

    eventMouseOver: (event) => {
      const eventRecord = event.eventRecord.data,
        dialog = Ext.ComponentQuery.query("ucare4-event-tip");

      if (dialog) {
        Ext.each(dialog, (tip) => {
          tip.destroy();
        });
      }

      if (eventRecord.type === "TASK") {
        Ext.create("UCare4.ux.tip.calendarevent.EventTip", {
          source: event,
          eventRecord: eventRecord
        }).show();
      }
    },
    dateChange: async (event) => {
      await me.refreshData(calendar, true);
      calendar.recalculateEventDurationWidgetForAllResources();
    }
  },

  handleYesResponse: async (event) => {
    const response = await me.deleteEventTravelTime(event);
    if ((response && Ext.isObject(response) && me.isSuccess(response.status)) || response === true) {
      return await me.updateEventWithTravelTime(event, calendar);
    } else {
      return false;
    }
  },

  handleNoResponse: async (event) => {
    const response = await me.deleteEventTravelTime(event);
    if ((response && Ext.isObject(response) && me.isSuccess(response.status)) || response === true) {
      return await me.updateEvent(event);
    } else {
      return false;
    }
  },

  addHoverTooltipToWeekButton: () => {
    const weekResourcesShowButton = document.querySelector('[data-ref="weekResourcesShowButton"]'),
      tooltip = document.createElement("div");

    tooltip.className = "week-resources-show-button-tooltip";
    tooltip.innerHTML = "Week view is for now disabled"; //TODO: translate later

    document.body.appendChild(tooltip);

    weekResourcesShowButton.addEventListener("mouseover", () => {
      // Change the button's background color
      const rect = weekResourcesShowButton.getBoundingClientRect();
      const top = rect.top - tooltip.offsetHeight - 5;
      const left = rect.left + (weekResourcesShowButton.offsetWidth - tooltip.offsetWidth) / 2;

      // Set the position and display the tooltip
      tooltip.style.top = top + "px";
      tooltip.style.left = left + "px";
      tooltip.style.display = "block";
    });

    weekResourcesShowButton.addEventListener("mouseout", function () {
      // Hide the tooltip on mouseout
      tooltip.style.display = "none";
    });
  },

  validateCreate(event) {
    const dragTargetResource = event.event.target && calendar.resolveResourceRecord(event.event.target);

    const dateIsAvailable = calendar.eventStore.isDateRangeAvailable(
      event.eventRecord.data.startDate,
      event.eventRecord.data.endDate,
      event.eventRecord,
      dragTargetResource
    );

    if (!dateIsAvailable) {
      return false;
    }

    if (dragTargetResource) event.eventRecord.data["resourceId"] = dragTargetResource.data.id;

    Ext.create("UCare4.ux.popup.customcalendarevent.CustomCalendarEventPopup", {
      eventRecord: event.eventRecord.data,
      calendar: calendar
    }).show();

    if (event.eventRecord && event.eventRecord.data) {
      calendar.recalculateEventDurationWidget(event, event.eventRecord.data.duration);
    }
    return true;
  },

  onSubviewCloseClick(domEvent, view) {
    const values = calendar.widgetMap.resourceFilter.value;
    let newValues = [];

    for (const value of values) {
      if (value.data.id === view.resourceId) {
        continue;
      }
      newValues.push(value);
    }
    this.widgetMap.resourceFilter.value = newValues;
  },

  getSubViewTotalHoursCountHeader(widget, resourceId, newEventRecordDuration) {
    let id = null;

    if (widget) {
      id = widget.owner.resourceId;
    }

    const events = calendar.events;

    if (!resourceId) {
      resourceId = id;
    }

    let totalEventsDuration = 0;

    for (const event of events) {
      if (event && event.resourceId === resourceId && calendar.isSameDay(event.startDate, calendar.date)) {
        totalEventsDuration = totalEventsDuration + event.data.duration;
      }
    }

    if (!totalEventsDuration) {
      totalEventsDuration = "--";
    }

    if (newEventRecordDuration) {
      totalEventsDuration = totalEventsDuration + newEventRecordDuration;
    }

    if (Number.isInteger(totalEventsDuration)) {
      // If there are no decimal places, return the number as is
      totalEventsDuration = totalEventsDuration;
    } else if (Ext.isNumeric(totalEventsDuration)) {
      // If there are decimal places, round to 2 decimal places
      totalEventsDuration = Ext.Number.toFixed(totalEventsDuration, 2);
    }

    return `<span class="total-event-duration blue fat">${totalEventsDuration} ${UCare4.util.Translator.translate(
      "hours_short"
    )}</span>`;
  },

  isSameDay: (eventDate, calendarDate) => {
    // Create Date objects for the given date and today's date
    const inputDate = new Date(eventDate);

    // Set hours, minutes, seconds, and milliseconds to 0 for accurate comparison
    inputDate.setHours(0, 0, 0, 0);
    calendarDate.setHours(0, 0, 0, 0);

    // Compare the two Date objects
    return inputDate.getTime() === calendarDate.getTime();
  },

  executeDragEnd: async (event, addNewDuration) => {
    let result = false,
      dragTargetResource = event.event.target && calendar.resolveResourceRecord(event.event.target);

    if (event.eventRecord.data.type === "TRAVEL") {
      UCare4.util.Notificator.show(
        "{event_travel_time_may_not_be_changed}",
        UCare4.util.Notificator.WARNING,
        2000,
        true,
        true
      );
      return false;
    }

    const dateIsAvailable = calendar.eventStore.isDateRangeAvailable(
      event.newStartDate,
      event.newEndDate,
      event.eventRecord,
      dragTargetResource
    );

    if (!dateIsAvailable) {
      return false;
    }

    if (
      event.eventRecord.originalData.resourceId !== dragTargetResource.data.id ||
      event.eventRecord.data.resourceId !== dragTargetResource.data.id
    ) {
      return false;
    }

    // Show MessageBox and wait for user response
    const userResponse = await new Promise((resolve) => {
      UCare4.ux.MessageBox.showConfirm(
        UCare4.util.Translator.translate("calculate_travel_time"),
        UCare4.util.Translator.translate("calculate_travel_time_message"),
        (choice) => {
          resolve(choice);
        }
      );
    });

    // Validate based on user response
    if (userResponse === "yes") {
      result = await calendar.handleYesResponse(event);
    } else {
      result = await calendar.handleNoResponse(event);
    }

    if (!result) return false;

    if (event.eventRecord && event.eventRecord.data) {
      if (addNewDuration) {
        const startDateUtc = me.getUtcDate(event.newStartDate),
          endDateUtc = me.getUtcDate(event.newEndDate),
          newDuration = me.getDurationInHoursFromUtc(startDateUtc, endDateUtc);

        event.eventRecord.data["duration"] = newDuration;
        calendar.recalculateEventDurationWidget(event);
      } else {
        calendar.recalculateEventDurationWidget(event);
      }
    }
    return true;
  },

  recalculateEventDurationWidget: (event, duration = null) => {
    if (event.view) {
      let resourceView = event.view.element,
        el = resourceView.querySelector(".total-event-duration"),
        newHtml = calendar.getSubViewTotalHoursCountHeader(null, event.eventRecord.data.resourceId, duration);

      el.outerHTML = newHtml;
    } else {
      calendar.recalculateEventDurationWidgetForAllResources(event.eventRecord.data.resourceId, duration);
    }
  },

  recalculateEventDurationWidgetForAllResources: function (resourceIdFromEvent = null, duration) {
    //refresh resourceInfo widget
    let resourceViewContent = document.querySelector(".b-resourceview-content").children;

    for (let resource of resourceViewContent) {
      const firstObjectIndex = 0; // Arrays are zero-indexed, so the second object is at index 1
      const lastObjectIndex = resourceViewContent.length - 1; //
      if (
        resourceViewContent[firstObjectIndex].$refOwnerId !== resource.$refOwnerId &&
        resourceViewContent[lastObjectIndex].$refOwnerId !== resource.$refOwnerId
      ) {
        const el = resource.querySelector(".total-event-duration"),
          resourceRecordFromEl = calendar.resolveResourceRecord(resource);

        if (resourceRecordFromEl && resourceIdFromEvent === resourceRecordFromEl.data.id) {
          newHtml = calendar.getSubViewTotalHoursCountHeader(null, resourceIdFromEvent, duration);
          el.outerHTML = newHtml;
        } else if (resourceIdFromEvent === null && resourceRecordFromEl && resourceRecordFromEl.data.id) {
          newHtml = calendar.getSubViewTotalHoursCountHeader(null, resourceRecordFromEl.data.id);
          el.outerHTML = newHtml;
        }
      }
    }
  },

  onFilterCriteriaChange() {
    this.eventStore.filter();
    this.modes.dayresource.onResourceFilterSelectionChange();
  }
});

Post by tasnim »

Hi,

The visibleStartTime needs to be in the root of modeDefaults

    modeDefaults : {
        visibleStartTime : 12
    },

Then It'll work.

Good Luck👍
Tasnim


Post by peter@tjecco.com »

Hi,

it does not work sadly.. If I place

visibleStartTime

in the root of

modeDefaults

, then my coreHours does not work anymore :/


Post by tasnim »

Hi,

Are you able to reproduce it here https://bryntum.com/products/calendar/examples/date-resource/? If you can, can you please share the steps to reproduce it?


Post by peter@tjecco.com »

Hi,

Yes I indeed can :) I see that it stops working when I add type: "resource" in the root of dayresource. You can copy paste this code to see it yourself https://bryntum.com/products/calendar/examples/date-resource/:

import { Calendar } from '../../build/calendar.module.js?474872';
import shared from '../_shared/shared.module.js?474872';

const calendar = new Calendar({
    appendTo : 'container',
    date     : new Date(2023, 3, 2),

// CrudManager arranges loading and syncing of data in JSON form from/to a web service
crudManager : {
    autoLoad : true,
    loadUrl  : 'data/data.json'
},

resourceImagePath : '../_shared/images/users/',

modeDefaults: {
    timeFormat: "HH:mm",
    visibleStartTime: 7,
    coreHours: {
          start: 8,
          end: 18
        },      
},


modes : {
    day         : null,
    week        : null,
    month       : null,
    year        : null,
    agenda      : null,
    dayresource : {
      range: "day",
      type: "resource",
      view: {
        type: "dayview",

        },
        // Save a little space by hiding weekends.
        hideNonWorkingDays : true,

        // Configure a nice min-width for the resource columns
        minResourceWidth : '10em',

        // Demo uses more padding than default, switch to the short event duration "earlier" to fit contents
        shortEventDuration : '1 hour'
    }
},

sidebar : {
    // Existing sidebar widgets can be customized and extra UI Widgets can be easily added too
    items : {
        datePicker : {
            tbar : {
                // Hide the next/prev year buttons for a bit cleaner UI
                items : {
                    prevYear : false,
                    nextYear : false
                }
            }
        },
        resourceFilter : {
            minHeight : '22em',
            store     : {
                // Group resources by a custom `team` field
                groupers : [
                    { field : 'team', ascending : true }
                ]
            },
            // initially select record team members of the Austin team
            selected : [1, 2, 3]
        }
    }
},

tbar : {
    items : {
        hideEmptyresources : {
            type     : 'checkbox',
            text     : 'Hide unscheduled resources',
            weight   : 600,
            checked  : false,
            style    : 'margin   : 0 1em',
            // "up." means resolve in owner will call on the Calendar
            onChange : 'up.onHideEmptyResourcesChanged'
        },

        showAvatars : {
            type     : 'checkbox',
            text     : 'Show avatar',
            weight   : 600,
            checked  : true,
            style    : 'margin   : 0 1em',
            // "up." means resolve in owner will call on the Calendar
            onChange : 'up.onShowAvatarsChanged'
        },

        hideWeekends : {
            type     : 'checkbox',
            text     : 'Hide weekends',
            weight   : 600,
            checked  : true,
            style    : 'margin   : 0 1em',
            // "up." means resolve in owner will call on the Calendar
            onChange : 'up.onHideWeekendsChanged'
        },

        viewWidth : {
            type        : 'slider',
            text        : 'Resource width',
            weight      : 640,
            min         : 4,
            max         : 35,
            value       : 10,
            width       : 150,
            unit        : 'em',
            showValue   : false,
            showTooltip : true,
            onInput     : 'up.onResourceWidthChanged'
        }
    }
},

onHideEmptyResourcesChanged({ value }) {
    this.activeView.hideEmptyResources = value;
},

onShowAvatarsChanged({ value }) {
    this.activeView.showHeaderAvatars = value;
},

onHideWeekendsChanged({ value }) {
    this.activeView.hideNonWorkingDays = value;
},

onResourceWidthChanged({ source : { unit }, value }) {
    this.activeView.minResourceWidth = `${value}${unit}`;
}
});

Post by ghulam.ghous »

Hi Peter,

The problem here is you are using type: resource inside a dayResource mode. But it is incorrect type. You should be using type: dayResource here. See the docs here https://bryntum.com/products/calendar/docs/api/Calendar/view/Calendar#config-modes.

modes : {
    day         : true,
    week        : null,
    month       : null,
    year        : null,
    agenda      : null,
    dayresource : {
      range: "day",
      type: "dayResource",
      view: {
        type: "dayview",

    },
    // Save a little space by hiding weekends.
    hideNonWorkingDays : true,

    // Configure a nice min-width for the resource columns
    minResourceWidth : '10em',

    // Demo uses more padding than default, switch to the short event duration "earlier" to fit contents
    shortEventDuration : '1 hour'
}
},

And you can see the visibleTime and coreHours both working as expected.

import { Calendar } from '../../build/calendar.module.js?474872';
import shared from '../_shared/shared.module.js?474872';

const calendar = new Calendar({
    appendTo : 'container',
    date     : new Date(2023, 3, 2),

// CrudManager arranges loading and syncing of data in JSON form from/to a web service
crudManager : {
    autoLoad : true,
    loadUrl  : 'data/data.json'
},

resourceImagePath : '../_shared/images/users/',

modeDefaults: {
    timeFormat: "HH:mm",
    visibleStartTime: 7,
    coreHours: {
          start: 8,
          end: 18
        },      
}, modes : { day : null, week : null, month : null, year : null, agenda : null, dayresource : { range: "day", type: "dayResource", view: { type: "dayview", }, // Save a little space by hiding weekends. hideNonWorkingDays : true, // Configure a nice min-width for the resource columns minResourceWidth : '10em', // Demo uses more padding than default, switch to the short event duration "earlier" to fit contents shortEventDuration : '1 hour' } }, sidebar : { // Existing sidebar widgets can be customized and extra UI Widgets can be easily added too items : { datePicker : { tbar : { // Hide the next/prev year buttons for a bit cleaner UI items : { prevYear : false, nextYear : false } } }, resourceFilter : { minHeight : '22em', store : { // Group resources by a custom `team` field groupers : [ { field : 'team', ascending : true } ] }, // initially select record team members of the Austin team selected : [1, 2, 3] } } }, tbar : { items : { hideEmptyresources : { type : 'checkbox', text : 'Hide unscheduled resources', weight : 600, checked : false, style : 'margin : 0 1em', // "up." means resolve in owner will call on the Calendar onChange : 'up.onHideEmptyResourcesChanged' }, showAvatars : { type : 'checkbox', text : 'Show avatar', weight : 600, checked : true, style : 'margin : 0 1em', // "up." means resolve in owner will call on the Calendar onChange : 'up.onShowAvatarsChanged' }, hideWeekends : { type : 'checkbox', text : 'Hide weekends', weight : 600, checked : true, style : 'margin : 0 1em', // "up." means resolve in owner will call on the Calendar onChange : 'up.onHideWeekendsChanged' }, viewWidth : { type : 'slider', text : 'Resource width', weight : 640, min : 4, max : 35, value : 10, width : 150, unit : 'em', showValue : false, showTooltip : true, onInput : 'up.onResourceWidthChanged' } } }, onHideEmptyResourcesChanged({ value }) { this.activeView.hideEmptyResources = value; }, onShowAvatarsChanged({ value }) { this.activeView.showHeaderAvatars = value; }, onHideWeekendsChanged({ value }) { this.activeView.hideNonWorkingDays = value; }, onResourceWidthChanged({ source : { unit }, value }) { this.activeView.minResourceWidth = `${value}${unit}`; } });

Regards,
Ghous


Post by peter@tjecco.com »

Hi,

VisibleTime and coreHours seems to work right now. Thanks! :) I have added my own styling for the events, but that does not work anymore neither my custom resourceFilter :/ I get the undefined error for "onResourceFilterSelectionChange" function inside the onFilterCriteriaChange event. And also when I am trying to drop events on the calendar, the event won't be created. It seems that if I change the type from "resource" to "dayresource" it breaks my functionality :/ My code:

const taskEventColor = "#cdcdcd",
  travelEventColor = "#1e2d56",
  leaveEventColor = "#1e2d56",
  breakEventColor = "#1e2d56",
  taskEventTextColor = "#1e2d56",
  travelEventTextColor = "#ffffff";


const events = [
    {
        "body": "",
        "resourceId": 9,
        "calendarOwnerUserId": 9,
        "complianceContactPersonId": 1,
        "realEventId": 10,
        "locationId": null,
        "subject": "Reistijd",
        "name": "Reistijd",
        "travelEventId": null,
        "type": "TRAVEL",
        "startDate": "2024-03-14T08:30:00.000Z",
        "endDate": "2024-03-14T10:00:00.000Z",
        "duration": 1.5,
        "durationUnit": "hours",
        "taskId": null,
        "planningTaskId": null,
        "eventStyle": "plain",
        "buildingId": null
    },
    {
        "body": "",
        "resourceId": 3,
        "calendarOwnerUserId": 3,
        "complianceContactPersonId": 2,
        "realEventId": 15,
        "locationId": null,
        "subject": "Reistijd",
        "name": "Reistijd",
        "travelEventId": null,
        "type": "TRAVEL",
        "startDate": "2024-03-15T05:00:00.000Z",
        "endDate": "2024-03-15T05:30:00.000Z",
        "duration": 0.5,
        "durationUnit": "hours",
        "taskId": null,
        "planningTaskId": null,
        "eventStyle": "plain",
        "buildingId": null
    },
    {
        "body": "",
        "resourceId": 9,
        "calendarOwnerUserId": 9,
        "complianceContactPersonId": 3,
        "realEventId": 17,
        "locationId": 14,
        "subject": "Bijvank",
        "name": "Bijvank 260",
        "travelEventId": null,
        "type": "TASK",
        "startDate": "2024-03-15T12:45:00.000Z",
        "endDate": "2024-03-15T15:15:00.000Z",
        "duration": 2.5,
        "durationUnit": "hours",
        "taskId": null,
        "planningTaskId": 1,
        "eventStyle": "plain",
        "buildingId": 1,
        "contracteeComplianceLicenseholderId": 1,
        "contractContactPersonId": 2,
        "contracteeComplianceTaskId": 2,
        "complianceTaskId": 1,
        "contracteeComplianceCustomerId": 1,
        "contracteeComplianceBuildingId": 1,
        "productCode": "017",
        "productName": "09603 - Analyse Legionella drinkwater",
        "complianceBuildingContactPersonAvatar": null,
        "complianceBuildingContactPersonName": null,
        "reportingContactPerson1": "HB",
        "reportingContactPerson2": "HB",
        "reportingContactPerson3": "AX",
        "customerName": "Woningcorporatie Domijn",
        "isManagement": true,
        "taskPlanningStartDate": "2024-03-01T03:00:00.000Z",
        "taskPlanningEndDate": "2024-04-30T02:00:00.000Z",
        "fullEventAddress": "Het Bijvank 260, 7544 DB, Enschede",
    },
     {
        "body": "",
        "resourceId": 9,
        "calendarOwnerUserId": 9,
        "complianceContactPersonId": 3,
        "realEventId": 2,
        "locationId": 14,
        "subject": "Bijvank",
        "name": "Bijvank 260",
        "travelEventId": null,
        "type": "TASK",
        "startDate": "2024-03-15T15:45:00.000Z",
        "endDate": "2024-03-15T17:15:00.000Z",
        "duration": 3.5,
        "durationUnit": "hours",
        "taskId": null,
        "planningTaskId": 1,
        "eventStyle": "plain",
        "buildingId": 1,
        "contracteeComplianceLicenseholderId": 1,
        "contractContactPersonId": 2,
        "contracteeComplianceTaskId": 2,
        "complianceTaskId": 1,
        "contracteeComplianceCustomerId": 1,
        "contracteeComplianceBuildingId": 1,
        "productCode": "017",
        "productName": "09603 - Analyse Legionella drinkwater",
        "complianceBuildingContactPersonAvatar": null,
        "complianceBuildingContactPersonName": null,
        "reportingContactPerson1": "HB",
        "reportingContactPerson2": "HB",
        "reportingContactPerson3": "AX",
        "customerName": "Woningcorporatie Domijn",
        "isManagement": true,
        "taskPlanningStartDate": "2024-03-01T04:00:00.000Z",
        "taskPlanningEndDate": "2024-04-30T03:00:00.000Z",
        "fullEventAddress": "Het Bijvank 260, 7544 DB, Enschede",
    },

];
const resources = [
        {
            "id": 9,
            "contact_person_id": 1,
            "name": "Rik Schrijver",
            "compliance_user_id": 2,
            "avatar": "resources/images/user-icon.jpg"
        },
        {
            "id": 3,
            "contact_person_id": 2,
            "name": "Erwin  Nijlant",
            "compliance_user_id": 3,
            "avatar": "resources/images/user-icon.jpg"
        },
        {
            "id": 6,
            "contact_person_id": 3,
            "name": "Janine  Asteleijner",
            "compliance_user_id": 4,
            "avatar": "resources/images/user-icon.jpg"
        }
    ];

const calendar = new Calendar({
      appendTo: "container",
      date: new Date(),
      cls: "executors-planned-tasks-calendar",
      height: 600,
      width: 1360,
      viewPreset: "hourAndDay",
      allowOverlap: false,
      shortEventDuration: "1 hour",
      mode: "dayresourceview",
      sidebar: {
        hidden: true,
        items: {
          resourceFilter: null
        }
      },

  project: {
    events,
    resources,
    listeners: {
      refresh({ source }) {
        const rf = calendar.widgetMap.resourceFilter;
        calendar.eventStore.addFilter({
          id: "custom-filter",
          filterBy: (event) => rf.valueCollection.includes(event.resourceId)
        });

        rf.store = source.resourceStore;
        rf.value = rf.store.records[0];
        window.rf = rf;
      },
      once: true
    }
  },

  modeDefaults: {
    timeFormat: "HH:mm",
    visibleStartTime: 7,
    coreHours: {
      start: 8,
      end: 18
    },
    view: {
      // Show a close icon to filter out the resource
      tools: {
        close: {
          cls: "b-fa b-fa-times",
          tooltip: "Filter out this resource",

          // Will find the handler on the Calendar
          handler: "up.onSubviewCloseClick"
        }
      },
      strips: {
        // A simple widget showing total planned calendar events' hours for each resource
        resourceInfo: {
          type: "widget",
          dock: "header",
          cls: "b-total-events-duration-header",
          // This method gets called when the panel is created and we return some meta data about the
          // resource, like "8u". Will be found on the Calendar
          html: "up.getSubViewTotalHoursCountHeader"
        }
      }
    }
  },

  modes: {
    day: null,
    week: null,
    month: null,
    year: null,
    agenda: null,
    dayresource: {
      descriptionRenderer() {
      },
      hideNonWorkingDays: false,
      title: "day",
      // Demo uses more padding than default, switch to the short event duration "earlier" to fit contents
      shortEventDuration: "1 hour",
      resourceWidth: "18em",
      weight: 500,
      range: "day",
      type: "dayResource",
      view: {
        type: "dayview",
        showHeaderAvatars: false,
        allDayEvents: {
          fullWeek: false
        },
        eventRenderer: (event) => {
          const eventRecord = event.eventRecord.data,
            renderData = event.renderData;
          let eventCls = "";

          switch (eventRecord.type) {
            case "TASK":
              eventCls = "task-event";
              renderData.eventColor = taskEventColor;
              renderData.bodyColor = taskEventColor;
              break;
            case "TRAVEL":
              eventCls = "travel-event";
              renderData.eventColor = travelEventColor;
              renderData.bodyColor = travelEventColor;
              break;
            case "LEAVE":
              eventCls = "leave-event";
              renderData.eventColor = taskEventColor;
              renderData.bodyColor = taskEventColor;
              break;
            case "BREAK":
              eventCls = "break-event";
              renderData.eventColor = breakEventColor;
              renderData.bodyColor = breakEventColor;
              break;
            default:
              break;
          }

          let amountOfAssets = eventRecord.planningAssetAmount ? eventRecord.planningAssetAmount : "",
            productCode = eventRecord.productCode ? eventRecord.productCode : "",
            eventName = eventRecord.name ? eventRecord.name : "";

          if (eventRecord.type === "TASK") {
            amountOfAssets = `${amountOfAssets}x`;

            //show "..." when text is too long
            switch (eventRecord.duration) {
              case 0.5:
              case 0.75:
              case 1:
                if (eventName.length >= 14 && eventName.length < 55) {
                  eventName = eventName.substring(0, 14) + "...";
                }
                break;
              case 1.25:
                if (eventName.length >= 55 && eventName.length < 60) {
                  eventName = eventName.substring(0, 55) + "...";
                }
                break;
              case 1.5:
                if (eventName.length >= 60 && eventName.length < 75) {
                  eventName = eventName.substring(0, 60) + "...";
                }
                eventName = eventName.substring(0, 60) + "...";
                break;
              case 1.75:
                if (eventName.length >= 75 && eventName.length < 85) {
                  eventName = eventName.substring(0, 75) + "...";
                }
                break;
              case 2:
                if (eventName.length >= 85 && eventName.length < 115) {
                  eventName = eventName.substring(0, 85) + "...";
                }
                break;
              case 2.25:
                if (eventName.length >= 115 && eventName.length < 135) {
                  eventName = eventName.substring(0, 115) + "...";
                }
                break;
              case 2.5:
                if (eventName.length >= 135 && eventName.length < 155) {
                  eventName = eventName.substring(0, 135) + "...";
                }
                break;
              case 2.75:
                if (eventName.length >= 155) {
                  eventName = eventName.substring(0, 155) + "...";
                }
                break;
              default:
                break;
            }
          } else if (eventRecord.type === "TRAVEL") {
            eventName = "Reistijd";
          }

          const html = `<div id="custom-${eventCls}">
              <div class="custom-event-name-container">
                <span class="custom-event-name-text">${amountOfAssets}<span class="product-code-text">${productCode}</span> ${eventName}</span>
              </div>
            </div>`;

          return html;
        }
      }
    },
    weekResources: {
      weight: 600,
      disabled: true,
      // Type has the final say over which view type is created
      type: "resource",
      title: "Werkweek",
      // Specify how wide each resource panel should be
      resourceWidth: "4em",
      hideNonWorkingDays: true,
      fitHours: true,
      // Info to display below a resource name
      meta: (resource) => resource.name
    }
  },

  tbar: {
    items: {
      resourceFilter: {
        type: "combo",
        width: "17rem",
        weight: 100,
        multiSelect: true,
        placeholder: "executors",
        listCls: "custom-resource-filter",
        // The value is the records selected
        valueField: null,
        displayField: "name",
        listItemTpl: (resource) => `
              <div class="resource-list-text">
                  <div class="resource-name">${resource.name}</div>
              </div>
          `,
        listeners: {
          // "up." means resolve in ownership chain. Will call on the Calendar
          change: "up.onFilterCriteriaChange",
          // We have to filter after the Calendar has processed the change
          prio: -1000
        },
        // We want the ChipView to scroll horizontally with no wrapping.
        chipView: {
          itemTpl: (resource) => {
            if (resource.name.length > 15) {
              return `${resource.name.substring(0, 12) + "..."}`;
            } else {
              return `${resource.name}`;
            }
          },
          scrollable: {
            overflowX: "hidden-scroll",
            overflowY: false
          }
        }
      },
      todayButton: false,
      prevButton: {
        weight: 200
      },
      viewDescription: {
        weight: 300
      },
      nextButton: {
        weight: 400
      },
      button: {
        weight: 700,
        text: "week",
        cls: "week-mode-button",
        disabled: true,
        tooltip: "Week view is for now disabled",
        onClick: (event) => {}
      }
    }
  },

  onSubviewCloseClick(domEvent, view) {
    const values = calendar.widgetMap.resourceFilter.value;
    let newValues = [];

    for (const value of values) {
      if (value.data.id === view.resourceId) {
        continue;
      }
      newValues.push(value);
    }
    this.widgetMap.resourceFilter.value = newValues;
  },

  onSubviewCloseClick(domEvent, view) {
    const values = calendar.widgetMap.resourceFilter.value;
    let newValues = [];

    for (const value of values) {
      if (value.data.id === view.resourceId) {
        continue;
      }
      newValues.push(value);
    }
    this.widgetMap.resourceFilter.value = newValues;
  },

  getSubViewTotalHoursCountHeader(widget, resourceId, newEventRecordDuration) {
    let id = null;

    if (widget) {
      id = widget.owner.resourceId;
    }

    const events = calendar.events;

    if (!resourceId) {
      resourceId = id;
    }

    let totalEventsDuration = 0;

    for (const event of events) {
      if (event && event.resourceId === resourceId && calendar.isSameDay(event.startDate, calendar.date)) {
        totalEventsDuration = totalEventsDuration + event.data.duration;
      }
    }

    if (!totalEventsDuration) {
      totalEventsDuration = "--";
    }

    if (newEventRecordDuration) {
      totalEventsDuration = totalEventsDuration + newEventRecordDuration;
    }

    if (Number.isInteger(totalEventsDuration)) {
      // If there are no decimal places, return the number as is
      totalEventsDuration = totalEventsDuration;
    } else if (Ext.isNumeric(totalEventsDuration)) {
      // If there are decimal places, round to 2 decimal places
      totalEventsDuration = Ext.Number.toFixed(totalEventsDuration, 2);
    }

    return `<span class="total-event-duration blue fat">${totalEventsDuration} u
      )}</span>`;
  },

  onFilterCriteriaChange() {
    this.eventStore.filter();
    this.modes.dayresource.onResourceFilterSelectionChange();
  }
});

Post by Animal »

It's simple. visibleStartTime is scrolled to if possible. That is if there's enough scroll range. You just configure it on the view.

See this example: https://codepen.io/Animal-Nige/pen/PogWdZq?editors=0110


Post Reply