diff --git a/api/booking-sheet.js b/api/booking-sheet.js index eedb18c..e6f5d89 100644 --- a/api/booking-sheet.js +++ b/api/booking-sheet.js @@ -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,