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
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:
-
Extract the requested booking date and time.
-
Retrieve all mechanic resources.
-
Check each mechanic’s resource availability.
-
Check each mechanic’s absence records for conflicts.
-
Remove any mechanics who are unavailable.
-
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