|
5 | 5 |
|
6 | 6 | const ApiClient = (function () { |
7 | 7 | let _baseUrl = null; |
8 | | - var _slotCache = {}; // key: "startISO|endISO|duration" → Promise<result> |
| 8 | + var _prefetchPromise = null; // Promise<{slots: [{start,end}]}> for full date range |
| 9 | + var _prefetchRange = null; // {start: ISO, end: ISO} |
9 | 10 |
|
10 | 11 | function init(appsScriptUrl) { |
11 | 12 | _baseUrl = appsScriptUrl; |
12 | 13 | } |
13 | 14 |
|
14 | 15 | /** |
15 | | - * Prefetch slots for a date range at 15-min granularity. |
16 | | - * Called early (page load) so data is ready when user reaches Step 3. |
| 16 | + * Prefetch slots for the full booking window at 15-min granularity. |
| 17 | + * Called on page load so data is ready when user reaches Step 3. |
17 | 18 | */ |
18 | 19 | function prefetchSlots(startDate, endDate) { |
19 | | - var key = startDate + '|' + endDate + '|15'; |
20 | | - if (_slotCache[key]) return; // already prefetching |
21 | | - _slotCache[key] = apiCall('getAvailableSlots', { |
| 20 | + if (_prefetchPromise) return; |
| 21 | + _prefetchRange = { start: startDate, end: endDate }; |
| 22 | + _prefetchPromise = apiCall('getAvailableSlots', { |
22 | 23 | startDate: startDate, |
23 | 24 | endDate: endDate, |
24 | 25 | durationMinutes: 15, |
25 | | - }).catch(function () { delete _slotCache[key]; }); |
| 26 | + }).catch(function () { |
| 27 | + _prefetchPromise = null; |
| 28 | + _prefetchRange = null; |
| 29 | + }); |
26 | 30 | } |
27 | 31 |
|
28 | 32 | /** |
29 | | - * Get the cache key for a slot request. Returns cached promise if available. |
| 33 | + * Try to serve a slot request from the prefetched data. |
| 34 | + * The prefetch uses 15-min slots; for larger durations we filter |
| 35 | + * to only include slots whose time boundaries align with the requested duration. |
30 | 36 | */ |
31 | | - function getCachedSlots(startDate, endDate, durationMinutes) { |
32 | | - var key = startDate + '|' + endDate + '|' + durationMinutes; |
33 | | - return _slotCache[key] || null; |
| 37 | + function tryServePrefetch(startDate, endDate, durationMinutes) { |
| 38 | + if (!_prefetchPromise || !_prefetchRange) return null; |
| 39 | + // Check if requested range is within prefetched range |
| 40 | + if (new Date(startDate) < new Date(_prefetchRange.start) || |
| 41 | + new Date(endDate) > new Date(_prefetchRange.end)) { |
| 42 | + return null; |
| 43 | + } |
| 44 | + var reqStart = new Date(startDate).getTime(); |
| 45 | + var reqEnd = new Date(endDate).getTime(); |
| 46 | + var durationMs = durationMinutes * 60 * 1000; |
| 47 | + |
| 48 | + return _prefetchPromise.then(function (result) { |
| 49 | + if (!result || !result.slots) return result; |
| 50 | + // Filter slots to the requested date range and duration |
| 51 | + var filtered = result.slots.filter(function (slot) { |
| 52 | + var s = new Date(slot.start).getTime(); |
| 53 | + var e = new Date(slot.end).getTime(); |
| 54 | + return s >= reqStart && e <= reqEnd; |
| 55 | + }); |
| 56 | + // For durations > 15 min, merge consecutive 15-min slots into |
| 57 | + // larger slots of the requested duration |
| 58 | + if (durationMinutes > 15) { |
| 59 | + filtered = mergeIntoSlots(filtered, durationMs); |
| 60 | + } |
| 61 | + return { success: true, slots: filtered }; |
| 62 | + }); |
34 | 63 | } |
35 | 64 |
|
36 | | - function cacheSlots(startDate, endDate, durationMinutes, promise) { |
37 | | - _slotCache[startDate + '|' + endDate + '|' + durationMinutes] = promise; |
| 65 | + /** |
| 66 | + * Merge consecutive 15-min slots into slots of the target duration. |
| 67 | + * E.g., four consecutive 15-min slots → one 60-min slot. |
| 68 | + */ |
| 69 | + function mergeIntoSlots(fifteenMinSlots, durationMs) { |
| 70 | + if (fifteenMinSlots.length === 0) return []; |
| 71 | + var slots = []; |
| 72 | + // Sort by start time |
| 73 | + fifteenMinSlots.sort(function (a, b) { |
| 74 | + return new Date(a.start).getTime() - new Date(b.start).getTime(); |
| 75 | + }); |
| 76 | + // Slide window: for each 15-min slot, check if there are enough |
| 77 | + // consecutive slots to fill the target duration |
| 78 | + for (var i = 0; i < fifteenMinSlots.length; i++) { |
| 79 | + var candidateStart = new Date(fifteenMinSlots[i].start).getTime(); |
| 80 | + var candidateEnd = candidateStart + durationMs; |
| 81 | + // Check that all 15-min slots needed are present and consecutive |
| 82 | + var covered = candidateStart; |
| 83 | + var valid = true; |
| 84 | + for (var j = i; j < fifteenMinSlots.length && covered < candidateEnd; j++) { |
| 85 | + var slotStart = new Date(fifteenMinSlots[j].start).getTime(); |
| 86 | + var slotEnd = new Date(fifteenMinSlots[j].end).getTime(); |
| 87 | + if (slotStart !== covered) { valid = false; break; } |
| 88 | + covered = slotEnd; |
| 89 | + } |
| 90 | + if (valid && covered >= candidateEnd) { |
| 91 | + slots.push({ |
| 92 | + start: fifteenMinSlots[i].start, |
| 93 | + end: new Date(candidateEnd).toISOString(), |
| 94 | + }); |
| 95 | + } |
| 96 | + } |
| 97 | + return slots; |
38 | 98 | } |
39 | 99 |
|
40 | 100 | async function apiCall(action, data) { |
@@ -77,17 +137,16 @@ const ApiClient = (function () { |
77 | 137 | } |
78 | 138 |
|
79 | 139 | async function getAvailableSlots(startDate, endDate, durationMinutes) { |
80 | | - // Check cache first |
81 | | - var cached = getCachedSlots(startDate, endDate, durationMinutes); |
82 | | - if (cached) return cached; |
| 140 | + // Try to serve from prefetched data (covers full booking window) |
| 141 | + var fromPrefetch = tryServePrefetch(startDate, endDate, durationMinutes); |
| 142 | + if (fromPrefetch) return fromPrefetch; |
83 | 143 |
|
84 | | - var promise = apiCall('getAvailableSlots', { |
| 144 | + // Fallback: fresh API call |
| 145 | + return apiCall('getAvailableSlots', { |
85 | 146 | startDate: startDate, |
86 | 147 | endDate: endDate, |
87 | 148 | durationMinutes: durationMinutes, |
88 | 149 | }); |
89 | | - cacheSlots(startDate, endDate, durationMinutes, promise); |
90 | | - return promise; |
91 | 150 | } |
92 | 151 |
|
93 | 152 | async function createBooking(bookingData) { |
|
0 commit comments