Smart Life US

← All posts · 2026-08-16 · Excel & Automation

Google Sheets dock door auto assignment with Apps Script – Booking 5

Google Sheets dock door auto assignment with Apps Script – Booking 5

Introduction – inbound bookings, door assignment is always a pain

When you use Google Sheets to take inbound appointments, there’s always one manual task left at the end: someone has to look at each booking and decide which door to put it on. If you’re manually choosing doors based on appointment time, equipment type, and priority, it’s very easy during busy hours to overload D01 or accidentally assign two loads to the same door at the same time. Out on the warehouse floor, “we took the appointments fine, but the door schedule got tangled” is something that can easily throw off the whole day.

Dock door auto-assignment flow

This post covers a Google Sheets dock door auto-assignment method to reduce that kind of problem. We’ll build an Apps Script that automatically assigns inbound bookings to doors in Google Sheets, and then wire it cleanly into the existing booking-save flow, step by step.

This is part 5 of the “Build a warehouse booking system in Google Sheets – booking module” series. In Prevent double-booking in Google Sheets inbound appointments with LockService: save safely and earlier, we implemented business hours & holidays, valid booking window calculation, capacity per time slot, input validation and saving, and overbooking prevention. Now on top of that, we’ll add an Apps Script dock door auto-allocation feature so that “even if multiple bookings come in for the same time, they get attached to D01, D02, D03… in order without overlaps.”


Designing auto door assignment – on what basis do we pick a door?

The core idea for auto-assigning dock doors in Google Sheets is simple. First, for the booking you’re trying to make, you look at date, time slot, and equipment type and compute which doors are already in use in that slot. Then you take your full door list, subtract the doors already in use, and from the remaining doors you assign the lowest-numbered door first.

From actual DC operations, assigning doors in random order is less helpful than just filling them in numeric order (e.g., D01→D02→D03). It’s better for travel paths and communication with the floor. It’s also easier for the ops team to tell at a glance which time blocks are heavily loaded on which stretch of doors, and to use that for next-day staffing.

The flow we’ll implement here looks like this:

  • BOOK_getDoorUsageForSlotCore_() calculates the list of doors already occupied for a given date, time, and equipment type.
  • autoAssignDoor_() uses that info to pick the lowest-numbered door from the list of available doors.
  • In the existing booking-save function BOOK_bookAppt(), if the door field is empty we call autoAssignDoor_() to try an auto-assignment. On success column D gets the door; on failure the row is saved with column D left blank.
  • After saving, notifyBookingConfirmed_() sends a confirmation email including the booking details and door assignment.

From experience, if all doors are full, it’s more realistic to save the booking in a “door pending” status rather than reject it outright, and let a supervisor tidy things up manually before the shift starts or during a slow time. The auto-assignment logic here is built with that in mind.


Door usage calculation function – implementing BOOK_getDoorUsageForSlotCore_

The first capability we need for auto door assignment is to accurately count “which doors are already in use in this time slot.” To do this, we’ll build a core function called BOOK_getDoorUsageForSlotCore_(). It uses the same main booking sheet APPT_MAIN from earlier parts, filters rows by matching date, time, and equipment type, and then aggregates the door values from those rows.

In practice, when you test this, three mistakes show up the most. First, selecting the wrong column range and ending up reading the wrong values. Second, if the door column is formatted as a number, you get numeric types instead of strings and some doors silently get skipped when checking usage. Third, typos or blanks in the equipment type column slip through, making a slot appear to have “zero usage” when it actually doesn’t. The code below is designed to guard against all three.

From here on we’ll write real Apps Script. We assume you already have helper functions like APPT_ymd_() and APPT_hm_() from earlier parts, which produce string keys for dates and times. If you’re only using this post on its own, you’ll need to create functions with the same interface yourself.

Step 1 — declare config constants for door usage calculation

  • What it does: centralizes the sheet name and column positions needed to compute door usage.
  • Where: at the very top of Code.gs, right under your existing BOOK_ constants.
  • After pasting: Save (⌘S/Ctrl+S) and make sure there are no errors.
Apps Script (JavaScript)
// → Config constants dedicated to auto door assignment
// These indices must mean **exactly what part 4's `BOOK_APPT_COLS` fixed**:
// A date | B start time | C equipment type | D door | E container | F carrier | G client | H remark
// I end time | J created at | K qty | L pallets | M booking ID - **there is no status column.**
const BOOK_DOOR_CONFIG = {                         // → Settings for this part
  APPT_SHEET_NAME: 'APPT_MAIN',                   // → Main booking sheet name
  COL_DATE: 1,                                    // → Column A: booking date
  COL_TIME: 2,                                    // → Column B: start time
  COL_EQUIP_TYPE: 3,                              // → Column C: equipment type
  COL_DOOR: 4,                                    // → Column D: door code
  HEADER_ROWS: 1                                  // → Number of header rows
};                                                //

How to check it: as long as saving doesn’t complain about a “duplicate identifier,” you’re good. It’s fine if values overlap with other constants as long as the names are unique.

Step 2 — BOOK_getDoorUsageForSlotCore_ and a test helper

  • What it does: computes the list of doors already assigned in a given date/time/equipment-type slot.
  • Where: paste this near the bottom of Code.gs, right after your existing BOOK_ functions.
  • After pasting: run the test helper BOOK_testGetDoorUsageForSlotCore_() and inspect the log.
Apps Script (JavaScript)
// → Calculate which doors are in use for a specific slot
function BOOK_getDoorUsageForSlotCore_(dateObj, timeObj, equipType) {         // → Core calculation
  if (!(dateObj instanceof Date) || isNaN(dateObj)) {                         // → Validate date
    throw new Error('Invalid date');                                          // → Block bad input
  }                                                                           //
  if (!(timeObj instanceof Date) || isNaN(timeObj)) {                         // → Validate time
    throw new Error('Invalid time');                                          // → Block bad input
  }                                                                           //
  if (typeof equipType !== 'string' || !equipType.trim()) {                   // → Validate equipment type
    throw new Error('Equipment type is empty');                               // → Required field
  }                                                                           //

  // Building a second list here creates values part 3's capacity function does not know,
  // and those bookings drop out of the count entirely.
  const allowedEquipTypes = BOOK_CAPACITY_CONFIG.VALID_TYPES;                 // → Part 3's shared list
  const cleanEquipType = equipType.trim().toUpperCase();                      // → Normalize case
  if (!allowedEquipTypes.includes(cleanEquipType)) {                          // → Check whitelist
    throw new Error('Unsupported equipment type: ' + equipType);             // → Block typos/unregistered
  }                                                                           //

  const ss = SpreadsheetApp.getActive();                                      // → Current spreadsheet
  const sheet = ss.getSheetByName(BOOK_DOOR_CONFIG.APPT_SHEET_NAME);          // → Booking sheet
  if (!sheet) {                                                               // → Sheet missing
    throw new Error('APPT_MAIN sheet not found');                             // → Config error
  }                                                                           //

  const lastRow = sheet.getLastRow();                                         // → Last row index
  if (lastRow <= BOOK_DOOR_CONFIG.HEADER_ROWS) {                              // → No data rows
    return {                                                                  // → Return empty result
      dateKey: APPT_ymd_(dateObj),                                           // → Date key
      timeKey: APPT_hm_(timeObj),                                            // → Time key
      equipType: cleanEquipType,                                             // → Equipment type
      usedDoors: []                                                          // → No doors in use
    };                                                                       //
  }                                                                           //

  const startRow = BOOK_DOOR_CONFIG.HEADER_ROWS + 1;                          // → First data row
  const numRows = lastRow - BOOK_DOOR_CONFIG.HEADER_ROWS;                     // → Row count
  const numColumns = BOOK_DOOR_CONFIG.COL_DOOR - BOOK_DOOR_CONFIG.COL_DATE + 1;   // → columns A-D
  const range = sheet.getRange(                                              // → Full data range
    startRow,                                                                 // → Start row
    BOOK_DOOR_CONFIG.COL_DATE,                                                // → Start column
    numRows,                                                                  // → Number of rows
    numColumns                                                                // → Number of columns
  );                                                                          //
  const values = range.getValues();                                           // → 2D array of data

  const targetDateKey = APPT_ymd_(dateObj);                                   // → Date key for comparison
  const targetTimeKey = APPT_hm_(timeObj);                                    // → Time key for comparison

  const usedDoorSet = new Set();                                              // → For deduplication
  const badRows = [];                                                         // → Rows with a broken door code

  for (let i = 0; i < values.length; i++) {                                   // → Iterate rows
    const row = values[i];                                                    //
    const dt = row[BOOK_DOOR_CONFIG.COL_DATE - BOOK_DOOR_CONFIG.COL_DATE];    // → Date value
    const tm = row[BOOK_DOOR_CONFIG.COL_TIME - BOOK_DOOR_CONFIG.COL_DATE];    // → Time value
    const type = row[BOOK_DOOR_CONFIG.COL_EQUIP_TYPE - BOOK_DOOR_CONFIG.COL_DATE]; // → Equipment type
    const doorRaw = row[BOOK_DOOR_CONFIG.COL_DOOR - BOOK_DOOR_CONFIG.COL_DATE];    // → Raw door

    // getLastRow() includes rows whose values were cleared. Feeding those straight into
    // APPT_ymd_ throws and **stops the whole assignment**. Skip fully empty rows; collect
    // partially filled ones so a person can look at them.
    if (!dt && !tm && !type && !doorRaw) {                                     // → Fully empty row
      continue;                                                               //
    }                                                                         //

    let rowDateKey;                                                           // → Row date key
    let rowTimeKey;                                                           // → Row time key
    try {                                                                     // → Read date/time
      rowDateKey = APPT_ymd_(dt);                                            // → 'YYYY-MM-DD'
      rowTimeKey = APPT_hm_(tm);                                             // → 'HH:mm'
    } catch (e) {                                                             // → Unreadable row
      badRows.push(i + BOOK_DOOR_CONFIG.HEADER_ROWS + 1);                     // → Record row number
      continue;                                                               //
    }                                                                         //

    if (rowDateKey !== targetDateKey) {                                       // → Different date
      continue;                                                               //
    }                                                                         //

    const rowType = String(type || '').trim().toUpperCase();                  // → Row equipment type
    if (rowType !== cleanEquipType) {                                         // → Different type
      continue;                                                               //
    }                                                                         //

    if (rowTimeKey !== targetTimeKey) {                                       // → Different time
      continue;                                                               //
    }                                                                         //

    const door = String(doorRaw || '').trim();                                // → Normalize door
    if (!door) {                                                              // → Skip blank door
      continue;                                                               //
    }                                                                         //

    // → Validate door code format (e.g. only accept D01, D02)
    // Silently skipping a malformed door makes it look free and the same door gets
    // assigned twice. Stop and let a person fix the sheet instead.
    const doorPattern = /^D\d{2}$/;                                           // → Door format regex
    if (!doorPattern.test(door)) {                                            // → Non-matching pattern
      badRows.push(i + BOOK_DOOR_CONFIG.HEADER_ROWS + 1);                     // → Record row number
      continue;                                                               // → Don’t treat as in use
    }                                                                         //

    usedDoorSet.add(door);                                                    // → Add to used doors
  }                                                                           //

  if (badRows.length > 0) {                                                   // → Any row we could not read
    throw new Error('Some booking rows could not be read (rows ' +               // → Stop assigning
                    badRows.slice(0, 10).join(', ') +
                    '). Check the date, start time and door code (D01 format), then run again.');
  }                                                                           //

  return {                                                                    // → Return result object
    dateKey: targetDateKey,                                                   // → Date key
    timeKey: targetTimeKey,                                                   // → Time key
    equipType: cleanEquipType,                                                // → Equipment type
    usedDoors: Array.from(usedDoorSet).sort()                                 // → Sorted door list
  };                                                                          //
}                                                                             //

Test helper:

Apps Script (JavaScript)
// → Test door usage calculation
function BOOK_testGetDoorUsageForSlotCore_() {                                // → Test function
  const today = new Date();                                                   // → Today’s date
  const time = new Date();                                                    // → Base for time
  time.setHours(9, 0, 0, 0);                                                  // → Fix at 09:00
  const equipType = 'CONTAINER';                                              // → Sample equipment

  const result = BOOK_getDoorUsageForSlotCore_(today, time, equipType);      // → Run function
  Logger.log(JSON.stringify(result));                                         // → Log result

  if (!Array.isArray(result.usedDoors)) {                                     // → Validate usedDoors type
    throw new Error('usedDoors is not an array');                             // → Fail fast
  }                                                                           //
}                                                                             //

How to check it: in the Apps Script editor, select and run BOOK_testGetDoorUsageForSlotCore_, then look at the execution log for something like {"dateKey":"...","timeKey":"...","usedDoors":[...]}. Pick a specific time slot in your sheet and manually count which doors are used; make sure that matches the output.


Auto door assignment – finding a free door with autoAssignDoor_

Now we’ll build the function that actually decides which door to assign. It will take the door usage result above, filter the active door list, and pick the lowest-numbered door that isn’t already in use.

Here we assume you already have a listActiveDoors_() function from a previous part, which reads from a config sheet and returns only doors marked as “in use (Y/N)”. If you’re only using this article, you’ll need to create a helper that returns an array like ['D01','D02',...] with the same interface.

If there are no doors defined at all, or all of them are used, the auto-assignment function won’t try to force anything. Instead, it returns isPending: true, and the higher-level logic will treat that as “door assignment pending.”

Step 3 — autoAssignDoor_ and its test

  • What it does: takes the active door list, subtracts used doors, and picks the lowest-numbered available door.
  • Where: paste this right under BOOK_getDoorUsageForSlotCore_().
  • After pasting: run BOOK_testAutoAssignDoor_() and verify you get at least one assigned door for a free slot, and pending status when all doors are full.
Apps Script (JavaScript)
// → Automatically choose a free door
function autoAssignDoor_(dateObj, timeObj, equipType) {                       // → Auto door assignment
  const usage = BOOK_getDoorUsageForSlotCore_(dateObj, timeObj, equipType);  // → Get used doors
  const usedDoors = new Set(usage.usedDoors);                                 // → For fast lookups

  const activeDoors = listActiveDoors_();                                     // → List of active doors
  if (!Array.isArray(activeDoors) || activeDoors.length === 0) {             // → No doors configured
    return {                                                                  // → Return pending
      assignedDoor: null,                                                     // → No door
      isPending: true                                                         // → Mark as pending
    };                                                                        //
  }                                                                           //

  const allDoors = activeDoors                                                // → Full door list
    .map(d => String(d || '').trim())                                        // → Normalize strings
    .filter(d => d);                                                          // → Drop blanks

  // Validate the candidate list too. A typo like 'D1' in the DOORS sheet is filtered out
  // of the usage count but survives here - and gets assigned on top of a door already in use.
  const badDoors = allDoors.filter(d => !/^D\d{2}$/.test(d));                 // → Malformed doors
  if (badDoors.length > 0) {                                                  // → Any at all
    throw new Error('The active door list contains malformed values: ' +       // → Stop assigning
                    badDoors.slice(0, 10).join(', ') +
                    '. Fix the DOORS sheet to the D01 format and run again.');
  }                                                                           //

  const available = allDoors                                                  // → Candidate doors
    .filter(d => !usedDoors.has(d))                                           // → Exclude used ones
    .sort();                                                                  // → Sort by code

  if (available.length === 0) {                                               // → No doors left
    return {                                                                  // → Return pending
      assignedDoor: null,                                                     // → No door
      isPending: true                                                         // → Mark as pending
    };                                                                        //
  }                                                                           //

  return {                                                                    // → Return success
    assignedDoor: available[0],                                               // → First (lowest) door
    isPending: false                                                          // → Normal assignment
  };                                                                          //
}                                                                             //

Test helper:

Apps Script (JavaScript)
// → Test auto door assignment
function BOOK_testAutoAssignDoor_() {                                         // → Test function
  const today = new Date();                                                   // → Today
  const time = new Date();                                                    // → Base time
  time.setHours(10, 0, 0, 0);                                                 // → Assume 10:00 slot
  const equipType = 'CONTAINER';                                              // → Sample equipment

  const result = autoAssignDoor_(today, time, equipType);                     // → Run assignment
  Logger.log(JSON.stringify(result));                                         // → Log result

  if (result.assignedDoor && result.isPending) {                              // → Sanity check
    throw new Error('assignedDoor and isPending are contradictory');         // → Guard logic
  }                                                                           //
}                                                                             //

How to check it: run it and look for a log like {"assignedDoor":"D01","isPending":false}. If you have no active doors configured, you should see {"assignedDoor":null,"isPending":true}. To verify the sequence, create multiple test bookings for the same slot in APPT_MAIN and confirm the doors advance in order.


Wiring auto-assignment and email into the booking-save flow

Once door usage and auto-select functions are ready, we need to wire them into the main booking-save function BOOK_bookAppt() to make them useful. In practice, the natural sequence looks like this:

  1. The user submits booking info via a form or sidebar.
  2. Inside BOOK_bookAppt(data), BOOK_validateBookingInput_(data) validates the input and returns a validated object. Its fields are exactly what part 4 fixed: date, time, endTime, type, door, containerNo. There is no dateObj, timeObj, or equipType.
  3. If validated.door is empty, call autoAssignDoor_() to pick a door.
  4. If no door is available, save the row with column D (door) left blank. The appointment sheet has only columns A-M and no status column, so an empty door cell is the pending state. A separate doorStatus value has nowhere to be written and simply disappears.
  5. After writing the row to the sheet, we call notifyBookingConfirmed_() to send a confirmation email.

One crucial point is locking for concurrent execution. If multiple users can book the same slot at nearly the same time, BOOK_bookAppt() as a whole should be wrapped in LockService.getScriptLock() so that only one request at a time can perform “door assignment + capacity check + duplicate check + write.” If you already implemented a LockService pattern in the previous part, just keep using the same structure.

Step 4 — insert auto-assignment snippet inside BOOK_bookAppt

This step doesn’t rewrite your entire function; you just plug in a small block at the right place.

  • What it does: when the door is empty, auto-assigns one; if none is free, saves with the door cell blank.
  • Do this first: part 4's BOOK_validateBookingInput_() rejects an empty door. Until you relax those two checks, execution never reaches auto-assignment.
  • Where: inside BOOK_bookAppt(payload), right after all validations and just before you actually write to the sheet.
  • After pasting: create several test bookings with the same date/time/equipment and check that doors are assigned in sequence.
TEXT
// In part 4's BOOK_validateBookingInput_() - change these two places.

// (1) Required-door check -> delete it, or relax it as below
//   if (!door) {
//     throw new Error('A door (or yard slot) is required.');
//   }
//   -> treat an empty door as a request for auto-assignment

// (2) Door format check -> only validate when a value was supplied
//   if (!/^D\d{2}$/.test(door)) {
//     throw new Error('Door format is invalid.');
//   }
//   ->
  if (door && !/^D\d{2}$/.test(door)) {
    throw new Error('Door format is invalid.');
  }

// (3) Add the confirmation e-mail address to the return object - without this,
//     no mail is ever sent from a real booking. In the cleanup section add:
  const email = String(data.email || '').trim();
//     and one more line in the returned object:
//       pallet: pallet,
  email: email

The third change matters: part 4's return object has no email field, so without it the notification function below simply returns without sending anything.

Then add this right after the validation call inside BOOK_bookAppt().

TEXT
// Inside BOOK_bookAppt(data) - right after BOOK_validateBookingInput_().

    const validated = BOOK_validateBookingInput_(data);   // <- already in part 4

    // Auto-assign the door. validated uses date/time/type - not dateObj/timeObj/equipType.
    if (!validated.door) {                                          // → No door supplied
      // autoAssignDoor_ takes two Date objects - same conversion part 4's save uses.
      const dp = validated.date.split('-');                         // → year/month/day
      const tp = validated.time.split(':');                         // → hour/minute
      const slotDate = new Date(Number(dp[0]), Number(dp[1]) - 1, Number(dp[2]));
      const slotTime = new Date(1899, 11, 30, Number(tp[0]), Number(tp[1]));
      const auto = autoAssignDoor_(slotDate, slotTime, validated.type);
      validated.door = auto.assignedDoor || '';                     // → blank = pending
    }

    BOOK_validateBusinessRules_(validated);                         // <- already in part 4
    BOOK_checkDuplicateContainer_(validated);                       // <- already in part 4
    BOOK_checkCapacityAndSave_(validated);                          // <- already in part 4

    // Mail only after the row is saved. Never roll a booking back over a mail failure.
    try {
      notifyBookingConfirmed_(validated);
    } catch (mailErr) {
      Logger.log('Confirmation mail failed (booking was saved): ' + mailErr.message);
    }

How to check it: in APPT_MAIN, create several bookings for the same date, time slot, and equipment type with the door left blank. Column D should fill D01, D02, D03 in order, and once you run out of active doors the rows should save with column D empty. To review pending bookings, filter for a blank column D - no status column needed.


Booking confirmation emails – basic skeleton for notifyBookingConfirmed_

Finally, after a booking is saved, we’ll add a function to send a confirmation email. In day-to-day operations, this message is what gets forwarded to the carrier or driver, telling them when to show up and which door to use. Here we’ll build a minimal structure you can expand as needed.

The notification function accepts a booking object and composes a simple text email with date, time, equipment type, and door status. If the email field is blank, it quietly exits without doing anything, so you don’t have to use dummy email addresses in test environments.

Step 5 — add notifyBookingConfirmed_ and a test function

  • What it does: sends a confirmation email including booking and door info.
  • Where: paste this underneath autoAssignDoor_().
  • After pasting: run BOOK_testNotifyBookingConfirmed_() and verify that you receive a test email.
Apps Script (JavaScript)
// → Send booking confirmation email
function notifyBookingConfirmed_(booking) {                                  // → Email sender
  // booking: the object part 4's BOOK_validateBookingInput_() returns, as-is.
  // { date:'YYYY-MM-DD', time:'HH:mm', type, door, containerNo, carrier, client ... }
  // An empty door string *is* the pending state - there is no separate status field.
  if (!booking || typeof booking !== 'object') {                             // → Validate input
    throw new Error('booking object is required');                           // → Required value
  }                                                                          //
  // email arrives only if you added it in step (3) above. With no address we send
  // nothing and return quietly - the booking is already saved, so this is not a failure.
  const to = String(booking.email || '').trim();                             // → Recipient email
  if (!to) {                                                                 // → No email
    return;                                                                  // → Exit quietly
  }                                                                          //

  const dateStr = APPT_ymd_(booking.date)   ;                                // → Date string
  const timeStr = APPT_hm_(booking.time)   ;                                 // → Time string
  const doorLabel = booking.door || '(Door assignment pending)';             // → Door label
  const subject = '[Inbound booking confirmed] ' + dateStr + ' ' + timeStr;  // → Subject

  let body = '';                                                             // → Email body
  body += 'Your inbound booking has been received.\n\n';                     // → Intro
  body += 'Date: ' + dateStr + '\n';                                         // → Date
  body += 'Time: ' + timeStr + '\n';                                         // → Time
  body += 'Equipment type: ' + (booking.type      || '') + '\n';             // → Equipment
  body += 'Container: ' + (booking.containerNo || '') + '\n';               // → Container
  body += 'Door: ' + doorLabel + '\n';                                       // → Door info
  if (!booking.door) {                                                       // → Door still blank
    body += '\nNote: a door has not been assigned yet. We will confirm before arrival.\n';
  }                                                                          //
  body += '\nIf you need to change or cancel this booking, please contact your warehouse representative.\n'; // → Closing

  MailApp.sendEmail({                                                        // → Send email
    to: to,                                                                  // → Recipient
    subject: subject,                                                        // → Subject
    body: body                                                               // → Body
  });                                                                        //
}                                                                            //

Test helper:

Apps Script (JavaScript)
// → Test confirmation email
function BOOK_testNotifyBookingConfirmed_() {                                // → Test function
  const today = new Date();                                                  // → Today
  const time = new Date();                                                   // → Now
  time.setHours(11, 30, 0, 0);                                               // → Set to 11:30

  const dummy = {                                                            // → Sample booking object
    email: Session.getActiveUser().getEmail() || '[email protected]',        // → Current user
    date: APPT_ymd_(today),                                                  // → 'YYYY-MM-DD'
    time: APPT_hm_(time),                                                    // → 'HH:mm'
    type: 'CONTAINER',                                                       // → Equipment type
    containerNo: 'TEST1234567',                                              // → Sample container
    door: 'D01'                                                              // → Sample door (blank = pending)
  };                                                                         //

  notifyBookingConfirmed_(dummy);                                            // → Send test email
}                                                                            //

How to check it: run BOOK_testNotifyBookingConfirmed_ from the editor, then look for an email in your inbox with a subject like “[Inbound booking confirmed] …”. Depending on your org’s settings, Session.getActiveUser().getEmail() may be empty, in which case the email goes to [email protected] as coded.


Practical tips – running safely when multiple bookings hit at once

Once you roll out a Google Sheets booking system with auto door assignment in a live warehouse, how you handle concurrency and edge cases matters more than the feature itself. From experience, these practices keep operations stable:

First, wrap the entire BOOK_bookAppt() function in LockService.getScriptLock(). The lock should cover door auto-assignment, capacity calculation, duplicate checks, and the actual write. If you don’t, two users hitting the same slot at the same time can end up with the same door. A wait time of about 20–30 seconds has been workable in real use.

Second, always whitelist string fields like equipment type and door code. A single typo in the equipment type column can make code think a slot has “zero door usage” and allow far too many bookings. Using a constant list of allowed values and rejecting anything else, as shown here, pays for itself in reduced troubleshooting.

Third, use “door assignment pending” proactively. Instead of forcing the system to magically find a door when they're all full, let the booking in and leave the door cell blank. A shift lead can then clean up the backlog in one pass. In this sheet structure a blank column D is the pending state, so a single blank-D filter gives you the pending view - no status column required.

Fourth, be clear that this series has no cancellation flow yet. The appointment sheet (columns A-M) has no status column, so there is no way to mark a cancelled booking as having released its door. For now, handle cancellations by deleting the row or moving it to the archive sheet - otherwise the door-usage count stays wrong. Adding a real status column means changing the sheet headers, the save function, the capacity count and this part's usage count all at once, which belongs in its own article.

Finally, always simulate on a test spreadsheet first. If you change code against the live sheet, that day’s door stats and reports will be inconsistent. Clone your live structure into a test sheet, seed it with a day’s worth of dummy bookings across hours and equipment types, and run multiple scenarios there before touching production.


Wrap-up – one check you can run today

Google Sheets dock door auto-assignment might look complex from a distance, but at its core it’s just “count which doors are already occupied in this slot, then pick one from the remaining doors” turned into code. Combined with overbooking prevention and locking, it can significantly reduce the time humans spend opening spreadsheets and hand-assigning doors.

One concrete thing you can do today: pick a specific date/time slot in your live APPT_MAIN sheet, manually count the used doors, then run BOOK_testGetDoorUsageForSlotCore_() for that same slot and compare. If the two match exactly, you’re ready to layer auto-assignment and email notifications on top. Passing this single check makes it much easier to confidently evolve the system later when you add doors, extend operating hours, or change capacity rules.