@happychef/algorithm 1.2.11 → 1.2.12
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/.github/workflows/ci-cd.yml +234 -234
- 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__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +175 -175
- package/__tests__/isTimeAvailable.test.js +168 -168
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +424 -424
- 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/January/PR8_add__change.md +39 -0
- package/changes/2026/January/PR9_add__change.md +20 -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/grouping.js +162 -162
- package/index.js +42 -42
- package/isDateAvailable.js +80 -80
- package/isDateAvailableWithTableCheck.js +171 -171
- package/isTimeAvailable.js +25 -25
- 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 +167 -167
- package/reservation_data/counter.js +64 -64
- package/restaurant_data/exceptions.js +149 -149
- package/restaurant_data/openinghours.js +123 -123
- package/simulateTableAssignment.js +709 -699
- package/tableHelpers.js +178 -178
- 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,222 +1,222 @@
|
|
|
1
|
-
// src/Pages/NewReservation/StepOne/algorithm/filters/maxGroupsFilter.js
|
|
2
|
-
// Import the dedicated getMealType function instead of relying on the openinghours version
|
|
3
|
-
// This ensures consistency in meal type determination
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Determines the meal type (breakfast, lunch, dinner) based on the time.
|
|
7
|
-
* @param {string} time - Time in HH:MM format
|
|
8
|
-
* @returns {string|null} - Meal type or null if outside meal times
|
|
9
|
-
*/
|
|
10
|
-
function getMealType(time) {
|
|
11
|
-
const hour = parseInt(time.split(':')[0], 10);
|
|
12
|
-
|
|
13
|
-
if (hour >= 4 && hour < 11) return 'breakfast';
|
|
14
|
-
if (hour >= 11 && hour < 16) return 'lunch';
|
|
15
|
-
if (hour >= 16 && hour < 23) return 'dinner';
|
|
16
|
-
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// --- Add console logs for debugging ---
|
|
21
|
-
// Set DEBUG_MAX_GROUPS to true in your development environment to enable detailed logs
|
|
22
|
-
const DEBUG_MAX_GROUPS = true; // Set to true to enable detailed logs for this filter
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Helper function for conditional logging.
|
|
26
|
-
* @param {string} message - The log message.
|
|
27
|
-
* @param {...any} args - Additional arguments to log.
|
|
28
|
-
*/
|
|
29
|
-
function logDebug(message, ...args) {
|
|
30
|
-
if (DEBUG_MAX_GROUPS) {
|
|
31
|
-
// Using console.debug might allow filtering in browser dev tools,
|
|
32
|
-
// but console.log ensures visibility.
|
|
33
|
-
console.log(`[MaxGroupsFilter] ${message}`, ...args);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// --- End log setup ---
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extracts a number value from various data formats (e.g., MongoDB NumberInt).
|
|
40
|
-
* Handles null, undefined, object with $numberInt, number, and string representations.
|
|
41
|
-
* Returns null if the value is not a valid integer representation.
|
|
42
|
-
* @param {*} value - Value from data object.
|
|
43
|
-
* @returns {number|null} - Parsed integer or null if invalid.
|
|
44
|
-
*/
|
|
45
|
-
function extractNumber(value) {
|
|
46
|
-
if (value === null || value === undefined) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Handle MongoDB NumberInt format: { "$numberInt": "1" }
|
|
51
|
-
if (typeof value === 'object' && value !== null && '$numberInt' in value) {
|
|
52
|
-
const parsed = parseInt(value.$numberInt, 10);
|
|
53
|
-
return isNaN(parsed) ? null : parsed;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Handle regular number type
|
|
57
|
-
if (typeof value === 'number') {
|
|
58
|
-
// Ensure it's an integer, not float/Infinity/NaN
|
|
59
|
-
return Number.isInteger(value) ? value : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Handle string number: "1"
|
|
63
|
-
if (typeof value === 'string') {
|
|
64
|
-
const parsed = parseInt(value, 10);
|
|
65
|
-
// Ensure the string was fully parsed as a number and is finite
|
|
66
|
-
return isNaN(parsed) ? null : parsed;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Not a recognized number format
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Filters available timeblocks based on maximum group size limits for each meal period.
|
|
75
|
-
* It checks if adding a new reservation of 'guests' count would exceed the configured
|
|
76
|
-
* maximum number of groups allowed for various size thresholds within the specific meal period.
|
|
77
|
-
*
|
|
78
|
-
* @param {Object} restaurantData - The main restaurant data object, expected to contain keys like 'max-groups-breakfast', 'max-groups-lunch', etc.
|
|
79
|
-
* @param {string} dateStr - The date string in "YYYY-MM-DD" format.
|
|
80
|
-
* @param {Object} timeblocks - An object of available timeblocks { "HH:MM": timeData } that have passed previous filters.
|
|
81
|
-
* @param {Array} reservations - An array of existing reservation objects for potentially multiple dates.
|
|
82
|
-
* @param {number|string} guests - The number of guests for the new reservation being considered.
|
|
83
|
-
* @returns {Object} - A new object containing only the timeblocks that satisfy the max group limits.
|
|
84
|
-
*/
|
|
85
|
-
function filterTimeblocksByMaxGroups(restaurantData, dateStr, timeblocks, reservations, guests) {
|
|
86
|
-
logDebug(`--- Running MaxGroupsFilter for Date: ${dateStr}, Guests: ${guests} ---`);
|
|
87
|
-
// Log the top-level keys related to max-groups to see what's actually present
|
|
88
|
-
const groupKeys = Object.keys(restaurantData).filter(k => k.startsWith('max-groups-'));
|
|
89
|
-
logDebug("Received restaurantData max-group keys:", groupKeys.length > 0 ? groupKeys : 'None found');
|
|
90
|
-
|
|
91
|
-
const filteredBlocks = {}; // Initialize the object to store timeblocks that pass the filter
|
|
92
|
-
const guestsNumber = parseInt(guests, 10); // Ensure guest count is a number
|
|
93
|
-
|
|
94
|
-
// Validate guest input
|
|
95
|
-
if (isNaN(guestsNumber) || guestsNumber <= 0) {
|
|
96
|
-
logDebug(`Invalid guest count (${guests}). Returning all ${Object.keys(timeblocks).length} input blocks unfiltered.`);
|
|
97
|
-
return timeblocks; // Return original blocks if guest count is invalid
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Pre-filter reservations to only those on the specific date for efficiency
|
|
101
|
-
const reservationsOnDate = reservations.filter(r => r.date === dateStr);
|
|
102
|
-
logDebug(`Relevant reservations on ${dateStr}: ${reservationsOnDate.length}`);
|
|
103
|
-
logDebug(`Input timeblocks count: ${Object.keys(timeblocks).length}`);
|
|
104
|
-
|
|
105
|
-
// Group reservations by meal type using our consistent getMealType function
|
|
106
|
-
const reservationsByMealType = {};
|
|
107
|
-
reservationsOnDate.forEach(r => {
|
|
108
|
-
const mealType = getMealType(r.time);
|
|
109
|
-
if (!mealType) return; // Skip if no meal type determined
|
|
110
|
-
|
|
111
|
-
if (!reservationsByMealType[mealType]) {
|
|
112
|
-
reservationsByMealType[mealType] = [];
|
|
113
|
-
}
|
|
114
|
-
reservationsByMealType[mealType].push(r);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
logDebug('Reservations grouped by meal type:', reservationsByMealType);
|
|
118
|
-
|
|
119
|
-
// Iterate through each timeblock that needs checking
|
|
120
|
-
for (const [time, timeData] of Object.entries(timeblocks)) {
|
|
121
|
-
const mealType = getMealType(time); // Determine meal period (e.g., 'breakfast') using our consistent function
|
|
122
|
-
logDebug(`\nChecking Time: ${time} -> Determined Meal: ${mealType}`);
|
|
123
|
-
|
|
124
|
-
// If the time doesn't fall into a recognized meal period, the filter doesn't apply
|
|
125
|
-
if (!mealType) {
|
|
126
|
-
logDebug(` Time ${time} has no associated meal type. Keeping block.`);
|
|
127
|
-
filteredBlocks[time] = timeData;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Construct the key for the settings specific to this meal (e.g., 'max-groups-breakfast')
|
|
132
|
-
const maxGroupSettingsKey = `max-groups-${mealType}`;
|
|
133
|
-
// Retrieve the settings object for this meal from the restaurant data
|
|
134
|
-
const maxGroupSettings = restaurantData[maxGroupSettingsKey];
|
|
135
|
-
|
|
136
|
-
logDebug(` Looking for settings using key: "${maxGroupSettingsKey}"`);
|
|
137
|
-
// If no settings object exists for this meal type, the filter doesn't apply to this timeblock
|
|
138
|
-
if (!maxGroupSettings || typeof maxGroupSettings !== 'object') {
|
|
139
|
-
logDebug(` No settings found for key "${maxGroupSettingsKey}". Filter doesn't apply. Keeping block.`);
|
|
140
|
-
filteredBlocks[time] = timeData;
|
|
141
|
-
continue; // Move to the next timeblock
|
|
142
|
-
}
|
|
143
|
-
// Log the actual settings found for this meal to verify correctness
|
|
144
|
-
logDebug(` Found settings for ${mealType}:`, JSON.stringify(maxGroupSettings));
|
|
145
|
-
|
|
146
|
-
// --- Settings exist, now apply the filter logic ---
|
|
147
|
-
let isTimeAllowed = true; // Assume the timeblock is allowed until a limit is violated
|
|
148
|
-
|
|
149
|
-
// Extract and sort the numeric group size limits defined in the settings (e.g., "6", "7" -> [6, 7])
|
|
150
|
-
const limitSizes = Object.keys(maxGroupSettings)
|
|
151
|
-
.map(key => parseInt(key, 10)) // Convert keys to numbers
|
|
152
|
-
.filter(num => !isNaN(num) && num > 0) // Keep only positive integers
|
|
153
|
-
.sort((a, b) => a - b); // Sort numerically (e.g., check 6 before 7)
|
|
154
|
-
logDebug(` Defined numeric limit sizes to check for ${mealType}: [${limitSizes.join(', ')}]`);
|
|
155
|
-
|
|
156
|
-
// Check against each defined limit size threshold
|
|
157
|
-
for (const limitSize of limitSizes) {
|
|
158
|
-
const limitSizeStr = String(limitSize); // Original string key (e.g., "6")
|
|
159
|
-
logDebug(` Checking Limit Condition: Groups of size >= ${limitSize}`);
|
|
160
|
-
|
|
161
|
-
// Does the new booking we're considering (guestsNumber) meet or exceed this limit threshold?
|
|
162
|
-
// Example: If limitSize is 6, and guestsNumber is 7, this limit applies. If guestsNumber is 5, it doesn't.
|
|
163
|
-
if (guestsNumber >= limitSize) {
|
|
164
|
-
logDebug(` Applicable: Booking guests (${guestsNumber}) >= Limit threshold (${limitSize})`);
|
|
165
|
-
|
|
166
|
-
// Extract the maximum number of groups allowed for this threshold
|
|
167
|
-
const maxAllowedCount = extractNumber(maxGroupSettings[limitSizeStr]);
|
|
168
|
-
logDebug(` Max groups allowed for >=${limitSize} is: ${maxAllowedCount} (raw setting: ${JSON.stringify(maxGroupSettings[limitSizeStr])})`);
|
|
169
|
-
|
|
170
|
-
// If the setting for this limit size is invalid (null or negative), treat it as 'no limit'
|
|
171
|
-
if (maxAllowedCount === null || maxAllowedCount < 0) {
|
|
172
|
-
logDebug(` Skipping check: Invalid or non-positive max count defined for size ${limitSize}.`);
|
|
173
|
-
continue; // Skip to the next limitSize
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Get reservations for this meal type - using only reservations from the same meal type
|
|
177
|
-
const reservationsForMealType = reservationsByMealType[mealType] || [];
|
|
178
|
-
|
|
179
|
-
// Count how many existing reservations for this meal type meet or exceed this limit size
|
|
180
|
-
const existingMatchingGroupsCount = reservationsForMealType.filter(reservation => {
|
|
181
|
-
const reservationGuests = parseInt(reservation.guests, 10);
|
|
182
|
-
return !isNaN(reservationGuests) && reservationGuests >= limitSize;
|
|
183
|
-
}).length;
|
|
184
|
-
|
|
185
|
-
logDebug(` Count of existing groups >=${limitSize} found in ${mealType}: ${existingMatchingGroupsCount}`);
|
|
186
|
-
|
|
187
|
-
// THE CORE LOGIC: Check if adding the new group would exceed the max allowed count
|
|
188
|
-
if (existingMatchingGroupsCount + 1 > maxAllowedCount) {
|
|
189
|
-
logDebug(` *** VIOLATION DETECTED *** for limit >=${limitSize}.`);
|
|
190
|
-
logDebug(` (Existing Groups: ${existingMatchingGroupsCount} + New Group: 1 = ${existingMatchingGroupsCount + 1}, which is > Max Allowed: ${maxAllowedCount})`);
|
|
191
|
-
logDebug(` Blocking time ${time} due to this violation.`);
|
|
192
|
-
isTimeAllowed = false; // Mark the timeblock as blocked
|
|
193
|
-
break; // No need to check further limits for this timeblock; it has already failed.
|
|
194
|
-
} else {
|
|
195
|
-
// This specific limit check passed
|
|
196
|
-
logDebug(` OK: Limit for >=${limitSize} not exceeded. (Existing: ${existingMatchingGroupsCount} + New: 1 <= Max: ${maxAllowedCount})`);
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
// The new booking's guest count is smaller than the current limit threshold, so this rule doesn't restrict it.
|
|
200
|
-
logDebug(` Not Applicable: Booking guests (${guestsNumber}) < Limit threshold (${limitSize}). Skipping this check.`);
|
|
201
|
-
}
|
|
202
|
-
} // End of loop through different limitSizes (e.g., checking 6, then 7)
|
|
203
|
-
|
|
204
|
-
// After checking all applicable limits for the timeblock:
|
|
205
|
-
if (isTimeAllowed) {
|
|
206
|
-
logDebug(` ✅ Result for time ${time}: ALLOWED (passed all applicable checks)`);
|
|
207
|
-
filteredBlocks[time] = timeData; // Add it to the results
|
|
208
|
-
} else {
|
|
209
|
-
logDebug(` ❌ Result for time ${time}: BLOCKED (failed at least one limit check)`);
|
|
210
|
-
// Do not add it to filteredBlocks
|
|
211
|
-
}
|
|
212
|
-
} // End of loop through all input timeblocks
|
|
213
|
-
|
|
214
|
-
logDebug(`--- MaxGroupsFilter Finished. Output timeblocks count: ${Object.keys(filteredBlocks).length} ---`);
|
|
215
|
-
return filteredBlocks; // Return the timeblocks that passed all checks
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Export the function for use in other parts of the application
|
|
219
|
-
module.exports = {
|
|
220
|
-
filterTimeblocksByMaxGroups,
|
|
221
|
-
getMealType, // Export getMealType for potential use by other modules
|
|
1
|
+
// src/Pages/NewReservation/StepOne/algorithm/filters/maxGroupsFilter.js
|
|
2
|
+
// Import the dedicated getMealType function instead of relying on the openinghours version
|
|
3
|
+
// This ensures consistency in meal type determination
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determines the meal type (breakfast, lunch, dinner) based on the time.
|
|
7
|
+
* @param {string} time - Time in HH:MM format
|
|
8
|
+
* @returns {string|null} - Meal type or null if outside meal times
|
|
9
|
+
*/
|
|
10
|
+
function getMealType(time) {
|
|
11
|
+
const hour = parseInt(time.split(':')[0], 10);
|
|
12
|
+
|
|
13
|
+
if (hour >= 4 && hour < 11) return 'breakfast';
|
|
14
|
+
if (hour >= 11 && hour < 16) return 'lunch';
|
|
15
|
+
if (hour >= 16 && hour < 23) return 'dinner';
|
|
16
|
+
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Add console logs for debugging ---
|
|
21
|
+
// Set DEBUG_MAX_GROUPS to true in your development environment to enable detailed logs
|
|
22
|
+
const DEBUG_MAX_GROUPS = true; // Set to true to enable detailed logs for this filter
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper function for conditional logging.
|
|
26
|
+
* @param {string} message - The log message.
|
|
27
|
+
* @param {...any} args - Additional arguments to log.
|
|
28
|
+
*/
|
|
29
|
+
function logDebug(message, ...args) {
|
|
30
|
+
if (DEBUG_MAX_GROUPS) {
|
|
31
|
+
// Using console.debug might allow filtering in browser dev tools,
|
|
32
|
+
// but console.log ensures visibility.
|
|
33
|
+
console.log(`[MaxGroupsFilter] ${message}`, ...args);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// --- End log setup ---
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts a number value from various data formats (e.g., MongoDB NumberInt).
|
|
40
|
+
* Handles null, undefined, object with $numberInt, number, and string representations.
|
|
41
|
+
* Returns null if the value is not a valid integer representation.
|
|
42
|
+
* @param {*} value - Value from data object.
|
|
43
|
+
* @returns {number|null} - Parsed integer or null if invalid.
|
|
44
|
+
*/
|
|
45
|
+
function extractNumber(value) {
|
|
46
|
+
if (value === null || value === undefined) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle MongoDB NumberInt format: { "$numberInt": "1" }
|
|
51
|
+
if (typeof value === 'object' && value !== null && '$numberInt' in value) {
|
|
52
|
+
const parsed = parseInt(value.$numberInt, 10);
|
|
53
|
+
return isNaN(parsed) ? null : parsed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle regular number type
|
|
57
|
+
if (typeof value === 'number') {
|
|
58
|
+
// Ensure it's an integer, not float/Infinity/NaN
|
|
59
|
+
return Number.isInteger(value) ? value : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle string number: "1"
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
const parsed = parseInt(value, 10);
|
|
65
|
+
// Ensure the string was fully parsed as a number and is finite
|
|
66
|
+
return isNaN(parsed) ? null : parsed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Not a recognized number format
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Filters available timeblocks based on maximum group size limits for each meal period.
|
|
75
|
+
* It checks if adding a new reservation of 'guests' count would exceed the configured
|
|
76
|
+
* maximum number of groups allowed for various size thresholds within the specific meal period.
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} restaurantData - The main restaurant data object, expected to contain keys like 'max-groups-breakfast', 'max-groups-lunch', etc.
|
|
79
|
+
* @param {string} dateStr - The date string in "YYYY-MM-DD" format.
|
|
80
|
+
* @param {Object} timeblocks - An object of available timeblocks { "HH:MM": timeData } that have passed previous filters.
|
|
81
|
+
* @param {Array} reservations - An array of existing reservation objects for potentially multiple dates.
|
|
82
|
+
* @param {number|string} guests - The number of guests for the new reservation being considered.
|
|
83
|
+
* @returns {Object} - A new object containing only the timeblocks that satisfy the max group limits.
|
|
84
|
+
*/
|
|
85
|
+
function filterTimeblocksByMaxGroups(restaurantData, dateStr, timeblocks, reservations, guests) {
|
|
86
|
+
logDebug(`--- Running MaxGroupsFilter for Date: ${dateStr}, Guests: ${guests} ---`);
|
|
87
|
+
// Log the top-level keys related to max-groups to see what's actually present
|
|
88
|
+
const groupKeys = Object.keys(restaurantData).filter(k => k.startsWith('max-groups-'));
|
|
89
|
+
logDebug("Received restaurantData max-group keys:", groupKeys.length > 0 ? groupKeys : 'None found');
|
|
90
|
+
|
|
91
|
+
const filteredBlocks = {}; // Initialize the object to store timeblocks that pass the filter
|
|
92
|
+
const guestsNumber = parseInt(guests, 10); // Ensure guest count is a number
|
|
93
|
+
|
|
94
|
+
// Validate guest input
|
|
95
|
+
if (isNaN(guestsNumber) || guestsNumber <= 0) {
|
|
96
|
+
logDebug(`Invalid guest count (${guests}). Returning all ${Object.keys(timeblocks).length} input blocks unfiltered.`);
|
|
97
|
+
return timeblocks; // Return original blocks if guest count is invalid
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Pre-filter reservations to only those on the specific date for efficiency
|
|
101
|
+
const reservationsOnDate = reservations.filter(r => r.date === dateStr);
|
|
102
|
+
logDebug(`Relevant reservations on ${dateStr}: ${reservationsOnDate.length}`);
|
|
103
|
+
logDebug(`Input timeblocks count: ${Object.keys(timeblocks).length}`);
|
|
104
|
+
|
|
105
|
+
// Group reservations by meal type using our consistent getMealType function
|
|
106
|
+
const reservationsByMealType = {};
|
|
107
|
+
reservationsOnDate.forEach(r => {
|
|
108
|
+
const mealType = getMealType(r.time);
|
|
109
|
+
if (!mealType) return; // Skip if no meal type determined
|
|
110
|
+
|
|
111
|
+
if (!reservationsByMealType[mealType]) {
|
|
112
|
+
reservationsByMealType[mealType] = [];
|
|
113
|
+
}
|
|
114
|
+
reservationsByMealType[mealType].push(r);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
logDebug('Reservations grouped by meal type:', reservationsByMealType);
|
|
118
|
+
|
|
119
|
+
// Iterate through each timeblock that needs checking
|
|
120
|
+
for (const [time, timeData] of Object.entries(timeblocks)) {
|
|
121
|
+
const mealType = getMealType(time); // Determine meal period (e.g., 'breakfast') using our consistent function
|
|
122
|
+
logDebug(`\nChecking Time: ${time} -> Determined Meal: ${mealType}`);
|
|
123
|
+
|
|
124
|
+
// If the time doesn't fall into a recognized meal period, the filter doesn't apply
|
|
125
|
+
if (!mealType) {
|
|
126
|
+
logDebug(` Time ${time} has no associated meal type. Keeping block.`);
|
|
127
|
+
filteredBlocks[time] = timeData;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Construct the key for the settings specific to this meal (e.g., 'max-groups-breakfast')
|
|
132
|
+
const maxGroupSettingsKey = `max-groups-${mealType}`;
|
|
133
|
+
// Retrieve the settings object for this meal from the restaurant data
|
|
134
|
+
const maxGroupSettings = restaurantData[maxGroupSettingsKey];
|
|
135
|
+
|
|
136
|
+
logDebug(` Looking for settings using key: "${maxGroupSettingsKey}"`);
|
|
137
|
+
// If no settings object exists for this meal type, the filter doesn't apply to this timeblock
|
|
138
|
+
if (!maxGroupSettings || typeof maxGroupSettings !== 'object') {
|
|
139
|
+
logDebug(` No settings found for key "${maxGroupSettingsKey}". Filter doesn't apply. Keeping block.`);
|
|
140
|
+
filteredBlocks[time] = timeData;
|
|
141
|
+
continue; // Move to the next timeblock
|
|
142
|
+
}
|
|
143
|
+
// Log the actual settings found for this meal to verify correctness
|
|
144
|
+
logDebug(` Found settings for ${mealType}:`, JSON.stringify(maxGroupSettings));
|
|
145
|
+
|
|
146
|
+
// --- Settings exist, now apply the filter logic ---
|
|
147
|
+
let isTimeAllowed = true; // Assume the timeblock is allowed until a limit is violated
|
|
148
|
+
|
|
149
|
+
// Extract and sort the numeric group size limits defined in the settings (e.g., "6", "7" -> [6, 7])
|
|
150
|
+
const limitSizes = Object.keys(maxGroupSettings)
|
|
151
|
+
.map(key => parseInt(key, 10)) // Convert keys to numbers
|
|
152
|
+
.filter(num => !isNaN(num) && num > 0) // Keep only positive integers
|
|
153
|
+
.sort((a, b) => a - b); // Sort numerically (e.g., check 6 before 7)
|
|
154
|
+
logDebug(` Defined numeric limit sizes to check for ${mealType}: [${limitSizes.join(', ')}]`);
|
|
155
|
+
|
|
156
|
+
// Check against each defined limit size threshold
|
|
157
|
+
for (const limitSize of limitSizes) {
|
|
158
|
+
const limitSizeStr = String(limitSize); // Original string key (e.g., "6")
|
|
159
|
+
logDebug(` Checking Limit Condition: Groups of size >= ${limitSize}`);
|
|
160
|
+
|
|
161
|
+
// Does the new booking we're considering (guestsNumber) meet or exceed this limit threshold?
|
|
162
|
+
// Example: If limitSize is 6, and guestsNumber is 7, this limit applies. If guestsNumber is 5, it doesn't.
|
|
163
|
+
if (guestsNumber >= limitSize) {
|
|
164
|
+
logDebug(` Applicable: Booking guests (${guestsNumber}) >= Limit threshold (${limitSize})`);
|
|
165
|
+
|
|
166
|
+
// Extract the maximum number of groups allowed for this threshold
|
|
167
|
+
const maxAllowedCount = extractNumber(maxGroupSettings[limitSizeStr]);
|
|
168
|
+
logDebug(` Max groups allowed for >=${limitSize} is: ${maxAllowedCount} (raw setting: ${JSON.stringify(maxGroupSettings[limitSizeStr])})`);
|
|
169
|
+
|
|
170
|
+
// If the setting for this limit size is invalid (null or negative), treat it as 'no limit'
|
|
171
|
+
if (maxAllowedCount === null || maxAllowedCount < 0) {
|
|
172
|
+
logDebug(` Skipping check: Invalid or non-positive max count defined for size ${limitSize}.`);
|
|
173
|
+
continue; // Skip to the next limitSize
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get reservations for this meal type - using only reservations from the same meal type
|
|
177
|
+
const reservationsForMealType = reservationsByMealType[mealType] || [];
|
|
178
|
+
|
|
179
|
+
// Count how many existing reservations for this meal type meet or exceed this limit size
|
|
180
|
+
const existingMatchingGroupsCount = reservationsForMealType.filter(reservation => {
|
|
181
|
+
const reservationGuests = parseInt(reservation.guests, 10);
|
|
182
|
+
return !isNaN(reservationGuests) && reservationGuests >= limitSize;
|
|
183
|
+
}).length;
|
|
184
|
+
|
|
185
|
+
logDebug(` Count of existing groups >=${limitSize} found in ${mealType}: ${existingMatchingGroupsCount}`);
|
|
186
|
+
|
|
187
|
+
// THE CORE LOGIC: Check if adding the new group would exceed the max allowed count
|
|
188
|
+
if (existingMatchingGroupsCount + 1 > maxAllowedCount) {
|
|
189
|
+
logDebug(` *** VIOLATION DETECTED *** for limit >=${limitSize}.`);
|
|
190
|
+
logDebug(` (Existing Groups: ${existingMatchingGroupsCount} + New Group: 1 = ${existingMatchingGroupsCount + 1}, which is > Max Allowed: ${maxAllowedCount})`);
|
|
191
|
+
logDebug(` Blocking time ${time} due to this violation.`);
|
|
192
|
+
isTimeAllowed = false; // Mark the timeblock as blocked
|
|
193
|
+
break; // No need to check further limits for this timeblock; it has already failed.
|
|
194
|
+
} else {
|
|
195
|
+
// This specific limit check passed
|
|
196
|
+
logDebug(` OK: Limit for >=${limitSize} not exceeded. (Existing: ${existingMatchingGroupsCount} + New: 1 <= Max: ${maxAllowedCount})`);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// The new booking's guest count is smaller than the current limit threshold, so this rule doesn't restrict it.
|
|
200
|
+
logDebug(` Not Applicable: Booking guests (${guestsNumber}) < Limit threshold (${limitSize}). Skipping this check.`);
|
|
201
|
+
}
|
|
202
|
+
} // End of loop through different limitSizes (e.g., checking 6, then 7)
|
|
203
|
+
|
|
204
|
+
// After checking all applicable limits for the timeblock:
|
|
205
|
+
if (isTimeAllowed) {
|
|
206
|
+
logDebug(` ✅ Result for time ${time}: ALLOWED (passed all applicable checks)`);
|
|
207
|
+
filteredBlocks[time] = timeData; // Add it to the results
|
|
208
|
+
} else {
|
|
209
|
+
logDebug(` ❌ Result for time ${time}: BLOCKED (failed at least one limit check)`);
|
|
210
|
+
// Do not add it to filteredBlocks
|
|
211
|
+
}
|
|
212
|
+
} // End of loop through all input timeblocks
|
|
213
|
+
|
|
214
|
+
logDebug(`--- MaxGroupsFilter Finished. Output timeblocks count: ${Object.keys(filteredBlocks).length} ---`);
|
|
215
|
+
return filteredBlocks; // Return the timeblocks that passed all checks
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Export the function for use in other parts of the application
|
|
219
|
+
module.exports = {
|
|
220
|
+
filterTimeblocksByMaxGroups,
|
|
221
|
+
getMealType, // Export getMealType for potential use by other modules
|
|
222
222
|
};
|
package/filters/timeFilter.js
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
// Archief/Fields/algorithm/stopTimeFilter.js
|
|
2
|
-
|
|
3
|
-
// Assuming moment-timezone is available in the project, as used elsewhere
|
|
4
|
-
const moment = require('moment-timezone');
|
|
5
|
-
|
|
6
|
-
// Import necessary helpers (adjust path if tableHelpers.js is elsewhere)
|
|
7
|
-
// Or redefine shifts, parseTime, getMealTypeByTime here if preferred
|
|
8
|
-
const { shifts, parseTime, getMealTypeByTime } = require('../tableHelpers');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Gets the current time in minutes since midnight in the Brussels timezone.
|
|
12
|
-
* @returns {number} Current time in minutes.
|
|
13
|
-
*/
|
|
14
|
-
function getCurrentTimeInBrusselsMinutes() {
|
|
15
|
-
const now = moment().tz('Europe/Brussels');
|
|
16
|
-
return now.hours() * 60 + now.minutes();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Filters a list of timeblocks based on stop times defined in general settings.
|
|
21
|
-
* This filter only applies if the provided dateStr is the current date.
|
|
22
|
-
*
|
|
23
|
-
* @param {Object} timeblocks - An object where keys are time strings ("HH:MM") and values are timeblock data.
|
|
24
|
-
* @param {Object} restaurantData - The main restaurant data object containing general-settings.
|
|
25
|
-
* @param {string} dateStr - The date string ("YYYY-MM-DD") to check against today.
|
|
26
|
-
* @returns {Object} - The filtered timeblocks object.
|
|
27
|
-
*/
|
|
28
|
-
function filterTimeblocksByStopTimes(timeblocks, restaurantData, dateStr) {
|
|
29
|
-
// Determine today's date string in the correct timezone
|
|
30
|
-
const todayStr = moment().tz('Europe/Brussels').format('YYYY-MM-DD');
|
|
31
|
-
|
|
32
|
-
// Only apply the filter if the date being checked is today
|
|
33
|
-
if (dateStr !== todayStr) {
|
|
34
|
-
return timeblocks; // No filtering needed for future dates
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.log(`Applying stop time filter for today (${dateStr})`);
|
|
38
|
-
|
|
39
|
-
const settings = restaurantData?.['general-settings'] || {};
|
|
40
|
-
const currentTimeMinutes = getCurrentTimeInBrusselsMinutes();
|
|
41
|
-
|
|
42
|
-
// Get stop times from settings and parse them (handle missing/null values)
|
|
43
|
-
const breakfastStopTimeStr = settings.ontbijtStop || null; // e.g., "10:00"
|
|
44
|
-
const lunchStopTimeStr = settings.lunchStop || null; // e.g., "14:00"
|
|
45
|
-
const dinnerStopTimeStr = settings.dinerStop || null; // e.g., "20:00"
|
|
46
|
-
|
|
47
|
-
const breakfastStopTime = breakfastStopTimeStr ? parseTime(breakfastStopTimeStr) : null;
|
|
48
|
-
const lunchStopTime = lunchStopTimeStr ? parseTime(lunchStopTimeStr) : null;
|
|
49
|
-
const dinnerStopTime = dinnerStopTimeStr ? parseTime(dinnerStopTimeStr) : null;
|
|
50
|
-
|
|
51
|
-
// console.log(`Current time (minutes): ${currentTimeMinutes}`); // Verbose
|
|
52
|
-
// console.log(`Stop times (minutes) - Breakfast: ${breakfastStopTime}, Lunch: ${lunchStopTime}, Dinner: ${dinnerStopTime}`); // Verbose
|
|
53
|
-
|
|
54
|
-
const filteredTimeblocks = {};
|
|
55
|
-
let skippedBreakfast = 0;
|
|
56
|
-
let skippedLunch = 0;
|
|
57
|
-
let skippedDinner = 0;
|
|
58
|
-
|
|
59
|
-
for (const time in timeblocks) {
|
|
60
|
-
if (!timeblocks.hasOwnProperty(time)) continue;
|
|
61
|
-
|
|
62
|
-
const mealType = getMealTypeByTime(time);
|
|
63
|
-
let skip = false;
|
|
64
|
-
|
|
65
|
-
if (mealType === 'breakfast' && breakfastStopTime !== null && !isNaN(breakfastStopTime) && currentTimeMinutes >= breakfastStopTime) {
|
|
66
|
-
skip = true;
|
|
67
|
-
skippedBreakfast++;
|
|
68
|
-
} else if (mealType === 'lunch' && lunchStopTime !== null && !isNaN(lunchStopTime) && currentTimeMinutes >= lunchStopTime) {
|
|
69
|
-
skip = true;
|
|
70
|
-
skippedLunch++;
|
|
71
|
-
} else if (mealType === 'dinner' && dinnerStopTime !== null && !isNaN(dinnerStopTime) && currentTimeMinutes >= dinnerStopTime) {
|
|
72
|
-
skip = true;
|
|
73
|
-
skippedDinner++;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!skip) {
|
|
77
|
-
filteredTimeblocks[time] = timeblocks[time];
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (skippedBreakfast > 0) console.log(`Stop time filter removed ${skippedBreakfast} breakfast slots.`);
|
|
82
|
-
if (skippedLunch > 0) console.log(`Stop time filter removed ${skippedLunch} lunch slots.`);
|
|
83
|
-
if (skippedDinner > 0) console.log(`Stop time filter removed ${skippedDinner} dinner slots.`);
|
|
84
|
-
|
|
85
|
-
return filteredTimeblocks;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
module.exports = {
|
|
89
|
-
filterTimeblocksByStopTimes
|
|
1
|
+
// Archief/Fields/algorithm/stopTimeFilter.js
|
|
2
|
+
|
|
3
|
+
// Assuming moment-timezone is available in the project, as used elsewhere
|
|
4
|
+
const moment = require('moment-timezone');
|
|
5
|
+
|
|
6
|
+
// Import necessary helpers (adjust path if tableHelpers.js is elsewhere)
|
|
7
|
+
// Or redefine shifts, parseTime, getMealTypeByTime here if preferred
|
|
8
|
+
const { shifts, parseTime, getMealTypeByTime } = require('../tableHelpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gets the current time in minutes since midnight in the Brussels timezone.
|
|
12
|
+
* @returns {number} Current time in minutes.
|
|
13
|
+
*/
|
|
14
|
+
function getCurrentTimeInBrusselsMinutes() {
|
|
15
|
+
const now = moment().tz('Europe/Brussels');
|
|
16
|
+
return now.hours() * 60 + now.minutes();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Filters a list of timeblocks based on stop times defined in general settings.
|
|
21
|
+
* This filter only applies if the provided dateStr is the current date.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} timeblocks - An object where keys are time strings ("HH:MM") and values are timeblock data.
|
|
24
|
+
* @param {Object} restaurantData - The main restaurant data object containing general-settings.
|
|
25
|
+
* @param {string} dateStr - The date string ("YYYY-MM-DD") to check against today.
|
|
26
|
+
* @returns {Object} - The filtered timeblocks object.
|
|
27
|
+
*/
|
|
28
|
+
function filterTimeblocksByStopTimes(timeblocks, restaurantData, dateStr) {
|
|
29
|
+
// Determine today's date string in the correct timezone
|
|
30
|
+
const todayStr = moment().tz('Europe/Brussels').format('YYYY-MM-DD');
|
|
31
|
+
|
|
32
|
+
// Only apply the filter if the date being checked is today
|
|
33
|
+
if (dateStr !== todayStr) {
|
|
34
|
+
return timeblocks; // No filtering needed for future dates
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`Applying stop time filter for today (${dateStr})`);
|
|
38
|
+
|
|
39
|
+
const settings = restaurantData?.['general-settings'] || {};
|
|
40
|
+
const currentTimeMinutes = getCurrentTimeInBrusselsMinutes();
|
|
41
|
+
|
|
42
|
+
// Get stop times from settings and parse them (handle missing/null values)
|
|
43
|
+
const breakfastStopTimeStr = settings.ontbijtStop || null; // e.g., "10:00"
|
|
44
|
+
const lunchStopTimeStr = settings.lunchStop || null; // e.g., "14:00"
|
|
45
|
+
const dinnerStopTimeStr = settings.dinerStop || null; // e.g., "20:00"
|
|
46
|
+
|
|
47
|
+
const breakfastStopTime = breakfastStopTimeStr ? parseTime(breakfastStopTimeStr) : null;
|
|
48
|
+
const lunchStopTime = lunchStopTimeStr ? parseTime(lunchStopTimeStr) : null;
|
|
49
|
+
const dinnerStopTime = dinnerStopTimeStr ? parseTime(dinnerStopTimeStr) : null;
|
|
50
|
+
|
|
51
|
+
// console.log(`Current time (minutes): ${currentTimeMinutes}`); // Verbose
|
|
52
|
+
// console.log(`Stop times (minutes) - Breakfast: ${breakfastStopTime}, Lunch: ${lunchStopTime}, Dinner: ${dinnerStopTime}`); // Verbose
|
|
53
|
+
|
|
54
|
+
const filteredTimeblocks = {};
|
|
55
|
+
let skippedBreakfast = 0;
|
|
56
|
+
let skippedLunch = 0;
|
|
57
|
+
let skippedDinner = 0;
|
|
58
|
+
|
|
59
|
+
for (const time in timeblocks) {
|
|
60
|
+
if (!timeblocks.hasOwnProperty(time)) continue;
|
|
61
|
+
|
|
62
|
+
const mealType = getMealTypeByTime(time);
|
|
63
|
+
let skip = false;
|
|
64
|
+
|
|
65
|
+
if (mealType === 'breakfast' && breakfastStopTime !== null && !isNaN(breakfastStopTime) && currentTimeMinutes >= breakfastStopTime) {
|
|
66
|
+
skip = true;
|
|
67
|
+
skippedBreakfast++;
|
|
68
|
+
} else if (mealType === 'lunch' && lunchStopTime !== null && !isNaN(lunchStopTime) && currentTimeMinutes >= lunchStopTime) {
|
|
69
|
+
skip = true;
|
|
70
|
+
skippedLunch++;
|
|
71
|
+
} else if (mealType === 'dinner' && dinnerStopTime !== null && !isNaN(dinnerStopTime) && currentTimeMinutes >= dinnerStopTime) {
|
|
72
|
+
skip = true;
|
|
73
|
+
skippedDinner++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!skip) {
|
|
77
|
+
filteredTimeblocks[time] = timeblocks[time];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (skippedBreakfast > 0) console.log(`Stop time filter removed ${skippedBreakfast} breakfast slots.`);
|
|
82
|
+
if (skippedLunch > 0) console.log(`Stop time filter removed ${skippedLunch} lunch slots.`);
|
|
83
|
+
if (skippedDinner > 0) console.log(`Stop time filter removed ${skippedDinner} dinner slots.`);
|
|
84
|
+
|
|
85
|
+
return filteredTimeblocks;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
filterTimeblocksByStopTimes
|
|
90
90
|
};
|