// Booking sheet integration for WORLDPLAY // Manages bed assignments on a Google Sheets booking sheet // Adapted from VotC booking-sheet.js for single-week event const fs = require('fs'); let sheetsClient = null; // Accommodation criteria mapping — maps accommodation_type to sheet matching rules const ACCOMMODATION_CRITERIA = { 'ch-shared': { venue: 'Commons Hub', bedTypes: ['Bunk up', 'Bunk down', 'Single'], }, 'ch-double': { venue: 'Commons Hub', bedTypes: ['Double'], }, 'hh-living': { venue: 'Herrnhof Villa', bedTypes: ['Living room', 'Sofa bed', 'Daybed'], }, 'hh-triple': { venue: 'Herrnhof Villa', bedTypes: ['Triple'], }, 'hh-twin': { venue: 'Herrnhof Villa', bedTypes: ['Twin', 'Double (separate)'], }, 'hh-single': { venue: 'Herrnhof Villa', bedTypes: ['Single'], }, 'hh-couple': { venue: 'Herrnhof Villa', bedTypes: ['Double (shared)', 'Double', 'Couple'], }, }; // Single week for WORLDPLAY (June 7-13) const WEEK_COLUMNS = ['Week 1']; function getCredentials() { const filePath = process.env.GOOGLE_SERVICE_ACCOUNT_FILE; if (filePath) { try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (err) { console.error('[Booking Sheet] Failed to read credentials file:', err.message); return null; } } const raw = process.env.GOOGLE_SERVICE_ACCOUNT; if (!raw) return null; try { return JSON.parse(raw.trim()); } catch { console.error('[Booking Sheet] Failed to parse service account JSON'); return null; } } async function getSheetsClient() { if (sheetsClient) return sheetsClient; const creds = getCredentials(); if (!creds) return null; const { google } = require('googleapis'); const auth = new google.auth.GoogleAuth({ credentials: creds, scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); sheetsClient = google.sheets({ version: 'v4', auth }); return sheetsClient; } /** * 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, weekColumns: { 'Week 1': colIndex }, occupancy: { 'Week 1': 'Guest Name' | null } } */ async function parseBookingSheet() { const sheets = await getSheetsClient(); if (!sheets) { console.log('[Booking Sheet] No credentials configured — skipping'); return null; } const sheetId = process.env.BOOKING_SHEET_ID; if (!sheetId) { console.log('[Booking Sheet] No BOOKING_SHEET_ID configured — skipping'); return null; } const sheetName = process.env.BOOKING_SHEET_TAB || 'Booking Sheet'; const response = await sheets.spreadsheets.values.get({ spreadsheetId: sheetId, range: `${sheetName}!A:G`, }); const rows = response.data.values || []; const beds = []; let currentVenue = null; let weekColIndexes = {}; 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; weekColIndexes = {}; continue; } const firstCell = (row[0] || '').toString().trim(); // Check if this is a venue header if (firstCell === 'Commons Hub' || firstCell === 'Herrnhof Villa') { currentVenue = firstCell; weekColIndexes = {}; continue; } // Check if this is the column header row (contains "Room" and week columns) if (firstCell.toLowerCase() === 'room' && currentVenue) { for (let c = 0; c < row.length; c++) { const header = (row[c] || '').toString().trim(); if (WEEK_COLUMNS.includes(header)) { weekColIndexes[header] = c; } } continue; } // If we have a venue and week columns, this is a bed row if (currentVenue && Object.keys(weekColIndexes).length > 0 && firstCell) { const room = firstCell; const bedType = (row[1] || '').toString().trim(); if (!bedType) continue; const occupancy = {}; for (const [week, colIdx] of Object.entries(weekColIndexes)) { const cellValue = (row[colIdx] || '').toString().trim(); occupancy[week] = cellValue || null; } beds.push({ venue: currentVenue, room, bedType, rowIndex: i, weekColumns: { ...weekColIndexes }, occupancy, }); } } return beds; } /** * Find an available bed matching the accommodation criteria. * A bed is "available" if the Week 1 column is empty. */ function findAvailableBed(beds, accommodationType) { const criteria = ACCOMMODATION_CRITERIA[accommodationType]; if (!criteria) { console.error(`[Booking Sheet] Unknown accommodation type: ${accommodationType}`); return null; } for (const bed of beds) { if (bed.venue !== criteria.venue) continue; const bedTypeLower = bed.bedType.toLowerCase(); const matchesBedType = criteria.bedTypes.some(bt => bedTypeLower.includes(bt.toLowerCase())); if (!matchesBedType) continue; // Check Week 1 is empty if (!bed.occupancy['Week 1']) { return bed; } } return null; } /** * Assign a guest to a bed on the booking sheet. * Writes guest name to the Week 1 column for the matched bed row. * * @param {string} guestName - Full name of the guest * @param {string} accommodationType - e.g. 'ch-shared', 'hh-single' * @returns {object} Result with success status, venue, room, bedType */ async function assignBooking(guestName, accommodationType) { if (!accommodationType) { return { success: false, reason: 'Missing accommodation type' }; } try { const beds = await parseBookingSheet(); if (!beds) { return { success: false, reason: 'Booking sheet not configured' }; } const bed = findAvailableBed(beds, accommodationType); if (!bed) { console.warn(`[Booking Sheet] No available bed for ${accommodationType}`); 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 weekCol = bed.weekColumns['Week 1']; if (weekCol === undefined) { return { success: false, reason: 'Week 1 column not found' }; } const colLetter = String.fromCharCode(65 + weekCol); const rowNum = bed.rowIndex + 1; // Sheets is 1-indexed await sheets.spreadsheets.values.update({ spreadsheetId: sheetId, range: `${sheetName}!${colLetter}${rowNum}`, valueInputOption: 'USER_ENTERED', requestBody: { values: [[guestName]] }, }); console.log(`[Booking Sheet] Assigned ${guestName} to ${bed.venue} Room ${bed.room} (${bed.bedType})`); return { success: true, venue: bed.venue, room: bed.room, bedType: bed.bedType, }; } catch (error) { console.error('[Booking Sheet] Assignment failed:', error); return { success: false, reason: error.message }; } } module.exports = { assignBooking, parseBookingSheet, findAvailableBed, ACCOMMODATION_CRITERIA };