Google Sheets Yard Trailer Status Dashboard — Part 3 (Final)
Intro: Manage yard slots on the sheet, not in your head
The inbound appointment system in this series connects reservations, check-in, and yard moves into a single flow. In the previous posts:
- Automate Google Sheets appointment views: Today sheet — Dashboard Part 1 built the structure that collects a day’s appointments into the TODAY sheet, and
- Google Sheets appointment status dashboard — Dashboard Part 2 summarized appointment counts by status so that managers can see the situation at a glance.
The goal of this post is to build a Google Sheets yard trailer status dashboard. The core idea is to create a logistics dashboard in Google Sheets where you can see on one screen “which trailer (or container) is sitting in which yard slot, and for how many hours.” Using Google Sheets and Apps Script, we’ll read from the YARD sheet and trailer move history (TRAILER_MOVES), calculate each slot’s current occupancy and elapsed time, and visualize it with colors.
On the operations floor, yard slots were often managed for a long time with whiteboards and verbal updates. As the number of units entering the yard increased and turns got faster, it became harder to answer questions like “When did that trailer come in?” or “Is that unit we parked last night still there?” on the spot. To solve this, we turned yard slot occupancy into a Google Sheets dashboard and highlighted risky slots by elapsed time with colors. In this post I’ll document the structure and code currently used in production as closely as possible.
Data structure: how to combine YARD and TRAILER_MOVES
To calculate yard slot occupancy automatically, you first need to define which data plays which role. In this series we’ve already built the following sheets tied into check-in:
- The TRAILER_MOVES sheet from Automatic yard slot assignment and move log in Google Sheets — Check-in Part 5
- The YARD sheet used for basic configuration
Roles of YARD and move history
The YARD sheet is the slot master.
It includes slot ID, description, active flag, etc. You design it to match the physical layout once and then rarely change it. In this post we’ll reuse the existing structure and won’t redefine the column layout. Instead, we’ll use the active flag as the rule to decide “which slots should be shown on the screen.”
The TRAILER_MOVES sheet is the trailer move history.
Every time a trailer’s location changes—check-in, yard slot move, departure—one log row is added. In Check-in Part 5 we used CHK_appendTrailerMove_() to record this, so here we’ll focus on reading that history to build the latest status per slot.
The tasks for Dashboard Part 3 are:
- From YARD, bring in only the currently active slots.
- From TRAILER_MOVES, keep only the most recent record per slot.
- If the last action is
"IN", the slot is occupied; otherwise (e.g."OUT") we consider it empty. - Using the occupancy start time and a reference time (execution time), calculate elapsed time in hours.
- In a defined area of the TODAY sheet, write, per row:
- Slot ID
- Slot name (or area description)
- Current container (or trailer) number
- Occupancy start time
- Elapsed time (hours)
- Status code
and color the rows so that the longer the elapsed time, the stronger the color.
The base design assumes “within 2 hours is normal, up to 4 hours is warning, beyond 4 hours is critical.” These thresholds live in the DASH_YARD_CONFIG constant so you can adjust them anytime to match yard operations. Set up this way, a single monitor lets you quickly see “what’s been sitting too long.”
Main function and config: the skeleton of yard status updates
The main function in this part is named DASH_updateYardStatusView(). In one line:
“Read YARD and TRAILER_MOVES, and draw per-slot current status on the TODAY sheet.”
Preventing concurrent writes: why use LockService
Yard status is often updated on a schedule (time-based triggers, manual runs, calls from other menus, etc.). If multiple executions modify TODAY at the same time, colors and values can overwrite each other or only partially apply. We addressed similar concerns on the appointment dashboard side.
So this main function uses LockService.getDocumentLock() to get a document-level lock, ensuring only one execution updates TODAY at a time. If acquiring the lock takes too long (e.g. 10 seconds), that execution throws an error and exits to prevent overlapping runs from corrupting the sheet.
Step 1 — Define config constants and the main function shell
1) What this code does
- Gathers sheet names, starting positions, thresholds, and color settings for the yard dashboard into a single config object.
- Creates the main function that acquires a document lock, reads YARD and TRAILER_MOVES, and updates the TODAY sheet.
2) Where to paste
- In Google Sheets → Extensions → Apps Script → the same project used in the rest of this series, paste this at the very bottom of the existing code file.
- If
onOpen()is already defined from previous posts, do not create a new one here. We’ll consolidate menus once we wrap up the whole dashboard. - Functions used from earlier parts (like
DASH_getApptDailyViewSheet_()) are not redefined; we assume they exist. If needed, see “Dashboard Part 1.”
3) What to do after pasting
- Save, then run
DASH_testUpdateYardStatusView()once to grant permissions.
// Configuration constants for the yard status view.
const DASH_YARD_CONFIG = {
TZ: 'America/New_York', // Fixed time zone
MOVE_SHEET_NAME: 'TRAILER_MOVES', // Move history sheet name
YARD_SHEET_NAME: 'YARD', // Yard slot sheet name
VIEW_SHEET_NAME: 'TODAY', // Dashboard display sheet
VIEW_START_ROW: 30, // Starting row of the yard table (adjust as needed)
VIEW_START_COL: 1, // Starting column of the yard table (column A)
MAX_SLOT_HOURS_OK: 2, // Up to 2 hours: OK
MAX_SLOT_HOURS_WARN: 4, // 2–4 hours: Warning
COLOR_EMPTY: '#ffffff', // Color for empty slots
COLOR_OK: '#e2f0d9', // Color for normal occupancy (light green)
COLOR_WARN: '#fff2cc', // Warning color (yellowish)
COLOR_ALERT: '#f4cccc' // Critical color (reddish)
};
/**
* Draws current yard slot occupancy status into the TODAY sheet.
* Uses LockService to prevent concurrent runs from corrupting TODAY.
*/
function DASH_updateYardStatusView() {
const lock = LockService.getDocumentLock();
// Wait up to 10 seconds so multiple runs don’t touch TODAY at the same time
const gotLock = lock.tryLock(10 * 1000);
if (!gotLock) {
throw new Error('Could not obtain lock for yard status update. Please try again shortly.');
}
try {
const ss = SpreadsheetApp.getActive();
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!moveSheet || !yardSheet || !viewSheet) {
throw new Error('Please create the YARD, TRAILER_MOVES, and TODAY sheets first.');
}
const now = new Date();
const yardSlots = DASH_loadActiveYardSlots_(yardSheet); // Active slot list
const latestMoves = DASH_loadLatestMovesBySlot_(moveSheet); // Latest status per slot
const viewData = DASH_buildYardViewTable_(yardSlots, latestMoves, now);
DASH_writeYardView_(viewSheet, viewData); // Write values to TODAY
DASH_colorYardView_(viewSheet, viewData); // Apply colors to TODAY
} finally {
// Always release the lock regardless of errors
lock.releaseLock();
}
}
/**
* Test wrapper. Runs the update, then logs completion and
* lightly validates the structure and types of the output data.
*/
function DASH_testUpdateYardStatusView() {
const ss = SpreadsheetApp.getActive();
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!viewSheet) {
throw new Error('TODAY sheet is required.');
}
DASH_updateYardStatusView();
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const lastRow = viewSheet.getLastRow();
const rows = Math.max(0, lastRow - startRow + 1);
if (rows <= 0) {
Logger.log('No yard status data found.');
return;
}
const range = viewSheet.getRange(startRow, startCol, rows, 6);
const values = range.getValues();
// Basic sanity checks
if (!Array.isArray(values)) {
throw new Error('TODAY yard view data is not an array.');
}
if (values.length > 0) {
const row = values[0];
if (!Array.isArray(row) || row.length !== 6) {
throw new Error('Each TODAY yard view row must be an array of length 6.');
}
if (typeof row[0] !== 'string' && typeof row[0] !== 'number') {
throw new Error('First column (slot ID) must be a string or number.');
}
}
Logger.log('DASH_updateYardStatusView executed and basic structure verified.');
}If running DASH_testUpdateYardStatusView() completes without errors, the skeleton is sound. Next we’ll fill in each step function.
Load active yard slots: decide what to display
Next we’ll pull only the slots from the YARD sheet that should appear on the dashboard. If you include inactive slots, you get too much noise and colors mixed in for areas that aren’t really in use.
Step 2 — Read the active yard slot list
In real sites it’s common to have temporary or retired slots still listed in the sheet. I add an “active” flag column in YARD and reflect a slot in the dashboard only when its value is in an allowed list. If you skip the allowed list, blank or odd values can all be interpreted as “active” and clutter the dashboard.
1) What this code does
- Reads the YARD sheet and returns only active slots as an array of
{slotId, name}. - Treats the active flag as valid only when it’s in the allowed values (
Y,YES,TRUE,1).
2) Where to paste
- Directly below the code from Step 1.
3) What to do after pasting
- Run
DASH_testLoadActiveYardSlots_()and check the execution log to see that the active slot list prints correctly.
/**
* Reads the list of active slots from the YARD sheet.
* Returns: [{slotId, name}, ...]
*
* Column assumptions: Col A=Slot ID, Col B=Name, Col C=Active flag
*/
function DASH_loadActiveYardSlots_(yardSheet) {
const lastRow = yardSheet.getLastRow();
if (lastRow < 2) {
// Header only
return [];
}
const range = yardSheet.getRange(2, 1, lastRow - 1, 3); // A:C
const values = range.getValues();
const slots = [];
const ACTIVE_VALUES = ['Y', 'YES', 'TRUE', '1'];
for (let i = 0; i < values.length; i++) {
const row = values[i];
const slotId = row[0]; // Col A
const name = row[1]; // Col B
const active = row[2]; // Col C
if (!slotId) {
// Skip if slot ID is missing (deleted row, etc.)
continue;
}
const activeStr = String(active).trim().toUpperCase();
if (!ACTIVE_VALUES.includes(activeStr)) {
// Exclude inactive slots from the dashboard
continue;
}
slots.push({ slotId, name });
}
return slots;
}
/**
* Test for active slots.
* Validates structure and types, and logs the slot list.
*/
function DASH_testLoadActiveYardSlots_() {
const ss = SpreadsheetApp.getActive();
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
if (!yardSheet) {
throw new Error('YARD sheet is required.');
}
const slots = DASH_loadActiveYardSlots_(yardSheet);
// Expected: array, with objects having required fields and proper types
if (!Array.isArray(slots)) {
throw new Error('DASH_loadActiveYardSlots_ result is not an array.');
}
if (slots.length > 0) {
const s = slots[0];
if (typeof s !== 'object' || s === null) {
throw new Error('Each slot item must be an object.');
}
if (!('slotId' in s) || !('name' in s)) {
throw new Error('Slot object is missing slotId or name field.');
}
if (typeof s.slotId !== 'string' && typeof s.slotId !== 'number') {
throw new Error('slotId must be a string or a number.');
}
if (typeof s.name !== 'string' && s.name !== null && s.name !== undefined) {
throw new Error('name must be a string or an empty value.');
}
}
Logger.log('Active slots: ' + JSON.stringify(slots));
}After running the test, confirm that only active slots are filtered in and that the object structure matches expectations.
Compute latest status per slot from TRAILER_MOVES
Now we need to extract the “current status” from the move history in TRAILER_MOVES. This is the core of tracking trailer locations with Google Sheets.
Step 3 — Keep only the most recent record per slot from move history
The TRAILER_MOVES sheet contains a mix of actions: check-in, yard move, departure, etc. Here we manage action codes with an allowed list so bad values don’t silently affect the dashboard. If a required value is missing or the timestamp isn’t a Date, we skip that row.
1) What this code does
- Reads all rows from TRAILER_MOVES and aggregates them so that for each slot, only the most recent record is stored as
{slotId: {...}}. - Only passes through allowed action codes (IN/OUT/MOVE); others are ignored.
2) Where to paste
- Directly below the Step 2 code.
3) What to do after pasting
- Run
DASH_testLoadLatestMovesBySlot_()and confirm in the logs that the latest status per slot looks correct and has the right structure.
/**
* Computes the latest move record per slot from the TRAILER_MOVES sheet.
* Returns: {slotId: {slotId, container, status, movedAt}, ...}
*
* Column assumptions: Col A=Timestamp, Col B=Slot ID, Col C=Container number, Col D=Action type (IN/OUT/MOVE etc.)
*/
function DASH_loadLatestMovesBySlot_(moveSheet) {
const lastRow = moveSheet.getLastRow();
if (lastRow < 2) {
// No data
return {};
}
const range = moveSheet.getRange(2, 1, lastRow - 1, 4); // A:D
const values = range.getValues();
const latestBySlot = {};
const ALLOWED_ACTIONS = ['IN', 'OUT', 'MOVE'];
for (let i = 0; i < values.length; i++) {
const row = values[i];
const ts = row[0]; // Timestamp
const slotId = row[1]; // Slot ID
const container = row[2]; // Container number
const actionRaw = row[3]; // IN/OUT/MOVE
if (!slotId || !ts) {
// Skip if required values are missing
continue;
}
if (!(ts instanceof Date)) {
// Ignore non-date timestamps (text, etc.)
continue;
}
const action = String(actionRaw || '').toUpperCase();
if (!ALLOWED_ACTIONS.includes(action)) {
// Do not reflect unsupported actions in the dashboard
continue;
}
const prev = latestBySlot[slotId];
if (!prev || prev.movedAt < ts) {
// Replace if this record is more recent for this slot
latestBySlot[slotId] = {
slotId: slotId,
container: container || '',
status: action,
movedAt: ts
};
}
}
return latestBySlot;
}
/**
* Test for latest move status per slot.
* Validates structure and types, and logs the result.
*/
function DASH_testLoadLatestMovesBySlot_() {
const ss = SpreadsheetApp.getActive();
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
if (!moveSheet) {
throw new Error('TRAILER_MOVES sheet is required.');
}
const latest = DASH_loadLatestMovesBySlot_(moveSheet);
// Expected: object keyed by slot ID; each entry has required fields and types
if (typeof latest !== 'object' || latest === null || Array.isArray(latest)) {
throw new Error('DASH_loadLatestMovesBySlot_ result must be an object keyed by slot ID.');
}
const keys = Object.keys(latest);
if (keys.length > 0) {
const example = latest[keys[0]];
if (typeof example !== 'object' || example === null) {
throw new Error('Each slot status must be an object.');
}
const required = ['slotId', 'container', 'status', 'movedAt'];
required.forEach(function (k) {
if (!(k in example)) {
throw new Error('Slot status object is missing field: ' + k);
}
});
if (!(example.movedAt instanceof Date)) {
throw new Error('movedAt field must be of Date type.');
}
if (typeof example.status !== 'string') {
throw new Error('status field must be a string.');
}
}
Logger.log('Latest move status per slot: ' + JSON.stringify(latest));
}Now we have logic to keep only the latest move per slot, with type validation included.
Build the table data: combine slots and moves with elapsed time
With the list of active slots and the latest move per slot, we can construct the 2D array that will be written directly into TODAY. This array includes elapsed time in hours, which we’ll use for coloring later.
Step 4 — Construct the per-slot status table
What matters in yard operations is “where, what, and for how long.” So we calculate elapsed time only for slots whose last status is IN. For OUT or other statuses, we leave the elapsed time blank instead of a number. If the time math goes wrong, NaN can spread and dirty the sheet, so we guard against non-Date values and negative times.
1) What this code does
- For each active slot, builds a row containing:
- Slot ID
- Slot name
- Current container number
- Occupancy start time (Date)
- Elapsed time (hours, 1 decimal place)
- Status code
2) Where to paste
- Directly below the Step 3 code.
3) What to do after pasting
- Run
DASH_testBuildYardViewTable_()and confirm in the logs that the table data has the desired shape and valid field types.
/**
* Builds a 2D array for the yard slot status table.
* Each row: [Slot ID, Name, Container, Start time (Date or ''), Elapsed hours, Status]
*/
function DASH_buildYardViewTable_(yardSlots, latestMoves, now) {
const rows = [];
const msPerHour = 1000 * 60 * 60;
for (let i = 0; i < yardSlots.length; i++) {
const slot = yardSlots[i];
const slotId = slot.slotId;
const name = slot.name || '';
const move = latestMoves[slotId];
let container = '';
let startTime = '';
let hours = '';
let status = '';
if (move && move.status === 'IN') {
// Currently occupied
container = move.container || '';
startTime = move.movedAt;
if (startTime instanceof Date && !isNaN(startTime.getTime())) {
const diffMs = now.getTime() - startTime.getTime();
const diffHours = diffMs / msPerHour;
const safeHours = diffHours < 0 ? 0 : diffHours; // Guard against future timestamps
const rounded = Math.round(safeHours * 10) / 10; // Round to 1 decimal place
hours = Number.isFinite(rounded) ? rounded : '';
}
status = 'IN';
} else if (move) {
// Last action was OUT, MOVE, etc.
status = move.status;
// For OUT status we leave elapsed time blank.
}
rows.push([slotId, name, container, startTime, hours, status]);
}
return rows;
}
/**
* Test for building the status table.
* Logs the resulting array and validates structure and types.
*/
function DASH_testBuildYardViewTable_() {
const ss = SpreadsheetApp.getActive();
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
if (!yardSheet || !moveSheet) {
throw new Error('Please check that YARD and TRAILER_MOVES sheets exist.');
}
const slots = DASH_loadActiveYardSlots_(yardSheet);
const latest = DASH_loadLatestMovesBySlot_(moveSheet);
const now = new Date();
const table = DASH_buildYardViewTable_(slots, latest, now);
// Expected: 2D array with rows of fixed length and correct field types
if (!Array.isArray(table)) {
throw new Error('DASH_buildYardViewTable_ result is not an array.');
}
if (table.length > 0) {
const row = table[0];
if (!Array.isArray(row)) {
throw new Error('Each yard view row must be an array.');
}
if (row.length !== 6) {
throw new Error('Each yard view row must have 6 fields.');
}
const [slotId, name, container, startTime, hours, status] = row;
if (typeof slotId !== 'string' && typeof slotId !== 'number') {
throw new Error('Slot ID must be a string or a number.');
}
if (typeof name !== 'string' && name !== null && name !== undefined) {
throw new Error('Slot name must be a string or an empty value.');
}
if (typeof container !== 'string' && container !== '') {
throw new Error('Container number must be a string or an empty string.');
}
if (!(startTime === '' || startTime instanceof Date)) {
throw new Error('Start time must be a Date or an empty string.');
}
if (!(hours === '' || typeof hours === 'number')) {
throw new Error('Elapsed hours must be a number or an empty string.');
}
if (typeof status !== 'string') {
throw new Error('Status code must be a string.');
}
}
Logger.log('Yard view table: ' + JSON.stringify(table));
}This gives you table data with built-in validation of structure and field types. You can also verify that OUT slots have blank elapsed time.
Write to TODAY and color by elapsed time
Now for the final two tasks of the Google Sheets yard slot status dashboard:
- Write
viewDatainto the designated region of the TODAY sheet - Change row colors by elapsed time
For performance, we perform value writes and color updates each in a single batch.
Step 5 — Write the yard status table and set display formats
The TODAY sheet may already be hosting other dashboards (appointment summary, status views, etc.). To keep this yard view safe, we place it as a separate block in an unused area near the bottom. This position is controlled by VIEW_START_ROW.
1) What this code does
- Clears the yard area in TODAY once, then writes
viewDataas is. - Leaves the occupancy time column as Date values and just sets the display format to
"MM-dd HH:mm".
2) Where to paste
- Directly below the Step 4 code.
3) What to do after pasting
- Run
DASH_testWriteYardView_()and confirm that data appears in the expected location on TODAY and that the written data’s structure is valid.
/**
* Writes the yard status table into the yard region of the TODAY sheet.
*/
function DASH_writeYardView_(viewSheet, viewData) {
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const maxRows = Math.max(viewData.length, 1);
const clearRange = viewSheet.getRange(startRow, startCol, maxRows, 6);
// Clear previous data and formatting
clearRange.clearContent();
clearRange.clearFormat();
if (viewData.length === 0) {
// Nothing to show
return;
}
const range = viewSheet.getRange(startRow, startCol, viewData.length, 6);
range.setValues(viewData);
// Apply time format to the start time column (4th column).
const timeRange = viewSheet.getRange(startRow, startCol + 3, viewData.length, 1);
timeRange.setNumberFormat('MM-dd HH:mm');
}
/**
* Test for write behavior.
* Validates viewData structure and actually writes to TODAY.
*/
function DASH_testWriteYardView_() {
const ss = SpreadsheetApp.getActive();
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!yardSheet || !moveSheet || !viewSheet) {
throw new Error('Please verify that YARD, TRAILER_MOVES, and TODAY sheets exist.');
}
const slots = DASH_loadActiveYardSlots_(yardSheet);
const latest = DASH_loadLatestMovesBySlot_(moveSheet);
const now = new Date();
const table = DASH_buildYardViewTable_(slots, latest, now);
// Basic validation of viewData
if (!Array.isArray(table)) {
throw new Error('viewData(table) is not an array.');
}
if (table.length > 0) {
const row = table[0];
if (!Array.isArray(row) || row.length !== 6) {
throw new Error('Each viewData row must be an array of length 6.');
}
}
DASH_writeYardView_(viewSheet, table);
// After writing to TODAY, read back and verify length and types.
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const range = viewSheet.getRange(startRow, startCol, table.length || 1, 6);
const written = range.getValues();
if (!Array.isArray(written)) {
throw new Error('Data read back from TODAY is not an array.');
}
Logger.log('TODAY yard view write and basic validation complete, row count: ' + written.length);
}If TODAY shows slot ID, name, container, start time, elapsed hours, and status in columns A–F starting at VIEW_START_ROW, things are working.
Step 6 — Color rows by elapsed time
The last step is visualization. Rows should change color as elapsed time grows so you can immediately spot trailers that have been sitting too long.
1) What this code does
- Loops through each row of
viewData, reads the elapsed hours, and for each: - No container or blank elapsed time: empty-slot color
MAX_SLOT_HOURS_OKor less: OK color- Up to
MAX_SLOT_HOURS_WARN: warning color - Above that: critical color
and then applies the background color to the entire row.
2) Where to paste
- Directly below the Step 5 code.
3) What to do after pasting
- Run
DASH_testColorYardView_()and verify that colors on TODAY follow the specified rules; the test also validates the viewData structure.
/**
* Applies elapsed-time-based colors to the yard status region in TODAY.
*/
function DASH_colorYardView_(viewSheet, viewData) {
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const rows = viewData.length;
if (rows === 0) {
return;
}
const colors = [];
for (let i = 0; i < rows; i++) {
const row = viewData[i];
const container = row[2]; // Container
const hours = row[4]; // Elapsed hours
let color = DASH_YARD_CONFIG.COLOR_EMPTY;
if (container && Number.isFinite(hours)) {
if (hours <= DASH_YARD_CONFIG.MAX_SLOT_HOURS_OK) {
color = DASH_YARD_CONFIG.COLOR_OK;
} else if (hours <= DASH_YARD_CONFIG.MAX_SLOT_HOURS_WARN) {
color = DASH_YARD_CONFIG.COLOR_WARN;
} else {
color = DASH_YARD_CONFIG.COLOR_ALERT;
}
}
const rowColors = [];
for (let c = 0; c < 6; c++) {
rowColors.push(color);
}
colors.push(rowColors);
}
const range = viewSheet.getRange(startRow, startCol, rows, 6);
range.setBackgrounds(colors);
}
/**
* Test function for color application.
* Validates the in-memory viewData structure and
* applies colors over the data already written in TODAY.
*/
function DASH_testColorYardView_() {
const ss = SpreadsheetApp.getActive();
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!viewSheet) {
throw new Error('TODAY sheet is required.');
}
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const lastRow = viewSheet.getLastRow();
const rows = Math.max(0, lastRow - startRow + 1);
if (rows <= 0) {
Logger.log('No data in the TODAY yard region.');
return;
}
const range = viewSheet.getRange(startRow, startCol, rows, 6);
const values = range.getValues();
// Expected: 2D array; each row length and hours type
if (!Array.isArray(values)) {
throw new Error('TODAY yard view values are not an array.');
}
if (values.length > 0) {
const row = values[0];
if (!Array.isArray(row) || row.length !== 6) {
throw new Error('Each TODAY yard view row must be an array of length 6.');
}
const hours = row[4];
if (!(hours === '' || typeof hours === 'number')) {
throw new Error('Elapsed hours column must be a number or an empty string.');
}
}
DASH_colorYardView_(viewSheet, values);
Logger.log('Yard view color application complete.');
}In the TODAY sheet’s yard region you should see:
- Slots with no container or blank elapsed time: white (
COLOR_EMPTY) - Slots with elapsed time ≤
MAX_SLOT_HOURS_OK(default 2 hours): light green (COLOR_OK) - Up to
MAX_SLOT_HOURS_WARN(default 4 hours): yellow (COLOR_WARN) - Above that: red-ish (
COLOR_ALERT)
You can freely adjust colors and thresholds in DASH_YARD_CONFIG.
Practical tips and error-prevention points
Tune thresholds and colors to your actual turn time
The 2/4 hour thresholds here are just examples. Realistic values depend on your turn time and customer expectations. Slower yards might prefer 4/8 hours as “OK/warn,” while very fast-turn operations might need 1/2-hour buckets.
The key is:
- First agree with your team on “what action to take when each color shows up,” then
- Reflect that agreement in both
DASH_YARD_CONFIGand your floor SOPs.
If colors change but there’s no behavior tied to them, operations will get confused.
Avoid layout clashes on the TODAY sheet
This series also puts other dashboards on TODAY:
- Dashboard Part 1: daily appointment list
- Dashboard Part 2: status summary
To avoid overlap:
- Check the last row currently used on TODAY.
- Leave 5–10 blank rows below that and set
VIEW_START_ROWto that new starting line. - On the row right above the yard view, add a note like “Yard Status (auto-generated area, do not edit manually)” so other users don’t overwrite it.
This makes future dashboard expansion less likely to cause layout conflicts.
Use the test functions and wire things up step by step
When first adding the yard status code, it’s better to run the test functions in order from top to bottom rather than jumping straight to the full flow:
DASH_testLoadActiveYardSlots_— validates YARD structure, active flag, and object shapeDASH_testLoadLatestMovesBySlot_— confirms TRAILER_MOVES structure, action codes, and typesDASH_testBuildYardViewTable_— checks table structure and field typesDASH_testWriteYardView_— confirms write location/format/row count on TODAYDASH_testColorYardView_— validates viewData and color rules on real sheet data- Finally,
DASH_testUpdateYardStatusView— checks the full flow and TODAY’s final layout
This makes it easy to pinpoint exactly where things are failing and then fix sheet layouts or data formats accordingly.
Wrap-up
In this post we focused on building a Google Sheets yard trailer status dashboard using the existing YARD master and TRAILER_MOVES move history from earlier in the series, and we implemented a per-slot current occupancy and elapsed-time view on TODAY with color coding.
Key points:
- We used the YARD sheet to collect only active slots as dashboard targets.
- We read TRAILER_MOVES and reduced it to one latest move per slot to infer current status.
- We calculated elapsed time in hours from occupancy start time to the current time.
- We wrote that data into a defined region of TODAY and colored rows by thresholds (2/4 hours) so the longest-staying trailers stand out.
- We protected TODAY from concurrent updates using
LockServiceso overlapping executions don’t scramble the view. - Each test function includes structure and type checks so that if someone changes the sheet layout, you get immediate, explicit errors instead of silent misbehavior.
The next concrete steps you can take:
- On the TODAY sheet, pick a blank area near the bottom and set
DASH_YARD_CONFIG.VIEW_START_ROWto that starting row. - Paste all the code from this post into the same Apps Script project used for the earlier dashboard parts.
- Run
DASH_testUpdateYardStatusView().
If you’re already writing move logs from Check-in Part 5, you should immediately see a yard board where each slot shows which container is there and for how many hours, with colors mapping to urgency. After that, just add a time-based trigger to run every 5–10 minutes, and a single monitor will give you continuous visibility into dwell time across the yard.