fix: rewrite booking-sheet parser to match actual sheet structure
CI/CD / deploy (push) Failing after 1m23s
Details
CI/CD / deploy (push) Failing after 1m23s
Details
Sheet tab is 'WorldPlay' not 'Booking Sheet'. Dates use DD/MM/YYYY format. Room/bed columns are at indices 2/4 not 0/1. Room numbers carry down for multi-bed rooms. Added 'Couch' to hh-living bed types. Changed valueInputOption to RAW to prevent formula injection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6b8bf32c3b
commit
c2b75cc5e9
|
|
@ -1,6 +1,12 @@
|
|||
// Booking sheet integration for WORLDPLAY
|
||||
// Manages bed assignments on a Google Sheets booking sheet
|
||||
// Adapted from VotC booking-sheet.js for single-week event
|
||||
// Sheet structure (tab "WorldPlay"):
|
||||
// Row: "Occupancy Commons Hub" (venue header)
|
||||
// Row: '' | '' | room # | bed # | bed type | 07/06/2026 | 08/06/2026 | ...
|
||||
// Row: '' | '' | 1 | 1 | single | | | ...
|
||||
// (empty rows = section separator)
|
||||
// Row: "Herrnhof Villa" (venue header)
|
||||
// Row: Appt name | Appt # | room # | bed # | bed type | 07/06/2026 | ...
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
|
|
@ -18,7 +24,7 @@ const ACCOMMODATION_CRITERIA = {
|
|||
},
|
||||
'hh-living': {
|
||||
venue: 'Herrnhof Villa',
|
||||
bedTypes: ['Living room', 'Sofa bed', 'Daybed'],
|
||||
bedTypes: ['Living room', 'Sofa bed', 'Daybed', 'Couch'],
|
||||
},
|
||||
'hh-triple': {
|
||||
venue: 'Herrnhof Villa',
|
||||
|
|
@ -38,12 +44,16 @@ const ACCOMMODATION_CRITERIA = {
|
|||
},
|
||||
};
|
||||
|
||||
// Per-day columns for WORLDPLAY (June 7-13)
|
||||
const DAY_COLUMNS = ['Jun 7', 'Jun 8', 'Jun 9', 'Jun 10', 'Jun 11', 'Jun 12', 'Jun 13'];
|
||||
const DAY_ID_TO_LABEL = {
|
||||
'2026-06-07': 'Jun 7', '2026-06-08': 'Jun 8', '2026-06-09': 'Jun 9',
|
||||
'2026-06-10': 'Jun 10', '2026-06-11': 'Jun 11', '2026-06-12': 'Jun 12',
|
||||
'2026-06-13': 'Jun 13',
|
||||
// Date columns on the sheet use DD/MM/YYYY format
|
||||
// Map our ISO date IDs to the sheet header strings
|
||||
const DAY_SHEET_HEADERS = [
|
||||
'07/06/2026', '08/06/2026', '09/06/2026', '10/06/2026',
|
||||
'11/06/2026', '12/06/2026', '13/06/2026',
|
||||
];
|
||||
const DAY_ID_TO_SHEET_HEADER = {
|
||||
'2026-06-07': '07/06/2026', '2026-06-08': '08/06/2026', '2026-06-09': '09/06/2026',
|
||||
'2026-06-10': '10/06/2026', '2026-06-11': '11/06/2026', '2026-06-12': '12/06/2026',
|
||||
'2026-06-13': '13/06/2026',
|
||||
};
|
||||
|
||||
/** Convert 0-based column index to spreadsheet letter (supports A-Z, AA-AZ) */
|
||||
|
|
@ -90,15 +100,9 @@ async function getSheetsClient() {
|
|||
|
||||
/**
|
||||
* Read and parse the booking sheet.
|
||||
* Expected structure per venue section:
|
||||
* Row: "Commons Hub" or "Herrnhof Villa" (venue header)
|
||||
* Row: Room | Bed Type | Week 1 (column headers)
|
||||
* Row: 5 | Bunk up | (bed rows)
|
||||
* ...
|
||||
* Empty row (section separator)
|
||||
*
|
||||
* Returns array of bed objects:
|
||||
* { venue, room, bedType, rowIndex, dayColumns: { 'Jun 7': colIndex, ... }, occupancy: { 'Jun 7': 'Guest Name' | null, ... } }
|
||||
* { venue, room, bedType, rowIndex, dayColumns: { '07/06/2026': colIndex, ... },
|
||||
* occupancy: { '07/06/2026': 'Guest Name' | null, ... } }
|
||||
*/
|
||||
async function parseBookingSheet() {
|
||||
const sheets = await getSheetsClient();
|
||||
|
|
@ -112,52 +116,71 @@ async function parseBookingSheet() {
|
|||
console.log('[Booking Sheet] No BOOKING_SHEET_ID configured — skipping');
|
||||
return null;
|
||||
}
|
||||
const sheetName = process.env.BOOKING_SHEET_TAB || 'Booking Sheet';
|
||||
const quotedName = `'${sheetName}'`;
|
||||
const sheetName = process.env.BOOKING_SHEET_TAB || 'WorldPlay';
|
||||
|
||||
const response = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: sheetId,
|
||||
range: `${quotedName}!A:J`,
|
||||
range: `${sheetName}!A:L`,
|
||||
});
|
||||
|
||||
const rows = response.data.values || [];
|
||||
const beds = [];
|
||||
let currentVenue = null;
|
||||
let dayColIndexes = {};
|
||||
let currentRoom = null; // Room numbers carry down for beds in same room
|
||||
let bedTypeCol = -1; // Column index for bed type (varies by section)
|
||||
let roomCol = -1; // Column index for room #
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
if (!row || row.length === 0 || row.every(cell => !cell || !cell.toString().trim())) {
|
||||
currentVenue = null;
|
||||
dayColIndexes = {};
|
||||
currentRoom = null;
|
||||
bedTypeCol = -1;
|
||||
roomCol = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstCell = (row[0] || '').toString().trim();
|
||||
|
||||
// Check if this is a venue header
|
||||
if (firstCell === 'Commons Hub' || firstCell === 'Herrnhof Villa') {
|
||||
currentVenue = firstCell;
|
||||
if (firstCell.startsWith('Occupancy Commons Hub') || firstCell === 'Commons Hub') {
|
||||
currentVenue = 'Commons Hub';
|
||||
dayColIndexes = {};
|
||||
currentRoom = null;
|
||||
continue;
|
||||
}
|
||||
if (firstCell === 'Herrnhof Villa') {
|
||||
currentVenue = 'Herrnhof Villa';
|
||||
dayColIndexes = {};
|
||||
currentRoom = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is the column header row (contains "Room" and day columns)
|
||||
if (firstCell.toLowerCase() === 'room' && currentVenue) {
|
||||
// Check if this is the column header row (look for date-formatted headers)
|
||||
if (currentVenue && !Object.keys(dayColIndexes).length) {
|
||||
// Find date columns and structural columns
|
||||
let foundDates = false;
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const header = (row[c] || '').toString().trim();
|
||||
if (DAY_COLUMNS.includes(header)) {
|
||||
if (DAY_SHEET_HEADERS.includes(header)) {
|
||||
dayColIndexes[header] = c;
|
||||
foundDates = true;
|
||||
}
|
||||
if (header.toLowerCase() === 'room #') roomCol = c;
|
||||
if (header.toLowerCase() === 'bed type') bedTypeCol = c;
|
||||
}
|
||||
continue;
|
||||
if (foundDates) continue;
|
||||
}
|
||||
|
||||
// If we have a venue and day columns, this is a bed row
|
||||
if (currentVenue && Object.keys(dayColIndexes).length > 0 && firstCell) {
|
||||
const room = firstCell;
|
||||
const bedType = (row[1] || '').toString().trim();
|
||||
if (!bedType) continue;
|
||||
if (currentVenue && Object.keys(dayColIndexes).length > 0 && roomCol >= 0 && bedTypeCol >= 0) {
|
||||
const roomCell = (row[roomCol] || '').toString().trim();
|
||||
if (roomCell) currentRoom = roomCell;
|
||||
|
||||
const bedType = (row[bedTypeCol] || '').toString().trim();
|
||||
if (!bedType || !currentRoom) continue;
|
||||
|
||||
const occupancy = {};
|
||||
for (const [day, colIdx] of Object.entries(dayColIndexes)) {
|
||||
|
|
@ -167,7 +190,7 @@ async function parseBookingSheet() {
|
|||
|
||||
beds.push({
|
||||
venue: currentVenue,
|
||||
room,
|
||||
room: currentRoom,
|
||||
bedType,
|
||||
rowIndex: i,
|
||||
dayColumns: { ...dayColIndexes },
|
||||
|
|
@ -184,9 +207,9 @@ async function parseBookingSheet() {
|
|||
* A bed is "available" if ALL selected day columns are empty.
|
||||
* @param {object[]} beds - parsed bed objects
|
||||
* @param {string} accommodationType - e.g. 'ch-shared'
|
||||
* @param {string[]} dayLabels - e.g. ['Jun 7', 'Jun 9', 'Jun 10']
|
||||
* @param {string[]} dayHeaders - e.g. ['07/06/2026', '09/06/2026']
|
||||
*/
|
||||
function findAvailableBed(beds, accommodationType, dayLabels) {
|
||||
function findAvailableBed(beds, accommodationType, dayHeaders) {
|
||||
const criteria = ACCOMMODATION_CRITERIA[accommodationType];
|
||||
if (!criteria) {
|
||||
console.error(`[Booking Sheet] Unknown accommodation type: ${accommodationType}`);
|
||||
|
|
@ -201,7 +224,7 @@ function findAvailableBed(beds, accommodationType, dayLabels) {
|
|||
if (!matchesBedType) continue;
|
||||
|
||||
// Check ALL selected days are empty for this bed
|
||||
const allEmpty = dayLabels.every(day => !bed.occupancy[day]);
|
||||
const allEmpty = dayHeaders.every(day => !bed.occupancy[day]);
|
||||
if (allEmpty) {
|
||||
return bed;
|
||||
}
|
||||
|
|
@ -224,13 +247,13 @@ async function assignBooking(guestName, accommodationType, selectedDays) {
|
|||
return { success: false, reason: 'Missing accommodation type' };
|
||||
}
|
||||
|
||||
// Convert selectedDays to day labels
|
||||
let dayLabels;
|
||||
// Convert selectedDays to sheet header date strings
|
||||
let dayHeaders;
|
||||
if (selectedDays === 'full-week' || !selectedDays) {
|
||||
dayLabels = [...DAY_COLUMNS]; // all 7 days
|
||||
dayHeaders = [...DAY_SHEET_HEADERS]; // all 7 days
|
||||
} else {
|
||||
dayLabels = selectedDays.map(id => DAY_ID_TO_LABEL[id]).filter(Boolean);
|
||||
if (dayLabels.length === 0) {
|
||||
dayHeaders = selectedDays.map(id => DAY_ID_TO_SHEET_HEADER[id]).filter(Boolean);
|
||||
if (dayHeaders.length === 0) {
|
||||
return { success: false, reason: 'No valid days selected' };
|
||||
}
|
||||
}
|
||||
|
|
@ -241,26 +264,25 @@ async function assignBooking(guestName, accommodationType, selectedDays) {
|
|||
return { success: false, reason: 'Booking sheet not configured' };
|
||||
}
|
||||
|
||||
const bed = findAvailableBed(beds, accommodationType, dayLabels);
|
||||
const bed = findAvailableBed(beds, accommodationType, dayHeaders);
|
||||
if (!bed) {
|
||||
console.warn(`[Booking Sheet] No available bed for ${accommodationType} on days: ${dayLabels.join(', ')}`);
|
||||
console.warn(`[Booking Sheet] No available bed for ${accommodationType} on days: ${dayHeaders.join(', ')}`);
|
||||
return { success: false, reason: 'No available bed matching criteria' };
|
||||
}
|
||||
|
||||
const sheets = await getSheetsClient();
|
||||
const sheetId = process.env.BOOKING_SHEET_ID;
|
||||
const sheetName = process.env.BOOKING_SHEET_TAB || 'Booking Sheet';
|
||||
const quotedName = `'${sheetName}'`;
|
||||
const sheetName = process.env.BOOKING_SHEET_TAB || 'WorldPlay';
|
||||
const rowNum = bed.rowIndex + 1; // Sheets is 1-indexed
|
||||
|
||||
// Build batch update data for each selected day column
|
||||
const data = [];
|
||||
for (const dayLabel of dayLabels) {
|
||||
const colIdx = bed.dayColumns[dayLabel];
|
||||
for (const dayHeader of dayHeaders) {
|
||||
const colIdx = bed.dayColumns[dayHeader];
|
||||
if (colIdx === undefined) continue;
|
||||
const colLetter = colIdxToLetter(colIdx);
|
||||
data.push({
|
||||
range: `${quotedName}!${colLetter}${rowNum}`,
|
||||
range: `${sheetName}!${colLetter}${rowNum}`,
|
||||
values: [[guestName]],
|
||||
});
|
||||
}
|
||||
|
|
@ -272,12 +294,12 @@ async function assignBooking(guestName, accommodationType, selectedDays) {
|
|||
await sheets.spreadsheets.values.batchUpdate({
|
||||
spreadsheetId: sheetId,
|
||||
resource: {
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
valueInputOption: 'RAW',
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[Booking Sheet] Assigned ${guestName} to ${bed.venue} Room ${bed.room} (${bed.bedType}) for ${dayLabels.length} days`);
|
||||
console.log(`[Booking Sheet] Assigned ${guestName} to ${bed.venue} Room ${bed.room} (${bed.bedType}) for ${dayHeaders.length} days`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue