Booking with diary

Hi,
I’m working with the Booking Object using a Diary + calendar setup, specifically using the Start field (booking time picker / timestamp field) with a fixed appointment duration of 1 hour.

I also have a Resource object (Mechanics), where I’m using the resource availability field to define working hours (e.g. Monday–Friday availability) of each mechanic.

I’m trying to understand the best (supported) way to customise the date selection behaviour, and would appreciate any guidance regarding the below:
I want to restrict the date range that users can select in the Start field, for example:

  1. Restrict booking dates to a fixed window (e.g. today + 30 days only)
  2. Prevent users from selecting dates outside this range
  3. Ensure the existing Diary-based slot generation logic remains unchanged
  4. Respect resource availability rules (Mon–Fri working hours per mechanic) - which it does because of the resource availability field, but unable to control this with absences integrated
  5. Support resource absence logic (e.g. holidays / leave), so mechanics are not assigned bookings on unavailable dates
  6. Ensure bookings are never allocated to unavailable resources automatically

Objects:
BKL-BOO : Booking [PROCESS] 1-1 CASE
BKL-BOO : Booking [DATA] 1-1 CASE
Booking type object:
BKL-MOT : Appointment 1-1 CASE
BKL-MOT : Appointment M-1 BKL-MOT : Resource (Mechanic)

BKL-MOT : Resource (Mechanic) 1-M BKL-MOT : Resource Absence (Using the relation selector to add an absence start and end date)

I have attempted to use a Resource subset constraint, however this appears to only support static filtering (e.g. user role = Admin), and does not support time-based logic such as:

  • Appointment Start Date ≥ Absence Start Date

  • Appointment End Date ≤ Absence End Date

Screenshots:

Example (resource availability set on mechanic):

Example (resource absence):

Has anyone implemented mechanic/resource holidays or absences with the Booking Object and Diary? If so, did you use a built-in diary feature, an AppShare accelerator, or a custom solution to achieve this?

Many thanks,

Amara

I think I have found the solution to this. It would be to use resource availability to define each mechanic’s normal working hours + a record selector.

For example:

Mechanic Availability

  • Object: BKL-MOT Resource (Mechanic)

  • Availability: Monday–Friday, 08:00–17:00

Then create a 1-to-many relationship between BKL-MOT Resource (Mechanic) and BKL-MOT Resource Absence. This will allow each mechanic resource to have multiple absence records stored against them.

Mechanic Absence

  • Object: BKL-MOT : Resource Absence

  • Absence Start Date: 10 June 2026

  • Absence End Date: 10 June 2026

When a user attempts to create a booking through the form, the booking type object and diary functionality are used. The diary contains a Code API Record Selector, which determines which mechanic is available based on existing appointments (bookings) and any records stored within the resource absence object.

The logic within the API Record Selector is:

  1. Extract the requested booking date and time.

  2. Retrieve all mechanic resources.

  3. Check each mechanic’s resource availability.

  4. Check each mechanic’s absence records for conflicts.

  5. Remove any mechanics who are unavailable.

  6. Return a randomly selected mechanic from the remaining available resources for assignment.

// RECORD SELECTOR CODE
return {
    main: function (params) {

        cs.log("========== START RECORD SELECTOR ==========");

        function toMs(val) {
            if (!val) return null;

            let num = Number(val);
            if (!isNaN(num)) {
                return num < 99999999999 ? num * 1000 : num;
            }

            let parsed = Date.parse(String(val).replace(" ", "T"));
            return isNaN(parsed) ? null : parsed;
        }

        cs.log("PARAMS = " + JSON.stringify(params));

        let currentBookingId = params?.booking_data?.id || null;

        let start = toMs(params?.booking_data?.start_stamp);
        let end = toMs(params?.booking_data?.end_stamp);

        // Current Unix timestamp (seconds)
        let now = Math.floor(Date.now() / 1000);

        cs.log("REQUEST START = " + start + " " + new Date(start).toISOString());
        cs.log("REQUEST END   = " + end + " " + new Date(end).toISOString());
        cs.log("CURRENT BOOKING ID = " + currentBookingId);
        cs.log("CURRENT UNIX TIME = " + now);

        if (!start || !end) {
            cs.log("Missing booking times.");
            return null;
        }

        let candidates = params?.candidates || [];

        cs.log("MECHANICS = " + JSON.stringify(candidates));

        if (!candidates.length) {
            cs.log("No diary mechanics.");
            return null;
        }

        let bookings = cs.search({
            base_object_id: cs.ref('bkl_mot_appointments_booking_object'),
            selects: {
                id: ':id',
                start: cs.ref('bkl_mot_appointments_booking_start'),
                end: cs.ref('bkl_mot_appointments_booking_end'),
                resource: cs.ref('bkl_mot_appointments_booking_resource'),
                status: cs.ref('bkl_mot_appointments_booking_reservation_status'),
                reservationtimeout: cs.ref('bkl_mot_appointments_booking_reservation_timeout')
            },
            return: 'data'
        });

        cs.log("BOOKINGS FOUND = " + bookings.length);

        let absences = cs.search({
            base_object_id: cs.ref('bkl_mechanic_absence_object'),
            selects: {
                id: ':id',
                mechanic: cs.ref('bkl_mech_absence_mechanic'),
                start: cs.ref('bkl_mech_absence_start'),
                end: cs.ref('bkl_mech_absence_end')
            },
            return: 'data'
        });

        cs.log("ABSENCES FOUND = " + absences.length);

        let available = [];

        for (let mechId of candidates.map(String)) {

            cs.log("------------------------------------");
            cs.log("CHECKING MECHANIC " + mechId);

            let blocked = false;

            // =========================
            // BOOKINGS
            // =========================
            for (let booking of bookings) {

                // Ignore current booking if editing
                if (currentBookingId && String(booking.id) === String(currentBookingId)) {
                    continue;
                }

                let resourceId = booking.resource?.id
                    ? String(booking.resource.id)
                    : String(booking.resource);

                if (resourceId !== mechId) {
                    continue;
                }

                let status = String(booking.status || "").toLowerCase();

                // Only secured and provisional bookings matter
                if (status !== "secured" && status !== "provisional") {
                    cs.log("Ignoring booking " + booking.id + " status=" + status);
                    continue;
                }

                // Ignore expired provisional bookings
                if (status === "provisional") {

                    let timeout = Number(booking.reservationtimeout || 0);

                    if (timeout <= now) {
                        cs.log(
                            "Ignoring expired provisional booking " +
                            booking.id +
                            " timeout=" + timeout +
                            " now=" + now
                        );
                        continue;
                    }

                    cs.log(
                        "Active provisional booking " +
                        booking.id +
                        " timeout=" + timeout
                    );
                }

                let bStart = toMs(booking.start);
                let bEnd = toMs(booking.end);

                if (!bStart || !bEnd) {
                    continue;
                }

                cs.log(
                    "COMPARE REQUEST [" +
                    new Date(start).toISOString() +
                    " - " +
                    new Date(end).toISOString() +
                    "] BOOKING [" +
                    new Date(bStart).toISOString() +
                    " - " +
                    new Date(bEnd).toISOString() +
                    "]"
                );

                // Strict overlap
                if (bStart < end && bEnd > start) {

                    cs.log(
                        "BLOCKED BY " +
                        status.toUpperCase() +
                        " BOOKING " +
                        booking.id
                    );

                    blocked = true;
                    break;
                }
            }

            if (blocked) {
                continue;
            }

            // =========================
            // ABSENCES
            // =========================
            for (let absence of absences) {

                let mechanicId = absence.mechanic?.id
                    ? String(absence.mechanic.id)
                    : String(absence.mechanic);

                if (mechanicId !== mechId) {
                    continue;
                }

                let aStart = toMs(absence.start);
                let aEnd = toMs(absence.end);

                if (!aStart || !aEnd) {
                    continue;
                }

                cs.log(
                    "ABSENCE " +
                    absence.id +
                    " [" +
                    new Date(aStart).toISOString() +
                    " - " +
                    new Date(aEnd).toISOString() +
                    "]"
                );

                if (aStart < end && aEnd > start) {

                    cs.log("BLOCKED BY ABSENCE " + absence.id);

                    blocked = true;
                    break;
                }
            }

            if (!blocked) {
                cs.log("AVAILABLE -> " + mechId);
                available.push(mechId);
            }
        }

        cs.log("AVAILABLE = " + JSON.stringify(available));

        if (!available.length) {
            cs.log("NO AVAILABLE MECHANICS");
            return null;
        }

        cs.log("SELECTED = " + available[0]);

        return available[0];
    }
};

At the moment, the logic only checks the absence date. I am going to investigate adding time-based checks against the absence object as well, as a mechanic may not necessarily be unavailable for a full day. For example, a mechanic could have a partial-day absence where they are unavailable only between specific start and end times.

Issue to resolve:
The remaining challenge is handling scenarios where no mechanic resource is available. Currently, the user can select a time slot from the diary dropdown, but when they proceed to the next step of the form, they receive the system message:

“BKL-CRS1185 not saved - No resources available on selected time slot.”

The question is whether this message can be customised or whether the unavailable time slots can be removed from the dropdown before the user selects them.

I have looked at using cs.alert, but this does not appear to work when called from within the Record Selector.

Thanks,

Amara