Our pure JavaScript Scheduler component


Post by pmiklashevich »

I want to have 2 combos (e.g. 2 fields) editor.

As I suggested above you can:

  • disable "resourceField" which is used by default for all resources
  • create 2 new combos
  • chain the resources and get just machines for one combo and operators for the other combo
  • Select corresponding resources on load
  • Save selected resources in both combos as multiassignment

Have you managed to do this? If no, please let me know what exactly is hard to do and I'll try to help you.

But at calendar I want to have one column with groups, like at sample

You probably mean locked part of the scheduler (left side). You just need to group resource store by the field which is responsible for the resource type. For example:

new SchedulerPro({
    resourceStore : {
        groupers : [
        // type could be 'Machines', 'Operators', etc
            { field : 'type', ascending : false }
        ]
    }
})

Please find more in the docs: https://www.bryntum.com/docs/scheduler-pro/#Grid/feature/Group

Pavlo Miklashevych
Sr. Frontend Developer


Post by eugenem »

I think there is some confusion. Your sample has a single list of resources of different types. But my data structure has 2 types of resources, and they have the same ids.

Machines: [ { 1, "Assembly station" }, { 2, "Workstation" } ]
Operators: [ { 1, "Celia" }, { 2, "George" } ]

My events look like:

{ start_time: "2021-01-01 10:00", end_time: "2021-01-01 11:00", machine: 1, operators: [ 1, 2 ], text: "blabla" }

I've tried to simply merge my resources into 1 list with different types, but calendar crashed as they had same ids:

Resources: [
{ 1, "Machine", "Assembly station" }
{ 2, "Machine", "Workstation" }
{ 1, "Operator", "Celia" }
{ 2, "Operator", "George" }
]

So I'm not sure how do you propose to solve this issue...


Post by pmiklashevich »

Yes, sorry. I was confused by the event editor configuration you requested. Now, when you revealed the data structure, it's clear what you're trying to achieve. Unfortunately, Scheduler does not support having multiple resource stores. So you need to merge the data of 2 sources in one resource store. You can create simple AjaxStore for each entity: machines and operators. Load the data and then merge it in one dataset by mapping the IDs to a prefix + id. For example:

// Machine "{ id : 1, name : 'Assembly station' }" to "{ id : 'M_1', type : 'machines', name : 'Assembly station' }" 
// Operator "{ id : 1, name : 'Celia' }" to "{ id : 'O_1', type : 'operators', name : 'Celia' }"

Since you need multi assignment, you need to create an AssignmentStore and describe connections between events and resources (mapped machines and mapped operators).

Here is a small example showing the combination of machines and operators, and customized event editor which has 2 different pickers for machines and resources. I made machines with single select assuming that one machine can work for a task, and I made operators with multi select assuming more than one specialist can operate one machine. But that could be easily changed. I made it this way to show you different options. Please try the following code in "scheduler/examples/multiassign/" demo locally (Scheduler/examples/multiassign/app.js):

import '../_shared/shared.js'; // not required, our example styling etc.
import Scheduler from '../../lib/Scheduler/view/Scheduler.js';
import ResourceStore from '../../lib/Scheduler/data/ResourceStore.js';
import '../../lib/Scheduler/column/ResourceInfoColumn.js';

//region Data

const
    machines    = [
        { id : 1, name : 'Assembly station' },
        { id : 2, name : 'Workstation' }
    ],
    operators   = [
        { id : 1, name : 'Celia' },
        { id : 2, name : 'Lee' },
        { id : 3, name : 'Macy' }
    ],
    resources   = [
        ...machines.map(m => ({ ...m, id : 'M_' + m.id, type : 'machines' })),
        ...operators.map(m => ({ ...m, id : 'O_' + m.id, type : 'operators' }))
    ],
    events      = [
        {
            id        : 1,
            startDate : new Date(2017, 0, 1, 10),
            endDate   : new Date(2017, 0, 1, 12),
            name      : 'Multi operators',
            iconCls   : 'b-fa b-fa-users'
        },
        {
            id         : 2,
            startDate  : new Date(2017, 0, 1, 13),
            endDate    : new Date(2017, 0, 1, 15),
            name       : 'Single operator',
            iconCls    : 'b-fa b-fa-user',
            eventColor : 'indigo'
        }
    ],
    assignments = [
        { id : 1, resourceId : 'O_1', eventId : 1 },
        { id : 2, resourceId : 'M_1', eventId : 1 },
        { id : 3, resourceId : 'O_2', eventId : 1 },
        { id : 4, resourceId : 'O_8', eventId : 1 },
        { id : 5, resourceId : 'O_3', eventId : 2 },
        { id : 6, resourceId : 'M_2', eventId : 2 }
    ];

const resourceStore = new ResourceStore({
    data : resources
});

const machineStore = resourceStore.makeChained(
    record => !record.isSpecialRow && record.type === 'machines',
    null,
    {
        // Need to show all records in the combo. Required in case resource store is a tree.
        excludeCollapsedRecords : false
    }
);

const operatorStore = resourceStore.makeChained(
    record => !record.isSpecialRow && record.type === 'operators',
    null,
    {
        // Need to show all records in the combo. Required in case resource store is a tree.
        excludeCollapsedRecords : false
    }
);

//endregion

const scheduler = new Scheduler({
    features : {
        group : 'type',

    eventEdit : {
        editorConfig : {
            align : 't-b'
        },
        items : {
            resourceField : false,

            machines : {
                label                   : 'Machines',
                type                    : 'combo',
                store                   : machineStore,
                name                    : 'machines',
                valueField              : 'id',
                displayField            : 'name',
                multiSelect             : false,
                weight                  : 210,
                highlightExternalChange : false
            },

            operators : {
                label                   : 'Operators',
                type                    : 'combo',
                store                   : operatorStore,
                name                    : 'operators',
                valueField              : 'id',
                displayField            : 'name',
                multiSelect             : true,
                weight                  : 220,
                highlightExternalChange : false
            }
        }
    }
},

listeners : {
    // load values to the machines and to the operators manually
    beforeEventEditShow({ eventRecord, editor }) {
        const { machines, operators } = editor.widgetMap;

        machines.value = eventRecord.resources.filter(r => r.type === 'machines');
        operators.value = eventRecord.resources.filter(r => r.type === 'operators');
    },
    // save values manually
    beforeEventSave({ eventRecord, values, resourceRecords }) {
        const selectedMachines = [values.machines]; // single select
        const selectedOperators = values.operators; // multi select
        const selectedResourceIds = [...selectedMachines, ...selectedOperators];

        // Assigning resources to event does not work here, because event editor reassigns the resources internally.
        // Therefore mutate the resourceRecords param which is used internally.
        const selectedResources = eventRecord.resourceStore.query(record => selectedResourceIds.includes(record.id), true);
        resourceRecords.splice(0, resourceRecords.length, ...selectedResources);
    }
},

appendTo  : 'container',
minHeight : '20em',

startDate         : new Date(2017, 0, 1, 6),
endDate           : new Date(2017, 0, 1, 20),
viewPreset        : 'hourAndDay',
eventStyle        : 'border',
resourceImagePath : '../_shared/images/users/',

columns : [
    { type : 'resourceInfo', text : 'Name', field : 'name', width : 130 }
],

resourceStore,
events,
assignments
});

Here is the result:

Снимок экрана 2021-01-20 в 12.55.57.png
Снимок экрана 2021-01-20 в 12.55.57.png (347.01 KiB) Viewed 1402 times

Please let me know if this solves your issue. If you have any further questions, fell free to ask and we will try to help you.

Best wishes,
Pavel

Pavlo Miklashevych
Sr. Frontend Developer


Post by eugenem »

I've got some progress. Note that I'm using Angular + CRUD, so it's a bit more tricky.

So now I get the grid with grouped resources, which is cool. But when I try to save a new event, it fails here, as eventRecord.resourceStore = null:

      const selectedResources = eventRecord.resourceStore.query(record => selectedResourceIds.includes(record.id), true);

There is my template:

          <bry-scheduler #scheduler2
            [height]="height - 100"
            [columns]="schedulerConfig.columns"
            [timeRangesFeature] = "schedulerConfig.features.timeRanges"
            [eventEditFeature]  = "schedulerConfig.eventEditFeature"
            [crudManager]       = "schedulerConfig.crudManager"
            [eventStyle]        = "schedulerConfig.eventStyle"
            [viewPreset] ="schedulerConfig.viewPreset"
          ></bry-scheduler>

and there is code:

  resourceStore: any = {};
  facilityStore: any = {};
  staffStore: any = {};

  schedulerConfig = {
    crudManager: {
      resourceStore : {
        // Add some custom fields
        groupers: [
          // type could be 'Machines', 'Operators', etc
          { field: 'type', ascending: false }
        ]
      },
      eventStore: {
        // Add a custom field and redefine durationUnit to default to hours
        //fields: ['dt', { name: 'durationUnit', defaultValue: 'hour' }]
        fields: ['companyId']
    },
      autoLoad: true,
      transport: {
        load: {
          url: '/api/Scheduler/GetB',
          headers: {
            'Authorization': `Bearer `
          },
        }
      }
    },

resourceImagePath: '../_shared/images/users/',
appendTo: 'container',
eventStyle: 'rounded',
startDate: '2020-03-23',
endDate: '2020-03-26',
// Custom view preset, with more compact display of hours
viewPreset: {
  base: 'hourAndDay',
  tickWidth: 35,
  headers: [
    {
      unit: 'day',
      dateFormat: 'ddd DD/MM' //Mon 01/10
    },
    {
      unit: 'hour',
      dateFormat: 'H'
    }
  ]
},

features: {
  timeRanges: {
    narrowThreshold: 10,
    enableResizing: true
  },
  resourceNonWorkingTime: true,
  cellEdit: true,
  filter: true,
  regionResize: true,
  dependencies: true,
  dependencyEdit: true,
  percentBar: true,
  group: 'type',
  sort: 'name',
  eventTooltip: {
    header: {
      title: 'Information',
      titleAlign: 'start'
    },
    tools: [
      {
        cls: 'b-fa b-fa-trash',
        handler: function () {
          this.eventRecord.remove();
          this.hide();
        }
      },
      {
        cls: 'b-fa b-fa-edit',
        handler: function () {
          this.schedulerPro.editEvent(this.eventRecord);
        }
      }
    ]
  }
},

eventEditFeature: {
  // Add extra widgets to the event editor
  items: {
    resourceField: false,

    facilities: {
      label: 'Facilities',
      type: 'combo',
      store: this.facilityStore,
      name: 'facilities',
      valueField: 'id',
      displayField: 'name',
      multiSelect: false,
      weight: 210,
      highlightExternalChange: false
    },

    staff: {
      label: 'Staff',
      type: 'combo',
      store: this.staffStore,
      name: 'staff',
      valueField: 'id',
      displayField: 'name',
      multiSelect: true,
      weight: 220,
      highlightExternalChange: false
    }
  }
},

columns: [
  {
    type: 'resourceInfo',
    text: 'Name',
    showEventCount: true,
    width: 220,
    validNames: null
  },
  {
    type: 'action',
    text: 'Actions',
    width: 80,
    align: 'center',
    actions: [{
      cls: 'b-fa b-fa-fw b-fa-plus',
      tooltip: 'Add task',
      onClick: async ({ record }) => {
        await this.schedulerPro.schedulerInstance.project.eventStore.add({
          name: 'New task',
          startDate: this.schedulerPro.startDate,
          duration: 4,
          durationUnit: 'h',
          resourceId: record.id
        });

        this.schedulerPro.schedulerInstance.editEvent(this.schedulerPro.schedulerInstance.project.eventStore.last as EventModel);
      }
    }, {
      cls: 'b-fa b-fa-fw b-fa-copy',
      tooltip: 'Duplicate resource',
      onClick: ({ record }) => {
        this.schedulerPro.schedulerInstance.resourceStore.add(record.copy({
          name: record.name + ' (copy)'
        }));
      }
    }]
  }
],
  };

  constructor(
    private backend: BackendService,
    private console: ConsoleLoggerService,
    private auth: AuthenticationService
  ) {
    const token = this.auth.currentUserValue.token;

this.schedulerConfig.crudManager.transport.load.headers.Authorization = 'Bearer ' + token;
  }

  async ngOnInit() {
    if (!this.immLookups) this.immLookups = await this.backend.loadImmutableLookupsAsync();

this.resourceStore = new ResourceStore({
  data: [
    ...this.immLookups.testFacilities.map(m => ({ ...m, id: 'F_' + m.id, type: 'facilities' })),
    ...this.immLookups.staff.map(m => ({ ...m, id: 'S_' + m.id, type: 'staff' }))
  ],
});

this.schedulerConfig.eventEditFeature.items.facilities.store = this.resourceStore.makeChained(
  record => !record.isSpecialRow && record.type === 'facilities',
  null,
  {
    // Need to show all records in the combo. Required in case resource store is a tree.
    excludeCollapsedRecords: false
  }
);

this.schedulerConfig.eventEditFeature.items.staff.store = this.resourceStore.makeChained(
  record => !record.isSpecialRow && record.type === 'staff',
  null,
  {
    // Need to show all records in the combo. Required in case resource store is a tree.
    excludeCollapsedRecords: false
  }
);


  }

  ngAfterViewInit() {
    // install beforeEventEdit listener
    this.schedulerPro.schedulerInstance.addListener('beforeEventEditShow', ({ eventRecord, editor }) => {
      const { facilities, staff } = editor.widgetMap;

  facilities.value = eventRecord.resources.filter(r => r.type === 'facilities');
  staff.value = eventRecord.resources.filter(r => r.type === 'staff');
});

this.schedulerPro.schedulerInstance.addListener('beforeEventSave', ({ eventRecord, values, resourceRecords }) => {
  const selectedFacilities = [values.facilities]; // single select
  const selectedStaff = values.staff; // multi select
  const selectedResourceIds = [...selectedFacilities, ...selectedStaff];

  // Assigning resources to event does not work here, because event editor reassigns the resources internally.
  // Therefore mutate the resourceRecords param which is used internally.
  const selectedResources = eventRecord.resourceStore.query(record => selectedResourceIds.includes(record.id), true);
  resourceRecords.splice(0, resourceRecords.length, ...selectedResources);
});
  }


Post by eugenem »


Post by pmiklashevich »

Indeed, I didn't take into account drag creating events. Please try out this code:

    listeners : {
        // load values to the machines and to the operators manually
        beforeEventEditShow({ eventRecord, resourceRecord, editor }) {
            const { machines, operators } = editor.widgetMap;
            // if there is no resources assigned to event fall back to the passed resource which is set on drag create
            const resources = eventRecord.resources?.length ? eventRecord.resources : (resourceRecord ? [resourceRecord] : []);

        machines.value = resources.filter(r => r.type === 'machines');
        operators.value = resources.filter(r => r.type === 'operators');
    },
    // save values manually
    beforeEventSave({ source : scheduler, eventRecord, values, resourceRecords }) {
        const selectedMachines = [values.machines]; // single select
        const selectedOperators = values.operators; // multi select
        const selectedResourceIds = [...selectedMachines, ...selectedOperators];

        // Assigning resources to event does not work here, because event editor reassigns the resources internally.
        // Therefore mutate the resourceRecords param which is used internally.
        const selectedResources = scheduler.resourceStore.query(record => selectedResourceIds.includes(record.id), true);
        resourceRecords.splice(0, resourceRecords.length, ...selectedResources);
    }
},

Pavlo Miklashevych
Sr. Frontend Developer


Post by eugenem »

actually at beforeEventEditShow it was fine (eventRecord.resources exists)
but at beforeEventSave, eventRecord.resourceStore is undefined


Post by eugenem »

I start without events at all. And click on empty space on the scheduler to create a new event.


Post by pmiklashevich »

Just change eventRecord.resourceStore to scheduler.resourceStore

Pavlo Miklashevych
Sr. Frontend Developer


Post by eugenem »

aha, it's working!

now, how can I catch the resource at which a new event was created, so it's automatically populated? Like it's taking time range automatically...


Post Reply