Shift Job Boards

From 3B Knowledge
Jump to navigation Jump to search

Introduction

Shift job boards is a functionality that allows candidates to search for open (requirement) shifts and either claim them or express interest in working for these shifts. This functionality ensures speedy shift fill rates and is an alternative to shift invitations where the scheduling user needs to first match & invite candidates.

We utilize the Matching and Compliance Engine in identifying suitable shifts for a candidate.

Mobile App: Shifts Job Board

Availability

The feature is available from version 2.3+

Configuration

Similar to the internal matching engine, the externally facing matching engine requires modifications to the Component Definition and Loaders custom metadata records.

Component Configuration

The "Clock In / Out Component" metadata record type should be modified to include shift job board specific configuration. Below is a fingerprint of the new options available for the Shift and Invitation schedulables:

{
    "schedulables": {
        ...,
        "shift": {
            ...
            "claimOpenShiftLabel": "Claim Shift",
            "additionalFields": [
                 {
                     "field": "b3s__Job__r.b3o__Client_Account__c"
                 },
                 {
                     "field": "b3s__Job__r.b3o__Job_Type__c"
                 },
                 {
                     "field": "b3s__Site__r.b3o__Coordinates__Latitude__s"
                 },
                 {
                     "field": "b3s__Site__r.b3o__Coordinates__Longitude__s"
                 }
             ],
             "matchSelectionClause": [
                "Id NOT IN (SELECT b3s__Shift__c FROM b3s__Invitation__c WHERE b3s__Contact__c = :contactUserId) AND b3s__Contact__c = null AND b3s__Status__c = 'Confirmed'"
             ],
             "contactsLoader": "contacts",
             "matchingLoaders": [
                "contacts",
                "suitability",
                "certificates",
                "certRequirements"
            ],
        }
        "invitation":{
             ...
             "statusFieldPath":"b3s__Status__c",
             "contactFieldPath": "b3s__Contact__c",
             "shiftFieldPath": "b3s__Shift__c",
             "claimOpenShiftDefaultStatus": "Accepted",
             "seenTimestampFieldPath":"b3s__Seen_Timestamp__c",
             ...
        }
    }
}

Shift:

  • claimOpenShiftLabel - This option overrides the label of the button that allows the candidate to "claim" the shift
  • additionalFields - Additional field paths from the shift to be loaded. These are needed so your compliance rules can access additional data
  • matchSelectionClause - One or multiple SOQL statements that allows you to narrow down which shifts are available for the shift job board matching functionality. Good practice is to utilize the Status field as shifts often need some sort of confirmation before they can be accessed/booked. In addition, in the example above, there is a sub-query that ensures that the candidate won't see shifts that they have already expressed interest for. Finally the Contact = null statement ensures that only requirements are available for matching
  • contactsLoader - this is the name of the loader, responsible for loading the contextual contact (user) and any other contacts in the mobile app. Ensure that the loader includes the context user
  • matchingLoaders - a list of names of loaders to be passed to the matching engine. Which loaders are required is governed by the matching rules.

Invitation:

  • statusFieldPath - the path to the Status field on the invitation
  • contactFieldPath - the path to the Contact reference field on the invitation
  • shiftFieldPath - the path to the Shift reference field on the invitation
  • claimOpenShiftDefaultStatus - this is the status that will be assigned to the newly created invitation once a candidate claims a shift

Scheduling Compliance Rule

A new field "Type" is added to the Scheduling Compliance Rule record (make sure to add it to the layout if upgrading from a version < 2.3). The Type must be assigned as " Clock In / Out Component" to any rules that are added specifically for the job board functionality.

Here's a sample rule that matches candidate's certificates to certificate requirements:

async ({ event, events, contactId, loaders, EvaluationResult }) => {
    if (!contactId) {
        console.warn('No contact id for evaluation');
        return [];
    }

    const certificatesLoader = loaders.find((l) => l.name === 'certificates');
    const contactCertificates = certificatesLoader?.loader?.grouped[contactId];

    console.log('App: Qualifications Check - contactCertificates', contactCertificates)

    const certRequirementsLoader = loaders.find((l) => l.name === 'certRequirements');
    const certRequirements = certRequirementsLoader?.loader.records;

    const eventAccountId = event.extendedProps?.b3s__Job__r?.b3o__Client_Account__c;
    const eventJobTypeId = event.extendedProps?.b3s__Job__r?.b3o__Job_Type__c;
    const eventSiteId = event.extendedProps?.b3s__Site__c;

    console.log('App: Qualifications Check - eventAccountId', eventAccountId)
    console.log('App: Qualifications Check - eventJobTypeId', eventJobTypeId)
    console.log('App: Qualifications Check - eventSiteId', eventSiteId)

    const accountCertRequirements = certRequirements.filter(
        (req) => req.b3s__Account__c != null && req.b3s__Account__c === eventAccountId,
    );
    const jobTypeCertRequirements = certRequirements.filter(
        (req) => req.b3s__Job_Type__c != null && req.b3s__Job_Type__c === eventJobTypeId,
    );
    const siteCertRequirements = certRequirements.filter(
        (req) => req.b3s__Site__c != null && req.b3s__Site__c === eventSiteId,
    );

    console.log('App: Qualifications Check - accountCertRequirements', accountCertRequirements)
    console.log('App: Qualifications Check - jobTypeCertRequirements', jobTypeCertRequirements)
    console.log('App: Qualifications Check - siteCertRequirements', siteCertRequirements)


    const getMissingCertificates = function (certificates = [], certRequirements = []) {
        const contactCertificateTypes = new Set(certificates.map((item) => item.b3o__Certificate_Type__c));
        const missingCerts = certRequirements.filter(
            (certReq) => !contactCertificateTypes.has(certReq.b3s__Certificate_Type__c),
        );

        return missingCerts;
    };

    const getValidCertificates = function (certificates = [], certRequirements = []) {
        const contactCertificateTypes = new Set(certificates.map((item) => item.b3o__Certificate_Type__c));
        const missingCerts = certRequirements.filter((certReq) =>
            contactCertificateTypes.has(certReq.b3s__Certificate_Type__c),
        );

        return missingCerts;
    };

    const missingCerts = getMissingCertificates(contactCertificates, [...accountCertRequirements, ...jobTypeCertRequirements, ...siteCertRequirements]);
    let results = [];
    for (const missingCert of missingCerts) {
        results.push(
            new EvaluationResult({
                event: event,
                detail: `👎 Missing ${missingCert.b3s__Certificate_Type__r.Name}`,
                category: 'Certificates',
                points: -50,
                isBlocking: true
            }),
        );
    }

    const validCerts = getValidCertificates(contactCertificates, [...accountCertRequirements, ...jobTypeCertRequirements, ...siteCertRequirements]);
    for (const validCert of validCerts) {
        results.push(
            new EvaluationResult({
                event: event,
                detail: `👍 Has ${validCert.b3s__Certificate_Type__r.Name}`,
                category: 'Certificates',
                points: 25,
                isBlocking: false
            }),
        );
    }
    return results;
}

Note: the EvaluationResult object now has a new property: "isBlocking". It defaults to false if not set. If you set it to true, then the shift will automatically be hidden from the match results. This is used to ensure that only fully compliant candidates & shifts can be matched. In the above example, if a candidate is missing a required certificate, the shift will automatically be removed as a potential shift.

Please reach out to the support team for help creating compliance rules.

Component Loader

Similar to internal matching loaders, you can instruct the app to load auxiliary data to be used in the matching operation. Here's an example of loading candidate certificates:

{
    name: "certificates",
    objectName: "b3o__Certificate__c",
    filterRecordsBy: function () {
        return "b3o__Contact__c = {1} AND b3o__Revoke_Certificate__c = false"
    },
    groupRecordsBy: "b3o__Contact__c",
    fieldToLoad: ['b3o__Certificate_Type__c', 'b3o__Contact__c', 'b3o__Start_Date__c', 'b3o__End_Date__c', 'b3o__Revoke_Certificate__c'],
    filteringItems: function () {
        return [
            `'${contextRecordId}'`,
            `'${contactUserId}'`
        ]
    }
}

Usage

There are two potential workflows that can be enabled:

Scheduler Initiated

A scheduler creates shifts as requirements and changes their status to one that will "publish" the shifts to mobile app users. Mobile app users can then check per day what shifts match their profile and "claim" these shifts.

When a candidate claims a shift, a Shift Invitation record is created against the shift. The default status is controlled by the claimOpenShiftDefaultStatus property on the invitation schedulable. Through Flows/Apex code, professional services can implement a F3 process, where the first candidate to "claim" the shift will get auto-booked for that shift. By default, however, the claiming of the shift will follow a Manager's Approval process whereby the shift will remain open until a scheduler manually books the candidate (through the Match option in the context menu from the schedule or through some other automation).

Candidate Shift Swapping

It is possible to configure the job boards functionality in a way that allows candidates to swap shifts. A candidate would need to change the shift status to something like "Swap" and through a customization in the matchSelectionClause on the shift schedulable, you can enable this shift for searching by other candidates

The Important Items

The matching engine relies on 3 things:

  • Matching Rules - configured through the Scheduling Compliance Rules, you can define job board specific matching rules, different than the internal matching rules
  • Loaders - additional data to be loaded for the compliance rules to utilize in the matching algorythms
  • Potential Shifts - identify shifts that will be evaluated in the matching operation, done through a restrictive SOQL query through the property matchSelectionClause on the shift schedulable

Tips

You can add Flow Errors to prevent candidates from "claiming" shifts. This can be done for example to prevent double booking or by executing Flow based matching rules. Simply add an error validation through a Flow or validation rule and the mobile app will reflect the message you have added to the error in the system.