@happychef/algorithm 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +16 -0
- package/.github/workflows/ci-cd.yml +80 -80
- package/BRANCH_PROTECTION_SETUP.md +167 -167
- package/CHANGELOG.md +8 -8
- package/README.md +144 -144
- package/RESERVERINGEN_GIDS.md +986 -986
- package/__tests__/crossMidnight.test.js +63 -0
- package/__tests__/crossMidnightTimeblocks.test.js +312 -0
- package/__tests__/edgeCases.test.js +271 -0
- package/__tests__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +179 -175
- package/__tests__/isTimeAvailable.test.js +174 -168
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +506 -444
- package/changes/2025/December/PR2___change.md +14 -14
- package/changes/2025/December/PR3_add__change.md +20 -20
- package/changes/2025/December/PR4___.md +15 -15
- package/changes/2025/December/PR5___.md +15 -15
- package/changes/2025/December/PR6__del_.md +17 -17
- package/changes/2025/December/PR7_add__change.md +21 -21
- package/changes/2026/February/PR15_add__change.md +21 -21
- package/changes/2026/February/PR16_add__.md +20 -0
- package/changes/2026/February/PR16_add_getDateClosingReasons.md +31 -31
- package/changes/2026/January/PR10_add__change.md +21 -21
- package/changes/2026/January/PR11_add__change.md +19 -19
- package/changes/2026/January/PR12_add__.md +21 -21
- package/changes/2026/January/PR13_add__change.md +20 -20
- package/changes/2026/January/PR14_add__change.md +19 -19
- package/changes/2026/January/PR8_add__change.md +38 -38
- package/changes/2026/January/PR9_add__change.md +19 -19
- package/dateHelpers.js +31 -0
- package/filters/maxArrivalsFilter.js +114 -114
- package/filters/maxGroupsFilter.js +221 -221
- package/filters/timeFilter.js +89 -89
- package/getAvailableTimeblocks.js +158 -158
- package/getDateClosingReasons.js +193 -193
- package/grouping.js +162 -162
- package/index.js +48 -43
- package/isDateAvailable.js +80 -80
- package/isDateAvailableWithTableCheck.js +172 -172
- package/isTimeAvailable.js +26 -26
- package/jest.config.js +23 -23
- package/package.json +27 -27
- package/processing/dailyGuestCounts.js +73 -73
- package/processing/mealTypeCount.js +133 -133
- package/processing/timeblocksAvailable.js +344 -182
- package/reservation_data/counter.js +82 -75
- package/restaurant_data/exceptions.js +150 -150
- package/restaurant_data/openinghours.js +142 -142
- package/simulateTableAssignment.js +833 -726
- package/tableHelpers.js +209 -209
- package/tables/time/parseTime.js +19 -19
- package/tables/time/shifts.js +7 -7
- package/tables/utils/calculateDistance.js +13 -13
- package/tables/utils/isTableFreeForAllSlots.js +14 -14
- package/tables/utils/isTemporaryTableValid.js +39 -39
- package/test/test_counter.js +194 -194
- package/test/test_dailyCount.js +81 -81
- package/test/test_datesAvailable.js +106 -106
- package/test/test_exceptions.js +172 -172
- package/test/test_isDateAvailable.js +330 -330
- package/test/test_mealTypeCount.js +54 -54
- package/test/test_timesAvailable.js +88 -88
- package/test-detailed-filter.js +100 -100
- package/test-lunch-debug.js +110 -110
- package/test-max-arrivals-filter.js +79 -79
- package/test-meal-stop-fix.js +147 -147
- package/test-meal-stop-simple.js +93 -93
- package/test-timezone-debug.js +47 -47
- package/test.js +336 -336
|
@@ -1,183 +1,345 @@
|
|
|
1
|
-
const { getDailyGuestCounts } = require('./dailyGuestCounts');
|
|
2
|
-
const { getMealTypesWithShifts } = require('./mealTypeCount');
|
|
3
|
-
const { getDataByDateAndMealWithExceptions } = require('../restaurant_data/exceptions');
|
|
4
|
-
const { getMealTypeByTime,
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
1
|
+
const { getDailyGuestCounts } = require('./dailyGuestCounts');
|
|
2
|
+
const { getMealTypesWithShifts } = require('./mealTypeCount');
|
|
3
|
+
const { getDataByDateAndMealWithExceptions } = require('../restaurant_data/exceptions');
|
|
4
|
+
const { getMealTypeByTime, daysOfWeekEnglish } = require('../restaurant_data/openinghours');
|
|
5
|
+
const { getGuestCountAtHour } = require('../reservation_data/counter');
|
|
6
|
+
const { getNextDateStr } = require('../dateHelpers');
|
|
7
|
+
const moment = require('moment-timezone');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses a time string in "HH:MM" format into minutes since midnight.
|
|
11
|
+
*/
|
|
12
|
+
function parseTime(timeStr) {
|
|
13
|
+
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
14
|
+
return hours * 60 + minutes;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves interval and duurReservatie from general settings
|
|
19
|
+
*/
|
|
20
|
+
function getInterval(data) {
|
|
21
|
+
let intervalReservatie = 15;
|
|
22
|
+
if (
|
|
23
|
+
data['general-settings'] &&
|
|
24
|
+
data['general-settings'].intervalReservatie &&
|
|
25
|
+
parseInt(data['general-settings'].intervalReservatie, 10) > 0
|
|
26
|
+
) {
|
|
27
|
+
intervalReservatie = parseInt(data['general-settings'].intervalReservatie, 10);
|
|
28
|
+
}
|
|
29
|
+
return intervalReservatie;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getDuurReservatie(data) {
|
|
33
|
+
let duurReservatie = 120;
|
|
34
|
+
if (
|
|
35
|
+
data['general-settings'] &&
|
|
36
|
+
data['general-settings'].duurReservatie &&
|
|
37
|
+
parseInt(data['general-settings'].duurReservatie, 10) > 0
|
|
38
|
+
) {
|
|
39
|
+
duurReservatie = parseInt(data['general-settings'].duurReservatie, 10);
|
|
40
|
+
}
|
|
41
|
+
return duurReservatie;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getHoursOpenedPastMidnight(data, dateStr) {
|
|
45
|
+
// Per-day value from dinner opening hours
|
|
46
|
+
const m = moment.tz(dateStr, 'YYYY-MM-DD', 'Europe/Brussels');
|
|
47
|
+
if (m.isValid()) {
|
|
48
|
+
const day = daysOfWeekEnglish[m.day()];
|
|
49
|
+
const dinnerSettings = data['openinghours-dinner']?.schemeSettings?.[day];
|
|
50
|
+
if (dinnerSettings?.hoursOpenedPastMidnight !== undefined) {
|
|
51
|
+
const parsed = parseInt(dinnerSettings.hoursOpenedPastMidnight, 10);
|
|
52
|
+
return (!isNaN(parsed) && parsed > 0) ? parsed : 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Fallback: global setting from general-settings (backward compat)
|
|
56
|
+
const val = data['general-settings']?.hoursOpenedPastMidnight;
|
|
57
|
+
const parsed = parseInt(val, 10);
|
|
58
|
+
return (!isNaN(parsed) && parsed > 0) ? parsed : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Checks if hoursOpenedPastMidnight is explicitly defined in settings.
|
|
63
|
+
* Used to distinguish bowling restaurants (who set it) from regular ones (who don't).
|
|
64
|
+
*/
|
|
65
|
+
function hasMidnightSetting(data, dateStr) {
|
|
66
|
+
const m = moment.tz(dateStr, 'YYYY-MM-DD', 'Europe/Brussels');
|
|
67
|
+
if (m.isValid()) {
|
|
68
|
+
const day = daysOfWeekEnglish[m.day()];
|
|
69
|
+
const dinnerSettings = data['openinghours-dinner']?.schemeSettings?.[day];
|
|
70
|
+
if (dinnerSettings?.hoursOpenedPastMidnight !== undefined) return true;
|
|
71
|
+
}
|
|
72
|
+
const val = data['general-settings']?.hoursOpenedPastMidnight;
|
|
73
|
+
return val !== undefined && val !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if a time slot belongs to a meal that has the selected giftcard enabled.
|
|
78
|
+
*/
|
|
79
|
+
function timeHasGiftcard(data, dateStr, timeStr, giftcard) {
|
|
80
|
+
if (!giftcard || (typeof giftcard === 'string' && !giftcard.trim())) {
|
|
81
|
+
return true; // No giftcard => no restriction
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const mealType = getMealTypeByTime(timeStr);
|
|
85
|
+
if (!mealType) return false;
|
|
86
|
+
|
|
87
|
+
const m = moment.tz(dateStr, 'YYYY-MM-DD', 'Europe/Brussels');
|
|
88
|
+
if (!m.isValid()) return false;
|
|
89
|
+
const englishDay = daysOfWeekEnglish[m.day()];
|
|
90
|
+
|
|
91
|
+
const ohKey = `openinghours-${mealType}`;
|
|
92
|
+
const daySettings =
|
|
93
|
+
data[ohKey] &&
|
|
94
|
+
data[ohKey].schemeSettings &&
|
|
95
|
+
data[ohKey].schemeSettings[englishDay]
|
|
96
|
+
? data[ohKey].schemeSettings[englishDay]
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
if (!daySettings) return false;
|
|
100
|
+
if (daySettings.giftcardsEnabled !== true) return false;
|
|
101
|
+
if (!Array.isArray(daySettings.giftcards) || daySettings.giftcards.length === 0) return false;
|
|
102
|
+
|
|
103
|
+
return daySettings.giftcards.includes(giftcard);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Determines if a given start time fits within any meal's timeframe.
|
|
108
|
+
* For bowling restaurants (hasMidnightSetting), the extended endTime is used so that
|
|
109
|
+
* slots like 23:15, 24:00 are valid start times.
|
|
110
|
+
* For regular restaurants, uses the original production logic:
|
|
111
|
+
* startTime + duurReservatie <= mealEndTime
|
|
112
|
+
* which naturally caps the last bookable slot (e.g. 22:45 for 23:00 close + 15min interval).
|
|
113
|
+
*/
|
|
114
|
+
function fitsWithinMeal(data, dateStr, startTimeStr, duurReservatie) {
|
|
115
|
+
const startTime = parseTime(startTimeStr);
|
|
116
|
+
|
|
117
|
+
// Original production logic for breakfast/lunch (always, regardless of bowling)
|
|
118
|
+
const mealType = getMealTypeByTime(startTimeStr);
|
|
119
|
+
if (mealType === 'breakfast' || mealType === 'lunch') {
|
|
120
|
+
const mealData = getDataByDateAndMealWithExceptions(data, dateStr, mealType);
|
|
121
|
+
if (!mealData) return false;
|
|
122
|
+
const mealStartTime = parseTime(mealData.startTime);
|
|
123
|
+
const mealEndTime = parseTime(mealData.endTime);
|
|
124
|
+
return startTime >= mealStartTime && startTime + duurReservatie <= mealEndTime;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (hasMidnightSetting(data, dateStr)) {
|
|
128
|
+
// Bowling dinner: allow start times up to extended endTime (e.g. 24:00)
|
|
129
|
+
const dinnerData = getDataByDateAndMealWithExceptions(data, dateStr, 'dinner');
|
|
130
|
+
if (!dinnerData) return false;
|
|
131
|
+
const dinnerStart = parseTime(dinnerData.startTime);
|
|
132
|
+
const dinnerEnd = parseTime(dinnerData.endTime);
|
|
133
|
+
return startTime >= dinnerStart && startTime <= dinnerEnd;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Regular restaurant dinner: original production logic
|
|
137
|
+
if (!mealType) return false;
|
|
138
|
+
const mealData = getDataByDateAndMealWithExceptions(data, dateStr, mealType);
|
|
139
|
+
if (!mealData) return false;
|
|
140
|
+
const mealStartTime = parseTime(mealData.startTime);
|
|
141
|
+
const mealEndTime = parseTime(mealData.endTime);
|
|
142
|
+
return startTime >= mealStartTime && startTime + duurReservatie <= mealEndTime;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Converts minutes since midnight to "HH:MM" string.
|
|
147
|
+
*/
|
|
148
|
+
function minutesToTimeStr(minutes) {
|
|
149
|
+
const h = Math.floor(minutes / 60);
|
|
150
|
+
const m = minutes % 60;
|
|
151
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets the max capacity for a time on a given date by checking all meals.
|
|
156
|
+
* Returns 0 if no meal covers the time (restaurant is closed at that time).
|
|
157
|
+
*/
|
|
158
|
+
function getMaxCapacityForTime(data, dateStr, timeMinutes) {
|
|
159
|
+
for (const mealType of ['breakfast', 'lunch', 'dinner']) {
|
|
160
|
+
const mealData = getDataByDateAndMealWithExceptions(data, dateStr, mealType);
|
|
161
|
+
if (!mealData) continue;
|
|
162
|
+
const mealStart = parseTime(mealData.startTime);
|
|
163
|
+
const mealEnd = parseTime(mealData.endTime);
|
|
164
|
+
if (timeMinutes >= mealStart && timeMinutes <= mealEnd) {
|
|
165
|
+
return parseInt(mealData.maxCapacity, 10) || 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false, duration = null) {
|
|
172
|
+
const defaultDuurReservatie = getDuurReservatie(data);
|
|
173
|
+
const duurReservatie = duration && duration > 0 ? duration : defaultDuurReservatie;
|
|
174
|
+
const intervalReservatie = getInterval(data);
|
|
175
|
+
|
|
176
|
+
// Slots needed
|
|
177
|
+
const slotsNeeded = Math.ceil(duurReservatie / intervalReservatie);
|
|
178
|
+
|
|
179
|
+
// Get guest counts and shifts info
|
|
180
|
+
const { guestCounts, shiftsInfo } = getDailyGuestCounts(data, dateStr, reservations);
|
|
181
|
+
|
|
182
|
+
// If custom duration exceeds default, extend guestCounts with additional slots
|
|
183
|
+
// so that late-evening reservations have enough consecutive slots to check against.
|
|
184
|
+
const hoursOpenedPastMidnight = getHoursOpenedPastMidnight(data, dateStr);
|
|
185
|
+
if (duurReservatie > defaultDuurReservatie && guestCounts && Object.keys(guestCounts).length > 0) {
|
|
186
|
+
const existingSlots = Object.keys(guestCounts).map(parseTime);
|
|
187
|
+
const maxExistingSlot = Math.max(...existingSlots);
|
|
188
|
+
const extraMinutes = duurReservatie - defaultDuurReservatie;
|
|
189
|
+
const extendedEnd = maxExistingSlot + extraMinutes;
|
|
190
|
+
|
|
191
|
+
// Find the last meal's maxCapacity for use with spillover slots
|
|
192
|
+
const lastMealMaxCap = getMaxCapacityForTime(data, dateStr, maxExistingSlot);
|
|
193
|
+
|
|
194
|
+
for (let t = maxExistingSlot + intervalReservatie; t <= extendedEnd; t += intervalReservatie) {
|
|
195
|
+
const timeStr = minutesToTimeStr(t);
|
|
196
|
+
if (!(timeStr in guestCounts)) {
|
|
197
|
+
if (t >= 1440) {
|
|
198
|
+
// Cross-midnight slot: use hoursOpenedPastMidnight limit and last meal's capacity
|
|
199
|
+
const spilloverMinutes = t - 1440;
|
|
200
|
+
if (hoursOpenedPastMidnight > 0 && spilloverMinutes < hoursOpenedPastMidnight * 60) {
|
|
201
|
+
const gc = getGuestCountAtHour(data, reservations, minutesToTimeStr(spilloverMinutes), getNextDateStr(dateStr));
|
|
202
|
+
if (lastMealMaxCap > 0) {
|
|
203
|
+
guestCounts[timeStr] = lastMealMaxCap - gc;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Same-day extended slot
|
|
208
|
+
const gc = getGuestCountAtHour(data, reservations, timeStr, dateStr);
|
|
209
|
+
const maxCap = getMaxCapacityForTime(data, dateStr, t);
|
|
210
|
+
if (maxCap > 0) {
|
|
211
|
+
guestCounts[timeStr] = maxCap - gc;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const availableTimeblocks = {};
|
|
219
|
+
|
|
220
|
+
// Handle shifts first
|
|
221
|
+
const mealTypesWithShifts = getMealTypesWithShifts(data, dateStr);
|
|
222
|
+
if (mealTypesWithShifts.length > 0 && shiftsInfo && shiftsInfo.length > 0) {
|
|
223
|
+
for (const shift of shiftsInfo) {
|
|
224
|
+
const { time, availableSeats } = shift;
|
|
225
|
+
if (availableSeats >= guests && fitsWithinMeal(data, dateStr, time, duurReservatie)) {
|
|
226
|
+
// Check if time matches giftcard requirement
|
|
227
|
+
if (timeHasGiftcard(data, dateStr, time, giftcard)) {
|
|
228
|
+
availableTimeblocks[time] = { name: time };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Handle non-shift times
|
|
235
|
+
if (guestCounts && Object.keys(guestCounts).length > 0) {
|
|
236
|
+
const timeSlots = Object.keys(guestCounts).sort((a, b) => parseTime(a) - parseTime(b));
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < timeSlots.length; i++) {
|
|
239
|
+
// Check capacity for the first slot
|
|
240
|
+
if (guestCounts[timeSlots[i]] < guests) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const startMinutes = parseTime(timeSlots[i]);
|
|
245
|
+
let consecutiveSlotsAvailable = true;
|
|
246
|
+
|
|
247
|
+
for (let j = 1; j < slotsNeeded; j++) {
|
|
248
|
+
const neededMinutes = startMinutes + j * intervalReservatie;
|
|
249
|
+
|
|
250
|
+
// First try same-day check from guestCounts.
|
|
251
|
+
// This handles both regular times AND extended meal times (e.g. "24:00", "24:30")
|
|
252
|
+
// produced by addDuurReservatieToEndTime. Without this, regular restaurants would
|
|
253
|
+
// break near midnight because the cross-midnight branch rejects when hoursOpenedPastMidnight=0.
|
|
254
|
+
const slotIndex = i + j;
|
|
255
|
+
if (slotIndex < timeSlots.length) {
|
|
256
|
+
const currentTimeSlot = timeSlots[slotIndex];
|
|
257
|
+
const currentTime = parseTime(currentTimeSlot);
|
|
258
|
+
if (currentTime === neededMinutes) {
|
|
259
|
+
if (guestCounts[currentTimeSlot] < guests) {
|
|
260
|
+
consecutiveSlotsAvailable = false;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
continue; // Slot available via guestCounts
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Slot not found in guestCounts — check cross-midnight if applicable
|
|
268
|
+
if (neededMinutes >= 1440) {
|
|
269
|
+
if (hoursOpenedPastMidnight <= 0) {
|
|
270
|
+
consecutiveSlotsAvailable = false;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
const spilloverMinutes = neededMinutes - 1440;
|
|
274
|
+
if (spilloverMinutes >= hoursOpenedPastMidnight * 60) {
|
|
275
|
+
consecutiveSlotsAvailable = false;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
// Use starting meal's capacity as limit for spillover period
|
|
279
|
+
const nextDateStr = getNextDateStr(dateStr);
|
|
280
|
+
const nextTimeStr = minutesToTimeStr(spilloverMinutes);
|
|
281
|
+
const nextDayGuestCount = getGuestCountAtHour(data, reservations, nextTimeStr, nextDateStr);
|
|
282
|
+
const startMealCap = getMaxCapacityForTime(data, dateStr, startMinutes);
|
|
283
|
+
if (startMealCap - nextDayGuestCount < guests) {
|
|
284
|
+
consecutiveSlotsAvailable = false;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
continue; // Cross-midnight slot OK
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Not in guestCounts and not cross-midnight — gap in availability
|
|
291
|
+
consecutiveSlotsAvailable = false;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If the reservation would end past midnight, enforce hoursOpenedPastMidnight limit
|
|
296
|
+
// Only applies when the setting is explicitly defined (bowling restaurants).
|
|
297
|
+
if (consecutiveSlotsAvailable && hasMidnightSetting(data, dateStr)) {
|
|
298
|
+
const endMinutes = startMinutes + duurReservatie;
|
|
299
|
+
if (endMinutes > 1440 + hoursOpenedPastMidnight * 60) {
|
|
300
|
+
consecutiveSlotsAvailable = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If all consecutive slots are available, check if the start time fits within a meal
|
|
305
|
+
if (consecutiveSlotsAvailable && fitsWithinMeal(data, dateStr, timeSlots[i], duurReservatie)) {
|
|
306
|
+
// Check if time matches giftcard requirement
|
|
307
|
+
if (timeHasGiftcard(data, dateStr, timeSlots[i], giftcard)) {
|
|
308
|
+
availableTimeblocks[timeSlots[i]] = { name: timeSlots[i] };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Filter out blocked time slots (skip for admin)
|
|
315
|
+
if (!isAdmin && blockedSlots && blockedSlots.length > 0) {
|
|
316
|
+
// Check if waitlist is enabled in settings (default to true if not defined)
|
|
317
|
+
// The setting key is 'waitlistEnabled' in 'general-settings'
|
|
318
|
+
const settings = data['general-settings'] || {};
|
|
319
|
+
const waitlistEnabled = settings.waitlistEnabled !== undefined ? settings.waitlistEnabled === true : true;
|
|
320
|
+
|
|
321
|
+
for (const blockedSlot of blockedSlots) {
|
|
322
|
+
// Check if the blocked slot matches the current date
|
|
323
|
+
if (blockedSlot.date === dateStr && blockedSlot.time) {
|
|
324
|
+
if (waitlistEnabled) {
|
|
325
|
+
// If waitlist is enabled, mark it as a waitlist item instead of deleting
|
|
326
|
+
// We force it into the list even if it wasn't there (e.g. if it was full)
|
|
327
|
+
// because a manual block + waitlist implies we want to capture interest for this specific blocked time.
|
|
328
|
+
availableTimeblocks[blockedSlot.time] = {
|
|
329
|
+
name: blockedSlot.time,
|
|
330
|
+
isWaitlist: true
|
|
331
|
+
};
|
|
332
|
+
} else {
|
|
333
|
+
// Default behavior: Remove the blocked time from available timeblocks
|
|
334
|
+
delete availableTimeblocks[blockedSlot.time];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return availableTimeblocks;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
timeblocksAvailable,
|
|
183
345
|
};
|