rows that belong to this room type
- let tr = roomEl.closest('tr');
- while (tr) {
- if (tr.classList.contains('js-rt-block-row')) {
- const blockId = tr.getAttribute('data-block-id') || '';
- const priceRaw = tr.getAttribute('data-hotel-rounded-price') || '';
-
- let fltrs = {};
- try { fltrs = JSON.parse(tr.getAttribute('data-fltrs') || '{}'); } catch(e) {}
-
- // Conditions cell (3rd ) holds meal plan + cancel info
- const cells = tr.querySelectorAll('td');
- const condCell = cells.length >= 3 ? cells[2].innerText || '' : '';
-
- const breakfastIncluded = condCell.toLowerCase().includes('breakfast');
- const nonRefundable = (fltrs.non_refundable === 1);
-
- // Free cancellation date: "Free cancellation before DD Month YYYY"
- const cancelMatch = condCell.match(/free cancellation before ([\\w\\s]+)/i);
- const freeCancelText = cancelMatch ? cancelMatch[1].trim() : null;
-
- // Persons: extract from block_id format {room_id}_{rate_plan_id}_{persons}_{meal}_0
- // More reliable than cell text parsing which can pick up the price instead.
- const blockParts = blockId.split('_');
- const maxPersons = blockParts.length >= 3 ? parseInt(blockParts[2]) : null;
-
- // Quantity dropdown: last has a with options 0..N
- // where N = actual available qty (capped at 10 by Booking.com).
- // This is more reliable than the "X left" scarcity text which only
- // appears when availability is low (typically ≤5).
- let qtyAvailable = availCount; // fallback to scarcity text
- const qtyCell = cells.length >= 4 ? cells[cells.length - 1] : null;
- if (qtyCell) {
- const sel = qtyCell.querySelector('select');
- if (sel) {
- const vals = Array.from(sel.options)
- .map(o => parseInt(o.value))
- .filter(v => !isNaN(v) && v > 0);
- if (vals.length > 0) qtyAvailable = Math.max(...vals);
- } else {
- // Fallback: parse max number from cell text
- // "Select rooms\\n0\\n1 (£208)\\n2 (£416)" → [1,2] → max 2
- const nums = (qtyCell.innerText || '').match(/^(\\d+)/gm);
- if (nums && nums.length > 0) {
- const parsed = nums.map(n => parseInt(n)).filter(v => v > 0);
- if (parsed.length > 0) qtyAvailable = Math.max(...parsed);
- }
- }
+ // Quantity dropdown in last : options 0..N where N = available qty (capped at 10)
+ const qtyCell = cells.length >= 2 ? cells[cells.length - 1] : null;
+ if (qtyCell) {
+ const sel = qtyCell.querySelector('select');
+ if (sel) {
+ const vals = Array.from(sel.options)
+ .map(o => parseInt(o.value))
+ .filter(v => !isNaN(v) && v > 0);
+ if (vals.length > 0) qtyAvailable = Math.max(...vals);
+ } else {
+ const nums = (qtyCell.innerText || '').match(/^(\\d+)/gm);
+ if (nums && nums.length > 0) {
+ const parsed = nums.map(n => parseInt(n)).filter(v => v > 0);
+ if (parsed.length > 0) qtyAvailable = Math.max(...parsed);
}
-
- results.push({
- room_id: roomId,
- room_name: roomName,
- avail_count: qtyAvailable,
- block_id: blockId,
- price: priceRaw ? parseInt(priceRaw) : null,
- breakfast_included: breakfastIncluded,
- non_refundable: nonRefundable,
- free_cancel_text: freeCancelText,
- max_persons: maxPersons,
- });
}
-
- tr = tr.nextElementSibling;
- if (!tr) break;
- // Stop at the next room type's header row
- if (tr.querySelector('[id^="room_type_id_"]')) break;
}
- });
+
+ results.push({
+ room_id: roomId,
+ room_name: roomName,
+ avail_count: qtyAvailable,
+ block_id: blockId,
+ price: priceRaw ? parseInt(priceRaw) : null,
+ breakfast_included: breakfastIncluded,
+ non_refundable: nonRefundable,
+ free_cancel_text: freeCancelText,
+ max_persons: !isNaN(maxPersons) ? maxPersons : null,
+ });
+ }
return results;
}