@happychef/algorithm 1.4.0 → 1.4.1
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/BRANCH_PROTECTION_SETUP.md +167 -167
- package/README.md +144 -144
- package/__tests__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +175 -179
- package/__tests__/isTimeAvailable.test.js +168 -174
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +0 -62
- package/bundle_entry.js +100 -0
- package/getAvailableTimeblocks.js +12 -0
- package/index.js +1 -7
- package/jest.config.js +23 -23
- package/moment-timezone-shim.js +179 -0
- package/nul +0 -0
- package/package.json +1 -1
- package/processing/timeblocksAvailable.js +26 -183
- package/reservation_data/counter.js +60 -67
- package/restaurant_data/openinghours.js +23 -0
- package/simulateTableAssignment.js +2 -108
- package/tableHelpers.js +5 -2
- package/test-detailed-filter.js +100 -100
- package/test-lunch-debug.js +110 -110
- package/test-max-arrivals-filter.js +79 -79
- package/test-timezone-debug.js +47 -47
- package/.claude/settings.local.json +0 -16
- package/__tests__/crossMidnight.test.js +0 -63
- package/__tests__/crossMidnightTimeblocks.test.js +0 -312
- package/__tests__/edgeCases.test.js +0 -271
- package/changes/2026/February/PR16_add__.md +0 -20
- package/changes/2026/February/PR16_add_getDateClosingReasons.md +0 -31
- package/dateHelpers.js +0 -31
- package/getDateClosingReasons.js +0 -193
|
@@ -150,6 +150,18 @@ function getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlot
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// For bowling venues, cap dinner time slots at 24:00 max
|
|
154
|
+
const isBowling = data?.account_type === 'bowling' || data?.accountType === 'bowling' || data?.['general-settings']?.account_type === 'bowling';
|
|
155
|
+
if (isBowling) {
|
|
156
|
+
for (const time in availableTimeblocks) {
|
|
157
|
+
const [h, m] = time.split(':').map(Number);
|
|
158
|
+
const timeMinutes = h * 60 + m;
|
|
159
|
+
if (timeMinutes > 1440) { // > 24:00
|
|
160
|
+
delete availableTimeblocks[time];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
153
165
|
return availableTimeblocks;
|
|
154
166
|
}
|
|
155
167
|
|
package/index.js
CHANGED
|
@@ -15,10 +15,6 @@ const restaurant_data = {
|
|
|
15
15
|
...require("./restaurant_data/openinghours")
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const dateHelpers = {
|
|
19
|
-
...require("./dateHelpers")
|
|
20
|
-
};
|
|
21
|
-
|
|
22
18
|
const test = {
|
|
23
19
|
...require("./assignTables"),
|
|
24
20
|
...require("./getAvailableTimeblocks"),
|
|
@@ -28,8 +24,7 @@ const test = {
|
|
|
28
24
|
...require("./simulateTableAssignment"),
|
|
29
25
|
...require("./isDateAvailableWithTableCheck"),
|
|
30
26
|
...require("./tableHelpers"),
|
|
31
|
-
...require("./test")
|
|
32
|
-
...require("./getDateClosingReasons")
|
|
27
|
+
...require("./test")
|
|
33
28
|
};
|
|
34
29
|
|
|
35
30
|
const filters = {
|
|
@@ -42,7 +37,6 @@ module.exports = {
|
|
|
42
37
|
...processing,
|
|
43
38
|
...reservation_data,
|
|
44
39
|
...restaurant_data,
|
|
45
|
-
...dateHelpers,
|
|
46
40
|
...test,
|
|
47
41
|
...filters
|
|
48
42
|
};
|
package/jest.config.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
testEnvironment: 'node',
|
|
3
|
-
coverageDirectory: 'coverage',
|
|
4
|
-
collectCoverageFrom: [
|
|
5
|
-
'**/*.js',
|
|
6
|
-
'!**/node_modules/**',
|
|
7
|
-
'!**/coverage/**',
|
|
8
|
-
'!jest.config.js',
|
|
9
|
-
'!test.js',
|
|
10
|
-
'!test-*.js',
|
|
11
|
-
'!**/__tests__/**'
|
|
12
|
-
],
|
|
13
|
-
testMatch: [
|
|
14
|
-
'**/__tests__/**/*.test.js'
|
|
15
|
-
],
|
|
16
|
-
testPathIgnorePatterns: [
|
|
17
|
-
'/node_modules/',
|
|
18
|
-
'/test\\.js$',
|
|
19
|
-
'/test-.*\\.js$'
|
|
20
|
-
],
|
|
21
|
-
verbose: true,
|
|
22
|
-
testTimeout: 10000
|
|
23
|
-
};
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
coverageDirectory: 'coverage',
|
|
4
|
+
collectCoverageFrom: [
|
|
5
|
+
'**/*.js',
|
|
6
|
+
'!**/node_modules/**',
|
|
7
|
+
'!**/coverage/**',
|
|
8
|
+
'!jest.config.js',
|
|
9
|
+
'!test.js',
|
|
10
|
+
'!test-*.js',
|
|
11
|
+
'!**/__tests__/**'
|
|
12
|
+
],
|
|
13
|
+
testMatch: [
|
|
14
|
+
'**/__tests__/**/*.test.js'
|
|
15
|
+
],
|
|
16
|
+
testPathIgnorePatterns: [
|
|
17
|
+
'/node_modules/',
|
|
18
|
+
'/test\\.js$',
|
|
19
|
+
'/test-.*\\.js$'
|
|
20
|
+
],
|
|
21
|
+
verbose: true,
|
|
22
|
+
testTimeout: 10000
|
|
23
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Minimal moment-timezone shim for Europe/Brussels only
|
|
2
|
+
// Replaces the full ~500KB moment-timezone library
|
|
3
|
+
// Implements only the subset used by @happychef/algorithm
|
|
4
|
+
|
|
5
|
+
const TIMEZONE = 'Europe/Brussels';
|
|
6
|
+
|
|
7
|
+
// EU DST rules: CET (UTC+1) / CEST (UTC+2)
|
|
8
|
+
// CEST starts last Sunday of March at 02:00 UTC
|
|
9
|
+
// CET starts last Sunday of October at 03:00 UTC
|
|
10
|
+
function getBrusselsOffset(date) {
|
|
11
|
+
const year = date.getUTCFullYear();
|
|
12
|
+
const month = date.getUTCMonth(); // 0-indexed
|
|
13
|
+
|
|
14
|
+
// Find last Sunday of March
|
|
15
|
+
const marchLast = new Date(Date.UTC(year, 2, 31));
|
|
16
|
+
const marchSunday = 31 - marchLast.getUTCDay();
|
|
17
|
+
const dstStart = Date.UTC(year, 2, marchSunday, 1, 0, 0); // 02:00 CET = 01:00 UTC
|
|
18
|
+
|
|
19
|
+
// Find last Sunday of October
|
|
20
|
+
const octLast = new Date(Date.UTC(year, 9, 31));
|
|
21
|
+
const octSunday = 31 - octLast.getUTCDay();
|
|
22
|
+
const dstEnd = Date.UTC(year, 9, octSunday, 1, 0, 0); // 03:00 CEST = 01:00 UTC
|
|
23
|
+
|
|
24
|
+
const ts = date.getTime();
|
|
25
|
+
if (ts >= dstStart && ts < dstEnd) {
|
|
26
|
+
return 2; // CEST (UTC+2)
|
|
27
|
+
}
|
|
28
|
+
return 1; // CET (UTC+1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toBrusselsDate(date) {
|
|
32
|
+
const offset = getBrusselsOffset(date);
|
|
33
|
+
return new Date(date.getTime() + offset * 60 * 60 * 1000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createMomentObject(date, offset) {
|
|
37
|
+
const brusselsDate = new Date(date.getTime() + offset * 60 * 60 * 1000);
|
|
38
|
+
|
|
39
|
+
const obj = {
|
|
40
|
+
_date: date,
|
|
41
|
+
_brusselsDate: brusselsDate,
|
|
42
|
+
_offset: offset,
|
|
43
|
+
_valid: true,
|
|
44
|
+
|
|
45
|
+
isValid() {
|
|
46
|
+
return this._valid;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
day() {
|
|
50
|
+
return this._brusselsDate.getUTCDay();
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
hours() {
|
|
54
|
+
return this._brusselsDate.getUTCHours();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
minutes() {
|
|
58
|
+
return this._brusselsDate.getUTCMinutes();
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
format(fmt) {
|
|
62
|
+
if (fmt === 'YYYY-MM-DD') {
|
|
63
|
+
const y = this._brusselsDate.getUTCFullYear();
|
|
64
|
+
const m = String(this._brusselsDate.getUTCMonth() + 1).padStart(2, '0');
|
|
65
|
+
const d = String(this._brusselsDate.getUTCDate()).padStart(2, '0');
|
|
66
|
+
return `${y}-${m}-${d}`;
|
|
67
|
+
}
|
|
68
|
+
if (fmt === 'HH:mm') {
|
|
69
|
+
const h = String(this._brusselsDate.getUTCHours()).padStart(2, '0');
|
|
70
|
+
const min = String(this._brusselsDate.getUTCMinutes()).padStart(2, '0');
|
|
71
|
+
return `${h}:${min}`;
|
|
72
|
+
}
|
|
73
|
+
return this._brusselsDate.toISOString();
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
startOf(unit) {
|
|
77
|
+
if (unit === 'day') {
|
|
78
|
+
const d = new Date(Date.UTC(
|
|
79
|
+
this._brusselsDate.getUTCFullYear(),
|
|
80
|
+
this._brusselsDate.getUTCMonth(),
|
|
81
|
+
this._brusselsDate.getUTCDate(),
|
|
82
|
+
0, 0, 0, 0
|
|
83
|
+
));
|
|
84
|
+
// Adjust back from Brussels to UTC
|
|
85
|
+
const utcDate = new Date(d.getTime() - this._offset * 60 * 60 * 1000);
|
|
86
|
+
return createMomentObject(utcDate, this._offset);
|
|
87
|
+
}
|
|
88
|
+
return this;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
endOf(unit) {
|
|
92
|
+
if (unit === 'day') {
|
|
93
|
+
const d = new Date(Date.UTC(
|
|
94
|
+
this._brusselsDate.getUTCFullYear(),
|
|
95
|
+
this._brusselsDate.getUTCMonth(),
|
|
96
|
+
this._brusselsDate.getUTCDate(),
|
|
97
|
+
23, 59, 59, 999
|
|
98
|
+
));
|
|
99
|
+
const utcDate = new Date(d.getTime() - this._offset * 60 * 60 * 1000);
|
|
100
|
+
return createMomentObject(utcDate, this._offset);
|
|
101
|
+
}
|
|
102
|
+
return this;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
isBefore(other) {
|
|
106
|
+
return this._date.getTime() < other._date.getTime();
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
isAfter(other) {
|
|
110
|
+
return this._date.getTime() > other._date.getTime();
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
tz() {
|
|
114
|
+
// Already in Brussels timezone, return self
|
|
115
|
+
return this;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return obj;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseDateStr(dateStr, format) {
|
|
123
|
+
if (format === 'YYYY-MM-DD') {
|
|
124
|
+
const parts = dateStr.split('-');
|
|
125
|
+
if (parts.length !== 3) return null;
|
|
126
|
+
const year = parseInt(parts[0], 10);
|
|
127
|
+
const month = parseInt(parts[1], 10) - 1;
|
|
128
|
+
const day = parseInt(parts[2], 10);
|
|
129
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) return null;
|
|
130
|
+
if (month < 0 || month > 11 || day < 1 || day > 31) return null;
|
|
131
|
+
// Create date at midnight Brussels time -> convert to UTC
|
|
132
|
+
const utcDate = new Date(Date.UTC(year, month, day, 0, 0, 0));
|
|
133
|
+
return utcDate;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Main moment function
|
|
139
|
+
function moment(dateStr, format, timezone) {
|
|
140
|
+
if (dateStr === undefined || dateStr === null) {
|
|
141
|
+
// moment() - current time
|
|
142
|
+
const now = new Date();
|
|
143
|
+
const offset = getBrusselsOffset(now);
|
|
144
|
+
const obj = createMomentObject(now, offset);
|
|
145
|
+
obj.tz = function() { return obj; };
|
|
146
|
+
return obj;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof dateStr === 'string') {
|
|
150
|
+
const date = parseDateStr(dateStr, format || 'YYYY-MM-DD');
|
|
151
|
+
if (!date) {
|
|
152
|
+
return { _valid: false, isValid() { return false; }, day() { return 0; }, tz() { return this; } };
|
|
153
|
+
}
|
|
154
|
+
const offset = getBrusselsOffset(date);
|
|
155
|
+
return createMomentObject(date, offset);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fallback
|
|
159
|
+
const now = new Date();
|
|
160
|
+
const offset = getBrusselsOffset(now);
|
|
161
|
+
return createMomentObject(now, offset);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// moment.tz(dateStr, format, timezone) or moment().tz(timezone)
|
|
165
|
+
moment.tz = function(dateStr, format, timezone) {
|
|
166
|
+
if (typeof dateStr === 'string' && typeof format === 'string' && typeof timezone === 'string') {
|
|
167
|
+
// moment.tz(dateStr, format, timezone)
|
|
168
|
+
return moment(dateStr, format, timezone);
|
|
169
|
+
}
|
|
170
|
+
if (typeof dateStr === 'string' && typeof format === 'string' && timezone === undefined) {
|
|
171
|
+
// moment.tz(dateStr, timezone) - dateStr is actual date, format is timezone
|
|
172
|
+
// This pattern: moment().tz('Europe/Brussels')
|
|
173
|
+
return moment(dateStr, 'YYYY-MM-DD');
|
|
174
|
+
}
|
|
175
|
+
// Fallback: current time in Brussels
|
|
176
|
+
return moment();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
module.exports = moment;
|
package/nul
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
const { getDailyGuestCounts } = require('./dailyGuestCounts');
|
|
2
2
|
const { getMealTypesWithShifts } = require('./mealTypeCount');
|
|
3
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');
|
|
4
|
+
const { getMealTypeByTime, parseTime: parseTimeOH, daysOfWeekEnglish } = require('../restaurant_data/openinghours');
|
|
7
5
|
const moment = require('moment-timezone');
|
|
8
6
|
|
|
9
7
|
/**
|
|
@@ -41,38 +39,6 @@ function getDuurReservatie(data) {
|
|
|
41
39
|
return duurReservatie;
|
|
42
40
|
}
|
|
43
41
|
|
|
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
42
|
/**
|
|
77
43
|
* Checks if a time slot belongs to a meal that has the selected giftcard enabled.
|
|
78
44
|
*/
|
|
@@ -104,73 +70,32 @@ function timeHasGiftcard(data, dateStr, timeStr, giftcard) {
|
|
|
104
70
|
}
|
|
105
71
|
|
|
106
72
|
/**
|
|
107
|
-
* Determines if a given start time fits within
|
|
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).
|
|
73
|
+
* Determines if a given start time plus duurReservatie fits within the meal timeframe.
|
|
113
74
|
*/
|
|
114
75
|
function fitsWithinMeal(data, dateStr, startTimeStr, duurReservatie) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Original production logic for breakfast/lunch (always, regardless of bowling)
|
|
76
|
+
// Determine the meal type based on the start time
|
|
118
77
|
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
78
|
if (!mealType) return false;
|
|
79
|
+
|
|
80
|
+
// Get the meal data (with exceptions applied)
|
|
138
81
|
const mealData = getDataByDateAndMealWithExceptions(data, dateStr, mealType);
|
|
139
82
|
if (!mealData) return false;
|
|
83
|
+
|
|
140
84
|
const mealStartTime = parseTime(mealData.startTime);
|
|
141
|
-
const mealEndTime = parseTime(mealData.endTime);
|
|
142
|
-
|
|
143
|
-
}
|
|
85
|
+
const mealEndTime = parseTime(mealData.endTime); // Already includes restaurant's duurReservatie
|
|
86
|
+
const startTime = parseTime(startTimeStr);
|
|
144
87
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const m = minutes % 60;
|
|
151
|
-
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
152
|
-
}
|
|
88
|
+
// mealEndTime includes the restaurant's base duurReservatie (added by addDuurReservatieToEndTime).
|
|
89
|
+
// Recover the actual closing time so bookings can't START after the venue closes,
|
|
90
|
+
// even when the user picks a shorter duration than the restaurant's default.
|
|
91
|
+
const restaurantDuur = getDuurReservatie(data);
|
|
92
|
+
const actualMealEnd = mealEndTime - restaurantDuur;
|
|
153
93
|
|
|
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;
|
|
94
|
+
return startTime >= mealStartTime && startTime <= actualMealEnd && startTime + duurReservatie <= mealEndTime;
|
|
169
95
|
}
|
|
170
96
|
|
|
171
97
|
function timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false, duration = null) {
|
|
172
|
-
const
|
|
173
|
-
const duurReservatie = duration && duration > 0 ? duration : defaultDuurReservatie;
|
|
98
|
+
const duurReservatie = duration && duration > 0 ? duration : getDuurReservatie(data);
|
|
174
99
|
const intervalReservatie = getInterval(data);
|
|
175
100
|
|
|
176
101
|
// Slots needed
|
|
@@ -179,42 +104,6 @@ function timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots =
|
|
|
179
104
|
// Get guest counts and shifts info
|
|
180
105
|
const { guestCounts, shiftsInfo } = getDailyGuestCounts(data, dateStr, reservations);
|
|
181
106
|
|
|
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
107
|
const availableTimeblocks = {};
|
|
219
108
|
|
|
220
109
|
// Handle shifts first
|
|
@@ -235,73 +124,27 @@ function timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots =
|
|
|
235
124
|
if (guestCounts && Object.keys(guestCounts).length > 0) {
|
|
236
125
|
const timeSlots = Object.keys(guestCounts).sort((a, b) => parseTime(a) - parseTime(b));
|
|
237
126
|
|
|
238
|
-
for (let i = 0; i
|
|
239
|
-
// Check capacity for
|
|
127
|
+
for (let i = 0; i <= timeSlots.length - slotsNeeded; i++) {
|
|
128
|
+
// Check capacity for all needed slots
|
|
129
|
+
let consecutiveSlotsAvailable = true;
|
|
240
130
|
if (guestCounts[timeSlots[i]] < guests) {
|
|
241
131
|
continue;
|
|
242
132
|
}
|
|
243
133
|
|
|
244
|
-
|
|
245
|
-
let consecutiveSlotsAvailable = true;
|
|
246
|
-
|
|
134
|
+
let previousTime = parseTime(timeSlots[i]);
|
|
247
135
|
for (let j = 1; j < slotsNeeded; j++) {
|
|
248
|
-
const
|
|
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
|
-
}
|
|
136
|
+
const currentTimeSlot = timeSlots[i + j];
|
|
137
|
+
const currentTime = parseTime(currentTimeSlot);
|
|
294
138
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (consecutiveSlotsAvailable && hasMidnightSetting(data, dateStr)) {
|
|
298
|
-
const endMinutes = startMinutes + duurReservatie;
|
|
299
|
-
if (endMinutes > 1440 + hoursOpenedPastMidnight * 60) {
|
|
139
|
+
// Check interval and capacity
|
|
140
|
+
if ((currentTime - previousTime) !== intervalReservatie || guestCounts[currentTimeSlot] < guests) {
|
|
300
141
|
consecutiveSlotsAvailable = false;
|
|
142
|
+
break;
|
|
301
143
|
}
|
|
144
|
+
previousTime = currentTime;
|
|
302
145
|
}
|
|
303
146
|
|
|
304
|
-
// If all consecutive slots are available, check if the
|
|
147
|
+
// If all consecutive slots are available, check if the full duration fits
|
|
305
148
|
if (consecutiveSlotsAvailable && fitsWithinMeal(data, dateStr, timeSlots[i], duurReservatie)) {
|
|
306
149
|
// Check if time matches giftcard requirement
|
|
307
150
|
if (timeHasGiftcard(data, dateStr, timeSlots[i], giftcard)) {
|