@happychef/algorithm 1.3.0 → 1.3.5

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.
Files changed (56) hide show
  1. package/.github/workflows/ci-cd.yml +80 -80
  2. package/CHANGELOG.md +8 -8
  3. package/RESERVERINGEN_GIDS.md +986 -986
  4. package/assignTables.js +444 -444
  5. package/bundle_entry.js +100 -0
  6. package/changes/2025/December/PR2___change.md +14 -14
  7. package/changes/2025/December/PR3_add__change.md +20 -20
  8. package/changes/2025/December/PR4___.md +15 -15
  9. package/changes/2025/December/PR5___.md +15 -15
  10. package/changes/2025/December/PR6__del_.md +17 -17
  11. package/changes/2025/December/PR7_add__change.md +21 -21
  12. package/changes/2026/February/PR15_add__change.md +21 -21
  13. package/changes/2026/January/PR10_add__change.md +21 -21
  14. package/changes/2026/January/PR11_add__change.md +19 -19
  15. package/changes/2026/January/PR12_add__.md +21 -21
  16. package/changes/2026/January/PR13_add__change.md +20 -20
  17. package/changes/2026/January/PR14_add__change.md +19 -19
  18. package/changes/2026/January/PR8_add__change.md +38 -38
  19. package/changes/2026/January/PR9_add__change.md +19 -19
  20. package/filters/maxArrivalsFilter.js +114 -114
  21. package/filters/maxGroupsFilter.js +221 -221
  22. package/filters/timeFilter.js +89 -89
  23. package/getAvailableTimeblocks.js +170 -158
  24. package/grouping.js +162 -162
  25. package/index.js +42 -43
  26. package/isDateAvailable.js +80 -80
  27. package/isDateAvailableWithTableCheck.js +172 -172
  28. package/isTimeAvailable.js +26 -26
  29. package/moment-timezone-shim.js +179 -0
  30. package/nul +0 -0
  31. package/package.json +27 -27
  32. package/processing/dailyGuestCounts.js +73 -73
  33. package/processing/mealTypeCount.js +133 -133
  34. package/processing/timeblocksAvailable.js +194 -182
  35. package/reservation_data/counter.js +74 -74
  36. package/restaurant_data/exceptions.js +150 -150
  37. package/restaurant_data/openinghours.js +166 -142
  38. package/simulateTableAssignment.js +727 -726
  39. package/tableHelpers.js +212 -209
  40. package/tables/time/parseTime.js +19 -19
  41. package/tables/time/shifts.js +7 -7
  42. package/tables/utils/calculateDistance.js +13 -13
  43. package/tables/utils/isTableFreeForAllSlots.js +14 -14
  44. package/tables/utils/isTemporaryTableValid.js +39 -39
  45. package/test/test_counter.js +194 -194
  46. package/test/test_dailyCount.js +81 -81
  47. package/test/test_datesAvailable.js +106 -106
  48. package/test/test_exceptions.js +172 -172
  49. package/test/test_isDateAvailable.js +330 -330
  50. package/test/test_mealTypeCount.js +54 -54
  51. package/test/test_timesAvailable.js +88 -88
  52. package/test-meal-stop-fix.js +147 -147
  53. package/test-meal-stop-simple.js +93 -93
  54. package/test.js +336 -336
  55. package/changes/2026/February/PR16_add_getDateClosingReasons.md +0 -31
  56. package/getDateClosingReasons.js +0 -193
@@ -1,727 +1,728 @@
1
- // =============================================================================
2
- // FILE: simulateTableAssignment.js (UPDATED)
3
- // =============================================================================
4
- // Archief/Fields/algorithm/simulateTableAssignment.js
5
-
6
- // --- Import Helpers ---
7
- const {
8
- getAllTables,
9
- isTemporaryTableValid,
10
- } = require('./tableHelpers');
11
-
12
-
13
- /**
14
- * Robust helper for parsing numeric values from various formats.
15
- * Handles MongoDB $numberInt format and regular values.
16
- */
17
- function safeParseInt(val, defaultValue) {
18
- console.log(`[safeParseInt] Parsing value:`, val, `with type:`, typeof val);
19
-
20
- if (val === undefined || val === null) return defaultValue;
21
-
22
- if (typeof val === 'object' && val !== null && '$numberInt' in val) {
23
- try {
24
- const parsed = parseInt(val.$numberInt, 10);
25
- console.log(`[safeParseInt] Parsed $numberInt format:`, parsed);
26
- if (!isNaN(parsed)) return parsed;
27
- } catch (e) {
28
- console.log(`[safeParseInt] Error parsing $numberInt:`, e);
29
- }
30
- return defaultValue;
31
- }
32
-
33
- if (typeof val === 'number' && !isNaN(val)) {
34
- console.log(`[safeParseInt] Using numeric value directly:`, val);
35
- return val;
36
- }
37
-
38
- if (typeof val === 'string') {
39
- try {
40
- const parsed = parseInt(val, 10);
41
- console.log(`[safeParseInt] Parsed string:`, parsed);
42
- if (!isNaN(parsed)) return parsed;
43
- } catch (e) {
44
- console.log(`[safeParseInt] Error parsing string:`, e);
45
- }
46
- }
47
- console.log(`[safeParseInt] Could not parse, using default:`, defaultValue);
48
- return defaultValue;
49
- }
50
-
51
- /**
52
- * Extracts actual table numbers from a reservation's tables array.
53
- * Handles MongoDB $numberInt format.
54
- */
55
- function extractTableNumbers(tablesArray) {
56
- if (!Array.isArray(tablesArray)) {
57
- return [];
58
- }
59
-
60
- return tablesArray.map(table => {
61
- return safeParseInt(table, null);
62
- }).filter(tableNum => tableNum !== null);
63
- }
64
-
65
- /**
66
- * Gets actual table assignments from reservation data if available.
67
- * @param {Object} reservation - The reservation object
68
- * @returns {Array} Array of table numbers that are actually assigned, or empty array if no data
69
- */
70
- function getActualTableAssignment(reservation) {
71
- // Check if reservation has actual table assignments
72
- if (reservation.tables && Array.isArray(reservation.tables)) {
73
- const tableNumbers = extractTableNumbers(reservation.tables);
74
- console.log(`[getActualTableAssignment] Found actual table assignment for reservation:`, tableNumbers);
75
- return tableNumbers;
76
- }
77
-
78
- console.log(`[getActualTableAssignment] No actual table assignment found for reservation`);
79
- return [];
80
- }
81
-
82
- /**
83
- * Converts a reservation's start time and duration into discrete time slots (in minutes from midnight).
84
- */
85
- function computeRequiredSlots(timeString, durationMinutes, intervalMinutes) {
86
- const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
87
- if (!timeString || !timePattern.test(timeString)) {
88
- console.error(`Invalid time format for computeRequiredSlots: ${timeString}. Expected 'HH:MM'.`);
89
- return [];
90
- }
91
-
92
- const [hour, minute] = timeString.split(":").map(Number);
93
- const startMinutes = hour * 60 + minute;
94
-
95
- if (intervalMinutes <= 0) {
96
- console.error("Interval must be positive in computeRequiredSlots.");
97
- return [startMinutes];
98
- }
99
-
100
- const slotCount = Math.ceil(durationMinutes / intervalMinutes);
101
- console.log(`Computing ${slotCount} slots for time ${timeString} (duration: ${durationMinutes}min, interval: ${intervalMinutes}min)`);
102
-
103
- const slots = [];
104
- for (let i = 0; i < slotCount; i++) {
105
- slots.push(startMinutes + i * intervalMinutes);
106
- }
107
-
108
- return slots;
109
- }
110
-
111
- /**
112
- * Checks if the given tableNumber is free for all requiredSlots based on the occupied map.
113
- */
114
- function isTableFreeForAllSlots(tableNumber, requiredSlots, tableOccupiedSlots) {
115
- const occupiedSlots = tableOccupiedSlots[tableNumber] || new Set();
116
-
117
- for (const slot of requiredSlots) {
118
- if (occupiedSlots.has(slot)) {
119
- return false;
120
- }
121
- }
122
- return true;
123
- }
124
-
125
- /**
126
- * Calculates Euclidean distance between two tables (assuming x, y properties).
127
- */
128
- function calculateDistance(tableA, tableB) {
129
- if (tableA?.x === undefined || tableA?.y === undefined || tableB?.x === undefined || tableB?.y === undefined) {
130
- return Infinity; // Cannot calculate distance if coordinates are missing
131
- }
132
- const dx = tableA.x - tableB.x;
133
- const dy = tableA.y - tableB.y;
134
- return Math.sqrt(dx * dx + dy * dy);
135
- }
136
-
137
- /**
138
- * QUICKSORT-INSPIRED OPTIMIZATION: O(n log n) multi-table assignment with partitioning
139
- *
140
- * Instead of exploring all combinations (exponential), we use:
141
- * 1. Greedy partitioning - divide tables into capacity buckets
142
- * 2. Pivot selection - pick optimal table as "pivot"
143
- * 3. Branch reduction - only explore promising paths
144
- * 4. Dynamic programming - cache intermediate results
145
- *
146
- * Complexity: O(n log n) instead of O(2^n)
147
- */
148
-
149
- // Cache for intermediate results (dynamic programming)
150
- const assignmentCache = new Map();
151
-
152
- function findMultiTableCombination(tables, guestsTotal, startIndex, currentSet, best) {
153
- // Calculate current set's min and max capacity sums
154
- let curMin = 0, curMax = 0;
155
- for (const t of currentSet) {
156
- curMin += t.minCapacity || 0;
157
- curMax += t.maxCapacity || 0;
158
- }
159
-
160
- // PRUNING: If sum of minCapacity already exceeds guests, this combination is invalid
161
- if (curMin > guestsTotal) return;
162
-
163
- // Check if current combination is a valid solution
164
- // Valid means: sum(minCapacity) <= guests <= sum(maxCapacity)
165
- if (currentSet.length > 0 && curMin <= guestsTotal && guestsTotal <= curMax) {
166
- if (currentSet.length < best.tableCount) {
167
- // Calculate distance only for improved solutions
168
- let distanceSum = 0;
169
- for (let i = 0; i < currentSet.length; i++) {
170
- for (let j = i + 1; j < currentSet.length; j++) {
171
- distanceSum += calculateDistance(currentSet[i], currentSet[j]);
172
- }
173
- }
174
- if (currentSet.length < best.tableCount ||
175
- (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
176
- best.minDistance = distanceSum;
177
- best.tables = [...currentSet];
178
- best.tableCount = currentSet.length;
179
- }
180
- }
181
- // Continue searching for potentially tighter clusters
182
- }
183
-
184
- // AGGRESSIVE PRUNING
185
- if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) return;
186
- if (startIndex >= tables.length) return;
187
-
188
- // Compute remaining capacity potential (suffix sum)
189
- let suffixMax = 0;
190
- for (let i = startIndex; i < tables.length; i++) {
191
- suffixMax += tables[i].maxCapacity || 0;
192
- }
193
-
194
- // PRUNING: Even with all remaining tables, can't reach guests
195
- if (curMax + suffixMax < guestsTotal) return;
196
-
197
- // OPTIMIZATION: Use cached result if available (Dynamic Programming)
198
- const cacheKey = `${guestsTotal}-${startIndex}-${currentSet.length}-${curMin}`;
199
- if (assignmentCache.has(cacheKey)) {
200
- const cached = assignmentCache.get(cacheKey);
201
- if (cached.tableCount < best.tableCount) {
202
- best.tableCount = cached.tableCount;
203
- best.tables = [...cached.tables];
204
- best.minDistance = cached.minDistance;
205
- }
206
- return;
207
- }
208
-
209
- // QUICKSORT-INSPIRED: Partition tables by capacity relative to guests needed
210
- const remaining = tables.slice(startIndex);
211
- const guestsNeeded = guestsTotal - curMax; // How much more capacity we need
212
- const exactFit = [];
213
- const overCapacity = [];
214
- const underCapacity = [];
215
-
216
- for (const tbl of remaining) {
217
- // Check if adding this table would exceed minCapacity constraint
218
- const newMin = curMin + (tbl.minCapacity || 0);
219
- if (newMin > guestsTotal) continue; // Skip - would violate minCapacity constraint
220
-
221
- const canSeat = tbl.maxCapacity || 0;
222
-
223
- if (tbl.minCapacity <= guestsTotal && guestsTotal <= tbl.maxCapacity && currentSet.length === 0) {
224
- // Single table that fits exactly (only relevant when starting fresh)
225
- exactFit.push(tbl);
226
- } else if (curMax + canSeat >= guestsTotal && newMin <= guestsTotal) {
227
- // This table could complete a valid combination
228
- overCapacity.push(tbl);
229
- } else {
230
- // Need more tables after this one
231
- underCapacity.push(tbl);
232
- }
233
- }
234
-
235
- // STRATEGY 1: Try exact fit first (best case - single table)
236
- if (exactFit.length > 0 && currentSet.length === 0) {
237
- // Sort by smallest capacity first (minimize waste)
238
- exactFit.sort((a, b) => a.maxCapacity - b.maxCapacity);
239
- const tbl = exactFit[0];
240
- currentSet.push(tbl);
241
- findMultiTableCombination(tables, guestsTotal, tables.length, currentSet, best);
242
- currentSet.pop();
243
-
244
- if (best.tableCount === 1) return; // Found optimal
245
- }
246
-
247
- // STRATEGY 2: Tables that can complete the combination
248
- if (overCapacity.length > 0) {
249
- // Sort by closest to completing the combination
250
- overCapacity.sort((a, b) => {
251
- const aTotal = curMax + a.maxCapacity;
252
- const bTotal = curMax + b.maxCapacity;
253
- return Math.abs(aTotal - guestsTotal) - Math.abs(bTotal - guestsTotal);
254
- });
255
-
256
- // Try top candidates
257
- const candidates = overCapacity.slice(0, Math.min(3, overCapacity.length));
258
- for (const tbl of candidates) {
259
- currentSet.push(tbl);
260
- findMultiTableCombination(tables, guestsTotal, tables.indexOf(tbl) + 1, currentSet, best);
261
- currentSet.pop();
262
-
263
- if (best.tableCount === currentSet.length + 1) return;
264
- }
265
- }
266
-
267
- // STRATEGY 3: Two-table combinations (most common case)
268
- if (underCapacity.length >= 2 && currentSet.length === 0 && best.tableCount > 2) {
269
- // Sort by capacity descending
270
- underCapacity.sort((a, b) => b.maxCapacity - a.maxCapacity);
271
-
272
- // Find pairs that form valid combinations
273
- for (let i = 0; i < Math.min(5, underCapacity.length); i++) {
274
- const first = underCapacity[i];
275
-
276
- for (let j = i + 1; j < underCapacity.length; j++) {
277
- const second = underCapacity[j];
278
- const totalMin = (first.minCapacity || 0) + (second.minCapacity || 0);
279
- const totalMax = (first.maxCapacity || 0) + (second.maxCapacity || 0);
280
-
281
- // Check if this pair forms a valid combination
282
- if (totalMin <= guestsTotal && guestsTotal <= totalMax) {
283
- currentSet.push(first);
284
- currentSet.push(second);
285
- findMultiTableCombination(tables, guestsTotal, tables.length, currentSet, best);
286
- currentSet.pop();
287
- currentSet.pop();
288
-
289
- if (best.tableCount === 2) return; // Found optimal two-table
290
- }
291
- }
292
- }
293
- }
294
-
295
- // STRATEGY 4: Limited backtracking only if no good solution found
296
- if (best.tableCount > 3 && remaining.length <= 10) {
297
- // Sort by efficiency (capacity per table)
298
- remaining.sort((a, b) => b.maxCapacity - a.maxCapacity);
299
-
300
- // Try only top 5 tables
301
- const limited = remaining.slice(0, Math.min(5, remaining.length));
302
- for (const tbl of limited) {
303
- const newMin = curMin + (tbl.minCapacity || 0);
304
- if (newMin > guestsTotal) continue; // Skip - would violate minCapacity constraint
305
-
306
- currentSet.push(tbl);
307
- const nextIdx = tables.indexOf(tbl) + 1;
308
- findMultiTableCombination(tables, guestsTotal, nextIdx, currentSet, best);
309
- currentSet.pop();
310
-
311
- if (best.tableCount <= 2) return;
312
- }
313
- }
314
-
315
- // Cache result
316
- if (best.tableCount < Infinity) {
317
- assignmentCache.set(cacheKey, {
318
- tableCount: best.tableCount,
319
- tables: [...best.tables],
320
- minDistance: best.minDistance
321
- });
322
- }
323
- }
324
-
325
- function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null, duration = null) {
326
- console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
327
-
328
- // Clear cache if it gets too large (prevent memory leaks)
329
- if (assignmentCache.size > 1000) {
330
- assignmentCache.clear();
331
- }
332
-
333
- // FIXED: More robust parsing of settings with detailed logging
334
- const generalSettings = restaurantData["general-settings"] || {};
335
-
336
- // DEFAULT VALUES - this approach mirrors the TypeScript implementation
337
- let duurReservatie = 120; // Default: 2 hours in minutes
338
- let intervalReservatie = 15; // Default: 15 minute intervals
339
-
340
- // Use safeParseInt for robust parsing
341
- duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
342
- intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
343
-
344
- // Use passed duration if available, otherwise default
345
- if (duration && duration > 0) {
346
- duurReservatie = duration;
347
- }
348
-
349
- console.log(`[assignTablesForGivenTime] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
350
-
351
- if (intervalReservatie <= 0) {
352
- console.error("Invalid interval settings.");
353
- return [];
354
- }
355
-
356
- const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
357
- if (!requiredSlots || requiredSlots.length === 0) {
358
- console.error(`Could not compute required slots for ${time}`);
359
- return [];
360
- }
361
-
362
- // Fetch all tables using the imported helper
363
- const allTables = getAllTables(restaurantData, selectedZitplaats);
364
- console.log(`[assignTablesForGivenTime] Checking ${allTables.length} tables`);
365
-
366
- // OPTIMIZATION: Pre-filter and sort tables for better performance
367
- // Filter out invalid and occupied tables first
368
- const validTables = allTables.filter(t =>
369
- isTemporaryTableValid(t, date, time) &&
370
- isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
371
- );
372
-
373
- if (validTables.length === 0) {
374
- console.log(`[assignTablesForGivenTime] No valid tables available`);
375
- return [];
376
- }
377
-
378
- // OPTIMIZATION: Sort tables by capacity (prefer exact matches first)
379
- // This helps find optimal solutions faster
380
- validTables.sort((a, b) => {
381
- // Prioritize tables that can seat exactly the number of guests
382
- const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
383
- const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
384
- if (aExact !== bExact) return aExact - bExact;
385
-
386
- // Then sort by capacity (smaller tables first to minimize waste)
387
- return a.maxCapacity - b.maxCapacity;
388
- });
389
-
390
- // --- Try single-table assignment first ---
391
- for (const t of validTables) {
392
- if (t.minCapacity <= guests && guests <= t.maxCapacity) {
393
- console.log(`[assignTablesForGivenTime] Assigned single table ${t.tableNumber} for ${guests} guests`);
394
- return [t.tableNumber];
395
- }
396
- }
397
-
398
- console.log(`[assignTablesForGivenTime] No single table found, trying combinations...`);
399
-
400
- // --- Try multi-table assignment ---
401
- const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
402
- findMultiTableCombination(
403
- validTables, // Use pre-filtered and sorted tables
404
- guests,
405
- 0, // Start index
406
- [], // Initial empty set
407
- best // Best solution object (no need for slots/occupancy - already filtered)
408
- );
409
-
410
- if (best.tables.length > 0) {
411
- console.log(`[assignTablesForGivenTime] Found multi-table solution: ${best.tables.map(t => t.tableNumber).join(', ')}`);
412
- } else {
413
- console.log(`[assignTablesForGivenTime] No table combination found for ${guests} guests`);
414
- }
415
-
416
- return best.tables.map(t => t.tableNumber);
417
- }
418
-
419
- function isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats = null, duration = null) {
420
- console.log(`\n[isTimeAvailableSync] Checking ${date} ${time} for ${guests} guests (duration: ${duration || 'default'})`);
421
-
422
- if (guests < 0) {
423
- console.log(`[isTimeAvailableSync] Detected negative guest count (${guests}), adjusting to default of 2`);
424
- guests = 2; // Use a reasonable default
425
- }
426
-
427
- const tableSettings = restaurantData?.['table-settings'] || {};
428
- const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
429
- tableSettings.assignmentMode === "automatic";
430
-
431
- console.log(`[isTimeAvailableSync] Table assignment enabled? ${isTableAssignmentEnabled}`);
432
- console.log(`- Table settings installed: ${tableSettings.isInstalled === true}`);
433
- console.log(`- Assignment mode: ${tableSettings.assignmentMode}`);
434
-
435
- if (!isTableAssignmentEnabled) {
436
- console.log(`[isTimeAvailableSync] No table assignment needed, returning true`);
437
- return true;
438
- }
439
-
440
- try {
441
- // Basic data check
442
- if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
443
- console.error(`[isTimeAvailableSync] Missing floors data for ${date} ${time}`);
444
- return false;
445
- }
446
-
447
- if (guests <= 0) {
448
- console.error(`[isTimeAvailableSync] Invalid guest count: ${guests}`);
449
- return false; // Cannot reserve for 0 or fewer guests
450
- }
451
-
452
- const generalSettings = restaurantData["general-settings"] || {};
453
- let defaultDuurReservatie = 120; // Default: 2 hours in minutes
454
- let intervalReservatie = 15; // Default: 15 minute intervals
455
- defaultDuurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
456
- intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
457
-
458
- // Use passed duration or default
459
- const currentDuurReservatie = duration && duration > 0 ? duration : defaultDuurReservatie;
460
-
461
- console.log(`[isTimeAvailableSync] Using duration: ${currentDuurReservatie}min (default: ${defaultDuurReservatie}), interval: ${intervalReservatie}min`);
462
-
463
- if (intervalReservatie <= 0) {
464
- console.error(`[isTimeAvailableSync] Invalid interval settings for ${date} ${time}`);
465
- return false;
466
- }
467
-
468
- const requiredSlots = computeRequiredSlots(time, currentDuurReservatie, intervalReservatie);
469
- if (!requiredSlots || requiredSlots.length === 0) {
470
- console.error(`[isTimeAvailableSync] Could not compute required slots for ${date} ${time}`);
471
- return false; // Cannot proceed if slots are invalid
472
- }
473
-
474
- // --- Build Occupancy Map using ACTUAL table assignments when available ---
475
- const tableOccupiedSlots = {}; // { tableNumber: Set<slotMinutes> }
476
- const reservationsForDate = reservations
477
- .filter(r => r.date === date && r.time && r.guests > 0)
478
- .sort((a, b) => {
479
- // Simple time string comparison
480
- const timeA = a.time.split(':').map(Number);
481
- const timeB = b.time.split(':').map(Number);
482
- return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
483
- });
484
-
485
- console.log(`[isTimeAvailableSync] Processing ${reservationsForDate.length} existing reservations on ${date}`);
486
-
487
- for (const r of reservationsForDate) {
488
- // Calculate duration once per reservation
489
- const rDuration = safeParseInt(r.duration, defaultDuurReservatie);
490
-
491
- console.log(`[DEBUG] Reservation ${r.time} (${r.firstName}): Duration=${rDuration}, Tables raw=${JSON.stringify(r.tables)}`);
492
-
493
- // NEW: Try to get actual table assignment first
494
- const actualTables = getActualTableAssignment(r);
495
- console.log(`[DEBUG] Reservation ${r.time}: actualTables detected = ${JSON.stringify(actualTables)}`);
496
-
497
- let assignedTables = [];
498
-
499
- if (actualTables.length > 0) {
500
- // Use actual table assignment from reservation data
501
- assignedTables = actualTables;
502
- console.log(`[isTimeAvailableSync] Using actual table assignment for reservation: ${assignedTables.join(', ')}`);
503
- } else {
504
- // ... simulation log ...
505
- console.log(`[isTimeAvailableSync] No actual table data, simulating assignment for reservation`);
506
- assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration); // Simulation - use reservation's own zitplaats
507
- }
508
-
509
- // Update the occupancy map based on the actual or simulated assignment
510
- if (assignedTables.length > 0) {
511
- // Use reservation specific duration if available
512
- const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
513
- console.log(`[DEBUG] Reservation ${r.time}: Occupying slots=${rSlots.join(',')} on tables=${assignedTables.join(',')}`);
514
-
515
- if (!rSlots || rSlots.length === 0) continue; // Skip if slots invalid
516
-
517
- assignedTables.forEach(tableNumber => {
518
- if (!tableOccupiedSlots[tableNumber]) {
519
- tableOccupiedSlots[tableNumber] = new Set();
520
- }
521
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
522
- });
523
- }
524
- }
525
-
526
- console.log(`[isTimeAvailableSync] Occupancy map built, checking availability for new reservation`);
527
-
528
- const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
529
- console.log(`[isTimeAvailableSync] Checking ${allTables.length} tables for new reservation`);
530
-
531
- // OPTIMIZATION: Pre-filter valid tables
532
- const validTables = allTables.filter(t =>
533
- isTemporaryTableValid(t, date, time) &&
534
- isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
535
- );
536
-
537
- if (validTables.length === 0) {
538
- console.log(`[isTimeAvailableSync] No valid tables available`);
539
- return false;
540
- }
541
-
542
- // OPTIMIZATION: Sort for faster exact matches
543
- validTables.sort((a, b) => {
544
- const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
545
- const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
546
- if (aExact !== bExact) return aExact - bExact;
547
- return a.maxCapacity - b.maxCapacity;
548
- });
549
-
550
- // 1. Try single table assignment
551
- for (const t of validTables) {
552
- if (t.minCapacity <= guests && guests <= t.maxCapacity) {
553
- console.log(`[isTimeAvailableSync] Found single table ${t.tableNumber} for ${guests} guests at ${time}`);
554
- return true; // Available
555
- }
556
- }
557
-
558
- console.log(`[isTimeAvailableSync] No single table found, trying combinations...`);
559
-
560
- // 2. Try multi-table assignment
561
- const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
562
- findMultiTableCombination(
563
- validTables, // Use pre-filtered tables
564
- guests,
565
- 0, // Start index
566
- [], // Empty current set
567
- best // Best solution
568
- );
569
-
570
- const result = best.tables.length > 0;
571
- if (result) {
572
- console.log(`[isTimeAvailableSync] Multi-table assignment possible: ${best.tables.map(t => t.tableNumber).join(', ')}`);
573
- } else {
574
- console.log(`[isTimeAvailableSync] No table combination available`);
575
- }
576
-
577
- return result; // Available if a combination was found
578
-
579
- } catch (error) {
580
- console.error(`[isTimeAvailableSync] Error during check for ${date} ${time}:`, error);
581
- return false; // Assume unavailable on error
582
- }
583
- }
584
-
585
- function filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null, duration = null) {
586
- console.log(`[filterTimeblocksByTableAvailability] Filtering timeblocks for ${date} with ${guests} guests`);
587
-
588
- if (guests < 0) {
589
- console.log(`[filterTimeblocksByTableAvailability] Detected negative guest count (${guests}), adjusting to default of 2`);
590
- guests = 0; // Use a reasonable default
591
- }
592
-
593
- const tableSettings = restaurantData?.['table-settings'] || {};
594
- const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
595
- tableSettings.assignmentMode === "automatic";
596
-
597
- if (!isTableAssignmentEnabled) {
598
- console.log(`[filterTimeblocksByTableAvailability] No table assignment enabled, returning all timeblocks`);
599
- return timeblocks;
600
- }
601
-
602
- console.log(`[filterTimeblocksByTableAvailability] Starting with ${Object.keys(timeblocks).length} timeblocks`);
603
-
604
- const filteredTimeblocks = {};
605
- let availableCount = 0;
606
- let unavailableCount = 0;
607
-
608
- for (const time in timeblocks) {
609
- if (isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats, duration)) {
610
- filteredTimeblocks[time] = timeblocks[time];
611
- availableCount++;
612
- } else {
613
- unavailableCount++;
614
- }
615
- }
616
-
617
- console.log(`[filterTimeblocksByTableAvailability] Result: ${availableCount} available, ${unavailableCount} unavailable times`);
618
- if (availableCount > 0) {
619
- console.log(`[filterTimeblocksByTableAvailability] Available times: ${Object.keys(filteredTimeblocks).join(', ')}`);
620
- }
621
-
622
- return filteredTimeblocks;
623
- }
624
-
625
- function getAvailableTimeblocksWithTableCheck(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null, duration = null) {
626
- return filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats, duration);
627
- }
628
-
629
- function getAvailableTablesForTime(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
630
- try {
631
- if (guests < 0) {
632
- console.log(`[getAvailableTablesForTime] Detected negative guest count (${guests}), adjusting to default of 2`);
633
- guests = 0;
634
- }
635
-
636
- const tableSettings = restaurantData?.['table-settings'] || {};
637
- const isAutomaticAssignment = tableSettings.isInstalled === true &&
638
- tableSettings.assignmentMode === "automatic";
639
-
640
- if (!isAutomaticAssignment) {
641
- return [];
642
- }
643
-
644
- if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
645
- console.error(`[getAvailableTablesForTime] Missing floors data for ${date} ${time}`);
646
- return [];
647
- }
648
- if (guests <= 0) return [];
649
- const generalSettings = restaurantData["general-settings"] || {};
650
- const duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
651
- const intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
652
-
653
- if (intervalReservatie <= 0) {
654
- console.error(`[getAvailableTablesForTime] Invalid interval settings for ${date} ${time}`);
655
- return [];
656
- }
657
-
658
- console.log(`\n[getAvailableTablesForTime] Finding available tables for ${guests} guests on ${date} at ${time}`);
659
- const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
660
- if (!requiredSlots || requiredSlots.length === 0) {
661
- console.error(`[getAvailableTablesForTime] Could not compute required slots for ${date} ${time}`);
662
- return [];
663
- }
664
- const tableOccupiedSlots = {};
665
- const reservationsForDate = reservations
666
- .filter(r => r.date === date && r.time && r.guests > 0)
667
- .sort((a, b) => {
668
- const timeA = a.time.split(':').map(Number);
669
- const timeB = b.time.split(':').map(Number);
670
- return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
671
- });
672
-
673
- console.log(`[getAvailableTablesForTime] Building occupancy map (Processing ${reservationsForDate.length} existing reservations)`);
674
- for (const r of reservationsForDate) {
675
- const rDuration = safeParseInt(r.duration, duurReservatie);
676
- const actualTables = getActualTableAssignment(r);
677
- let assignedTables = [];
678
-
679
- if (actualTables.length > 0) {
680
- assignedTables = actualTables;
681
- } else {
682
- assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration);
683
- }
684
-
685
- if (assignedTables.length > 0) {
686
- const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
687
- if (!rSlots || rSlots.length === 0) continue;
688
- assignedTables.forEach(tableNumber => {
689
- if (!tableOccupiedSlots[tableNumber]) {
690
- tableOccupiedSlots[tableNumber] = new Set();
691
- }
692
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
693
- });
694
- }
695
- }
696
- console.log("[getAvailableTablesForTime] Occupancy map built");
697
- const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables with zitplaats filter applied
698
- const availableIndividualTables = [];
699
-
700
- console.log(`[getAvailableTablesForTime] Checking ${allTables.length} tables for individual availability`);
701
- for (const t of allTables) {
702
- if (!isTemporaryTableValid(t, date, time)) continue;
703
- if (t.minCapacity <= guests && guests <= t.maxCapacity) {
704
- if (isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)) {
705
- console.log(`[getAvailableTablesForTime] Table ${t.tableNumber} is available`);
706
- availableIndividualTables.push(t); // Storing the whole object might be useful
707
- }
708
- }
709
- }
710
-
711
- console.log(`[getAvailableTablesForTime] Found ${availableIndividualTables.length} individually available tables: ${availableIndividualTables.map(t => t.tableNumber).join(', ')}`);
712
- availableIndividualTables.sort((a, b) => a.tableNumber - b.tableNumber);
713
-
714
- return availableIndividualTables; // Return array of table objects
715
-
716
- } catch (error) {
717
- console.error(`[getAvailableTablesForTime] Error for ${date} ${time}:`, error);
718
- return []; // Return empty on error
719
- }
720
- }
721
-
722
- module.exports = {
723
- isTimeAvailableSync,
724
- filterTimeblocksByTableAvailability,
725
- getAvailableTimeblocksWithTableCheck,
726
- getAvailableTablesForTime,
1
+ // =============================================================================
2
+ // FILE: simulateTableAssignment.js (UPDATED)
3
+ // =============================================================================
4
+ // Archief/Fields/algorithm/simulateTableAssignment.js
5
+
6
+ // --- Import Helpers ---
7
+ const {
8
+ getAllTables,
9
+ isTemporaryTableValid,
10
+ } = require('./tableHelpers');
11
+
12
+
13
+ /**
14
+ * Robust helper for parsing numeric values from various formats.
15
+ * Handles MongoDB $numberInt format and regular values.
16
+ */
17
+ function safeParseInt(val, defaultValue) {
18
+ console.log(`[safeParseInt] Parsing value:`, val, `with type:`, typeof val);
19
+
20
+ if (val === undefined || val === null) return defaultValue;
21
+
22
+ if (typeof val === 'object' && val !== null && '$numberInt' in val) {
23
+ try {
24
+ const parsed = parseInt(val.$numberInt, 10);
25
+ console.log(`[safeParseInt] Parsed $numberInt format:`, parsed);
26
+ if (!isNaN(parsed)) return parsed;
27
+ } catch (e) {
28
+ console.log(`[safeParseInt] Error parsing $numberInt:`, e);
29
+ }
30
+ return defaultValue;
31
+ }
32
+
33
+ if (typeof val === 'number' && !isNaN(val)) {
34
+ console.log(`[safeParseInt] Using numeric value directly:`, val);
35
+ return val;
36
+ }
37
+
38
+ if (typeof val === 'string') {
39
+ try {
40
+ const parsed = parseInt(val, 10);
41
+ console.log(`[safeParseInt] Parsed string:`, parsed);
42
+ if (!isNaN(parsed)) return parsed;
43
+ } catch (e) {
44
+ console.log(`[safeParseInt] Error parsing string:`, e);
45
+ }
46
+ }
47
+ console.log(`[safeParseInt] Could not parse, using default:`, defaultValue);
48
+ return defaultValue;
49
+ }
50
+
51
+ /**
52
+ * Extracts actual table numbers from a reservation's tables array.
53
+ * Handles MongoDB $numberInt format.
54
+ */
55
+ function extractTableNumbers(tablesArray) {
56
+ if (!Array.isArray(tablesArray)) {
57
+ return [];
58
+ }
59
+
60
+ return tablesArray.map(table => {
61
+ return safeParseInt(table, null);
62
+ }).filter(tableNum => tableNum !== null);
63
+ }
64
+
65
+ /**
66
+ * Gets actual table assignments from reservation data if available.
67
+ * @param {Object} reservation - The reservation object
68
+ * @returns {Array} Array of table numbers that are actually assigned, or empty array if no data
69
+ */
70
+ function getActualTableAssignment(reservation) {
71
+ // Check if reservation has actual table assignments
72
+ if (reservation.tables && Array.isArray(reservation.tables)) {
73
+ const tableNumbers = extractTableNumbers(reservation.tables);
74
+ console.log(`[getActualTableAssignment] Found actual table assignment for reservation:`, tableNumbers);
75
+ return tableNumbers;
76
+ }
77
+
78
+ console.log(`[getActualTableAssignment] No actual table assignment found for reservation`);
79
+ return [];
80
+ }
81
+
82
+ /**
83
+ * Converts a reservation's start time and duration into discrete time slots (in minutes from midnight).
84
+ */
85
+ function computeRequiredSlots(timeString, durationMinutes, intervalMinutes) {
86
+ // Support cross-midnight times (e.g., "24:00", "25:00") for bowling venues
87
+ const timePattern = /^([0-2]\d):([0-5]\d)$/;
88
+ if (!timeString || !timePattern.test(timeString)) {
89
+ console.error(`Invalid time format for computeRequiredSlots: ${timeString}. Expected 'HH:MM'.`);
90
+ return [];
91
+ }
92
+
93
+ const [hour, minute] = timeString.split(":").map(Number);
94
+ const startMinutes = hour * 60 + minute;
95
+
96
+ if (intervalMinutes <= 0) {
97
+ console.error("Interval must be positive in computeRequiredSlots.");
98
+ return [startMinutes];
99
+ }
100
+
101
+ const slotCount = Math.ceil(durationMinutes / intervalMinutes);
102
+ console.log(`Computing ${slotCount} slots for time ${timeString} (duration: ${durationMinutes}min, interval: ${intervalMinutes}min)`);
103
+
104
+ const slots = [];
105
+ for (let i = 0; i < slotCount; i++) {
106
+ slots.push(startMinutes + i * intervalMinutes);
107
+ }
108
+
109
+ return slots;
110
+ }
111
+
112
+ /**
113
+ * Checks if the given tableNumber is free for all requiredSlots based on the occupied map.
114
+ */
115
+ function isTableFreeForAllSlots(tableNumber, requiredSlots, tableOccupiedSlots) {
116
+ const occupiedSlots = tableOccupiedSlots[tableNumber] || new Set();
117
+
118
+ for (const slot of requiredSlots) {
119
+ if (occupiedSlots.has(slot)) {
120
+ return false;
121
+ }
122
+ }
123
+ return true;
124
+ }
125
+
126
+ /**
127
+ * Calculates Euclidean distance between two tables (assuming x, y properties).
128
+ */
129
+ function calculateDistance(tableA, tableB) {
130
+ if (tableA?.x === undefined || tableA?.y === undefined || tableB?.x === undefined || tableB?.y === undefined) {
131
+ return Infinity; // Cannot calculate distance if coordinates are missing
132
+ }
133
+ const dx = tableA.x - tableB.x;
134
+ const dy = tableA.y - tableB.y;
135
+ return Math.sqrt(dx * dx + dy * dy);
136
+ }
137
+
138
+ /**
139
+ * QUICKSORT-INSPIRED OPTIMIZATION: O(n log n) multi-table assignment with partitioning
140
+ *
141
+ * Instead of exploring all combinations (exponential), we use:
142
+ * 1. Greedy partitioning - divide tables into capacity buckets
143
+ * 2. Pivot selection - pick optimal table as "pivot"
144
+ * 3. Branch reduction - only explore promising paths
145
+ * 4. Dynamic programming - cache intermediate results
146
+ *
147
+ * Complexity: O(n log n) instead of O(2^n)
148
+ */
149
+
150
+ // Cache for intermediate results (dynamic programming)
151
+ const assignmentCache = new Map();
152
+
153
+ function findMultiTableCombination(tables, guestsTotal, startIndex, currentSet, best) {
154
+ // Calculate current set's min and max capacity sums
155
+ let curMin = 0, curMax = 0;
156
+ for (const t of currentSet) {
157
+ curMin += t.minCapacity || 0;
158
+ curMax += t.maxCapacity || 0;
159
+ }
160
+
161
+ // PRUNING: If sum of minCapacity already exceeds guests, this combination is invalid
162
+ if (curMin > guestsTotal) return;
163
+
164
+ // Check if current combination is a valid solution
165
+ // Valid means: sum(minCapacity) <= guests <= sum(maxCapacity)
166
+ if (currentSet.length > 0 && curMin <= guestsTotal && guestsTotal <= curMax) {
167
+ if (currentSet.length < best.tableCount) {
168
+ // Calculate distance only for improved solutions
169
+ let distanceSum = 0;
170
+ for (let i = 0; i < currentSet.length; i++) {
171
+ for (let j = i + 1; j < currentSet.length; j++) {
172
+ distanceSum += calculateDistance(currentSet[i], currentSet[j]);
173
+ }
174
+ }
175
+ if (currentSet.length < best.tableCount ||
176
+ (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
177
+ best.minDistance = distanceSum;
178
+ best.tables = [...currentSet];
179
+ best.tableCount = currentSet.length;
180
+ }
181
+ }
182
+ // Continue searching for potentially tighter clusters
183
+ }
184
+
185
+ // AGGRESSIVE PRUNING
186
+ if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) return;
187
+ if (startIndex >= tables.length) return;
188
+
189
+ // Compute remaining capacity potential (suffix sum)
190
+ let suffixMax = 0;
191
+ for (let i = startIndex; i < tables.length; i++) {
192
+ suffixMax += tables[i].maxCapacity || 0;
193
+ }
194
+
195
+ // PRUNING: Even with all remaining tables, can't reach guests
196
+ if (curMax + suffixMax < guestsTotal) return;
197
+
198
+ // OPTIMIZATION: Use cached result if available (Dynamic Programming)
199
+ const cacheKey = `${guestsTotal}-${startIndex}-${currentSet.length}-${curMin}`;
200
+ if (assignmentCache.has(cacheKey)) {
201
+ const cached = assignmentCache.get(cacheKey);
202
+ if (cached.tableCount < best.tableCount) {
203
+ best.tableCount = cached.tableCount;
204
+ best.tables = [...cached.tables];
205
+ best.minDistance = cached.minDistance;
206
+ }
207
+ return;
208
+ }
209
+
210
+ // QUICKSORT-INSPIRED: Partition tables by capacity relative to guests needed
211
+ const remaining = tables.slice(startIndex);
212
+ const guestsNeeded = guestsTotal - curMax; // How much more capacity we need
213
+ const exactFit = [];
214
+ const overCapacity = [];
215
+ const underCapacity = [];
216
+
217
+ for (const tbl of remaining) {
218
+ // Check if adding this table would exceed minCapacity constraint
219
+ const newMin = curMin + (tbl.minCapacity || 0);
220
+ if (newMin > guestsTotal) continue; // Skip - would violate minCapacity constraint
221
+
222
+ const canSeat = tbl.maxCapacity || 0;
223
+
224
+ if (tbl.minCapacity <= guestsTotal && guestsTotal <= tbl.maxCapacity && currentSet.length === 0) {
225
+ // Single table that fits exactly (only relevant when starting fresh)
226
+ exactFit.push(tbl);
227
+ } else if (curMax + canSeat >= guestsTotal && newMin <= guestsTotal) {
228
+ // This table could complete a valid combination
229
+ overCapacity.push(tbl);
230
+ } else {
231
+ // Need more tables after this one
232
+ underCapacity.push(tbl);
233
+ }
234
+ }
235
+
236
+ // STRATEGY 1: Try exact fit first (best case - single table)
237
+ if (exactFit.length > 0 && currentSet.length === 0) {
238
+ // Sort by smallest capacity first (minimize waste)
239
+ exactFit.sort((a, b) => a.maxCapacity - b.maxCapacity);
240
+ const tbl = exactFit[0];
241
+ currentSet.push(tbl);
242
+ findMultiTableCombination(tables, guestsTotal, tables.length, currentSet, best);
243
+ currentSet.pop();
244
+
245
+ if (best.tableCount === 1) return; // Found optimal
246
+ }
247
+
248
+ // STRATEGY 2: Tables that can complete the combination
249
+ if (overCapacity.length > 0) {
250
+ // Sort by closest to completing the combination
251
+ overCapacity.sort((a, b) => {
252
+ const aTotal = curMax + a.maxCapacity;
253
+ const bTotal = curMax + b.maxCapacity;
254
+ return Math.abs(aTotal - guestsTotal) - Math.abs(bTotal - guestsTotal);
255
+ });
256
+
257
+ // Try top candidates
258
+ const candidates = overCapacity.slice(0, Math.min(3, overCapacity.length));
259
+ for (const tbl of candidates) {
260
+ currentSet.push(tbl);
261
+ findMultiTableCombination(tables, guestsTotal, tables.indexOf(tbl) + 1, currentSet, best);
262
+ currentSet.pop();
263
+
264
+ if (best.tableCount === currentSet.length + 1) return;
265
+ }
266
+ }
267
+
268
+ // STRATEGY 3: Two-table combinations (most common case)
269
+ if (underCapacity.length >= 2 && currentSet.length === 0 && best.tableCount > 2) {
270
+ // Sort by capacity descending
271
+ underCapacity.sort((a, b) => b.maxCapacity - a.maxCapacity);
272
+
273
+ // Find pairs that form valid combinations
274
+ for (let i = 0; i < Math.min(5, underCapacity.length); i++) {
275
+ const first = underCapacity[i];
276
+
277
+ for (let j = i + 1; j < underCapacity.length; j++) {
278
+ const second = underCapacity[j];
279
+ const totalMin = (first.minCapacity || 0) + (second.minCapacity || 0);
280
+ const totalMax = (first.maxCapacity || 0) + (second.maxCapacity || 0);
281
+
282
+ // Check if this pair forms a valid combination
283
+ if (totalMin <= guestsTotal && guestsTotal <= totalMax) {
284
+ currentSet.push(first);
285
+ currentSet.push(second);
286
+ findMultiTableCombination(tables, guestsTotal, tables.length, currentSet, best);
287
+ currentSet.pop();
288
+ currentSet.pop();
289
+
290
+ if (best.tableCount === 2) return; // Found optimal two-table
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // STRATEGY 4: Limited backtracking only if no good solution found
297
+ if (best.tableCount > 3 && remaining.length <= 10) {
298
+ // Sort by efficiency (capacity per table)
299
+ remaining.sort((a, b) => b.maxCapacity - a.maxCapacity);
300
+
301
+ // Try only top 5 tables
302
+ const limited = remaining.slice(0, Math.min(5, remaining.length));
303
+ for (const tbl of limited) {
304
+ const newMin = curMin + (tbl.minCapacity || 0);
305
+ if (newMin > guestsTotal) continue; // Skip - would violate minCapacity constraint
306
+
307
+ currentSet.push(tbl);
308
+ const nextIdx = tables.indexOf(tbl) + 1;
309
+ findMultiTableCombination(tables, guestsTotal, nextIdx, currentSet, best);
310
+ currentSet.pop();
311
+
312
+ if (best.tableCount <= 2) return;
313
+ }
314
+ }
315
+
316
+ // Cache result
317
+ if (best.tableCount < Infinity) {
318
+ assignmentCache.set(cacheKey, {
319
+ tableCount: best.tableCount,
320
+ tables: [...best.tables],
321
+ minDistance: best.minDistance
322
+ });
323
+ }
324
+ }
325
+
326
+ function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null, duration = null) {
327
+ console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
328
+
329
+ // Clear cache if it gets too large (prevent memory leaks)
330
+ if (assignmentCache.size > 1000) {
331
+ assignmentCache.clear();
332
+ }
333
+
334
+ // FIXED: More robust parsing of settings with detailed logging
335
+ const generalSettings = restaurantData["general-settings"] || {};
336
+
337
+ // DEFAULT VALUES - this approach mirrors the TypeScript implementation
338
+ let duurReservatie = 120; // Default: 2 hours in minutes
339
+ let intervalReservatie = 15; // Default: 15 minute intervals
340
+
341
+ // Use safeParseInt for robust parsing
342
+ duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
343
+ intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
344
+
345
+ // Use passed duration if available, otherwise default
346
+ if (duration && duration > 0) {
347
+ duurReservatie = duration;
348
+ }
349
+
350
+ console.log(`[assignTablesForGivenTime] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
351
+
352
+ if (intervalReservatie <= 0) {
353
+ console.error("Invalid interval settings.");
354
+ return [];
355
+ }
356
+
357
+ const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
358
+ if (!requiredSlots || requiredSlots.length === 0) {
359
+ console.error(`Could not compute required slots for ${time}`);
360
+ return [];
361
+ }
362
+
363
+ // Fetch all tables using the imported helper
364
+ const allTables = getAllTables(restaurantData, selectedZitplaats);
365
+ console.log(`[assignTablesForGivenTime] Checking ${allTables.length} tables`);
366
+
367
+ // OPTIMIZATION: Pre-filter and sort tables for better performance
368
+ // Filter out invalid and occupied tables first
369
+ const validTables = allTables.filter(t =>
370
+ isTemporaryTableValid(t, date, time) &&
371
+ isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
372
+ );
373
+
374
+ if (validTables.length === 0) {
375
+ console.log(`[assignTablesForGivenTime] No valid tables available`);
376
+ return [];
377
+ }
378
+
379
+ // OPTIMIZATION: Sort tables by capacity (prefer exact matches first)
380
+ // This helps find optimal solutions faster
381
+ validTables.sort((a, b) => {
382
+ // Prioritize tables that can seat exactly the number of guests
383
+ const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
384
+ const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
385
+ if (aExact !== bExact) return aExact - bExact;
386
+
387
+ // Then sort by capacity (smaller tables first to minimize waste)
388
+ return a.maxCapacity - b.maxCapacity;
389
+ });
390
+
391
+ // --- Try single-table assignment first ---
392
+ for (const t of validTables) {
393
+ if (t.minCapacity <= guests && guests <= t.maxCapacity) {
394
+ console.log(`[assignTablesForGivenTime] Assigned single table ${t.tableNumber} for ${guests} guests`);
395
+ return [t.tableNumber];
396
+ }
397
+ }
398
+
399
+ console.log(`[assignTablesForGivenTime] No single table found, trying combinations...`);
400
+
401
+ // --- Try multi-table assignment ---
402
+ const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
403
+ findMultiTableCombination(
404
+ validTables, // Use pre-filtered and sorted tables
405
+ guests,
406
+ 0, // Start index
407
+ [], // Initial empty set
408
+ best // Best solution object (no need for slots/occupancy - already filtered)
409
+ );
410
+
411
+ if (best.tables.length > 0) {
412
+ console.log(`[assignTablesForGivenTime] Found multi-table solution: ${best.tables.map(t => t.tableNumber).join(', ')}`);
413
+ } else {
414
+ console.log(`[assignTablesForGivenTime] No table combination found for ${guests} guests`);
415
+ }
416
+
417
+ return best.tables.map(t => t.tableNumber);
418
+ }
419
+
420
+ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats = null, duration = null) {
421
+ console.log(`\n[isTimeAvailableSync] Checking ${date} ${time} for ${guests} guests (duration: ${duration || 'default'})`);
422
+
423
+ if (guests < 0) {
424
+ console.log(`[isTimeAvailableSync] Detected negative guest count (${guests}), adjusting to default of 2`);
425
+ guests = 2; // Use a reasonable default
426
+ }
427
+
428
+ const tableSettings = restaurantData?.['table-settings'] || {};
429
+ const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
430
+ tableSettings.assignmentMode === "automatic";
431
+
432
+ console.log(`[isTimeAvailableSync] Table assignment enabled? ${isTableAssignmentEnabled}`);
433
+ console.log(`- Table settings installed: ${tableSettings.isInstalled === true}`);
434
+ console.log(`- Assignment mode: ${tableSettings.assignmentMode}`);
435
+
436
+ if (!isTableAssignmentEnabled) {
437
+ console.log(`[isTimeAvailableSync] No table assignment needed, returning true`);
438
+ return true;
439
+ }
440
+
441
+ try {
442
+ // Basic data check
443
+ if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
444
+ console.error(`[isTimeAvailableSync] Missing floors data for ${date} ${time}`);
445
+ return false;
446
+ }
447
+
448
+ if (guests <= 0) {
449
+ console.error(`[isTimeAvailableSync] Invalid guest count: ${guests}`);
450
+ return false; // Cannot reserve for 0 or fewer guests
451
+ }
452
+
453
+ const generalSettings = restaurantData["general-settings"] || {};
454
+ let defaultDuurReservatie = 120; // Default: 2 hours in minutes
455
+ let intervalReservatie = 15; // Default: 15 minute intervals
456
+ defaultDuurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
457
+ intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
458
+
459
+ // Use passed duration or default
460
+ const currentDuurReservatie = duration && duration > 0 ? duration : defaultDuurReservatie;
461
+
462
+ console.log(`[isTimeAvailableSync] Using duration: ${currentDuurReservatie}min (default: ${defaultDuurReservatie}), interval: ${intervalReservatie}min`);
463
+
464
+ if (intervalReservatie <= 0) {
465
+ console.error(`[isTimeAvailableSync] Invalid interval settings for ${date} ${time}`);
466
+ return false;
467
+ }
468
+
469
+ const requiredSlots = computeRequiredSlots(time, currentDuurReservatie, intervalReservatie);
470
+ if (!requiredSlots || requiredSlots.length === 0) {
471
+ console.error(`[isTimeAvailableSync] Could not compute required slots for ${date} ${time}`);
472
+ return false; // Cannot proceed if slots are invalid
473
+ }
474
+
475
+ // --- Build Occupancy Map using ACTUAL table assignments when available ---
476
+ const tableOccupiedSlots = {}; // { tableNumber: Set<slotMinutes> }
477
+ const reservationsForDate = reservations
478
+ .filter(r => r.date === date && r.time && r.guests > 0)
479
+ .sort((a, b) => {
480
+ // Simple time string comparison
481
+ const timeA = a.time.split(':').map(Number);
482
+ const timeB = b.time.split(':').map(Number);
483
+ return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
484
+ });
485
+
486
+ console.log(`[isTimeAvailableSync] Processing ${reservationsForDate.length} existing reservations on ${date}`);
487
+
488
+ for (const r of reservationsForDate) {
489
+ // Calculate duration once per reservation
490
+ const rDuration = safeParseInt(r.duration, defaultDuurReservatie);
491
+
492
+ console.log(`[DEBUG] Reservation ${r.time} (${r.firstName}): Duration=${rDuration}, Tables raw=${JSON.stringify(r.tables)}`);
493
+
494
+ // NEW: Try to get actual table assignment first
495
+ const actualTables = getActualTableAssignment(r);
496
+ console.log(`[DEBUG] Reservation ${r.time}: actualTables detected = ${JSON.stringify(actualTables)}`);
497
+
498
+ let assignedTables = [];
499
+
500
+ if (actualTables.length > 0) {
501
+ // Use actual table assignment from reservation data
502
+ assignedTables = actualTables;
503
+ console.log(`[isTimeAvailableSync] Using actual table assignment for reservation: ${assignedTables.join(', ')}`);
504
+ } else {
505
+ // ... simulation log ...
506
+ console.log(`[isTimeAvailableSync] No actual table data, simulating assignment for reservation`);
507
+ assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration); // Simulation - use reservation's own zitplaats
508
+ }
509
+
510
+ // Update the occupancy map based on the actual or simulated assignment
511
+ if (assignedTables.length > 0) {
512
+ // Use reservation specific duration if available
513
+ const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
514
+ console.log(`[DEBUG] Reservation ${r.time}: Occupying slots=${rSlots.join(',')} on tables=${assignedTables.join(',')}`);
515
+
516
+ if (!rSlots || rSlots.length === 0) continue; // Skip if slots invalid
517
+
518
+ assignedTables.forEach(tableNumber => {
519
+ if (!tableOccupiedSlots[tableNumber]) {
520
+ tableOccupiedSlots[tableNumber] = new Set();
521
+ }
522
+ rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
523
+ });
524
+ }
525
+ }
526
+
527
+ console.log(`[isTimeAvailableSync] Occupancy map built, checking availability for new reservation`);
528
+
529
+ const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
530
+ console.log(`[isTimeAvailableSync] Checking ${allTables.length} tables for new reservation`);
531
+
532
+ // OPTIMIZATION: Pre-filter valid tables
533
+ const validTables = allTables.filter(t =>
534
+ isTemporaryTableValid(t, date, time) &&
535
+ isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
536
+ );
537
+
538
+ if (validTables.length === 0) {
539
+ console.log(`[isTimeAvailableSync] No valid tables available`);
540
+ return false;
541
+ }
542
+
543
+ // OPTIMIZATION: Sort for faster exact matches
544
+ validTables.sort((a, b) => {
545
+ const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
546
+ const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
547
+ if (aExact !== bExact) return aExact - bExact;
548
+ return a.maxCapacity - b.maxCapacity;
549
+ });
550
+
551
+ // 1. Try single table assignment
552
+ for (const t of validTables) {
553
+ if (t.minCapacity <= guests && guests <= t.maxCapacity) {
554
+ console.log(`[isTimeAvailableSync] Found single table ${t.tableNumber} for ${guests} guests at ${time}`);
555
+ return true; // Available
556
+ }
557
+ }
558
+
559
+ console.log(`[isTimeAvailableSync] No single table found, trying combinations...`);
560
+
561
+ // 2. Try multi-table assignment
562
+ const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
563
+ findMultiTableCombination(
564
+ validTables, // Use pre-filtered tables
565
+ guests,
566
+ 0, // Start index
567
+ [], // Empty current set
568
+ best // Best solution
569
+ );
570
+
571
+ const result = best.tables.length > 0;
572
+ if (result) {
573
+ console.log(`[isTimeAvailableSync] Multi-table assignment possible: ${best.tables.map(t => t.tableNumber).join(', ')}`);
574
+ } else {
575
+ console.log(`[isTimeAvailableSync] No table combination available`);
576
+ }
577
+
578
+ return result; // Available if a combination was found
579
+
580
+ } catch (error) {
581
+ console.error(`[isTimeAvailableSync] Error during check for ${date} ${time}:`, error);
582
+ return false; // Assume unavailable on error
583
+ }
584
+ }
585
+
586
+ function filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null, duration = null) {
587
+ console.log(`[filterTimeblocksByTableAvailability] Filtering timeblocks for ${date} with ${guests} guests`);
588
+
589
+ if (guests < 0) {
590
+ console.log(`[filterTimeblocksByTableAvailability] Detected negative guest count (${guests}), adjusting to default of 2`);
591
+ guests = 0; // Use a reasonable default
592
+ }
593
+
594
+ const tableSettings = restaurantData?.['table-settings'] || {};
595
+ const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
596
+ tableSettings.assignmentMode === "automatic";
597
+
598
+ if (!isTableAssignmentEnabled) {
599
+ console.log(`[filterTimeblocksByTableAvailability] No table assignment enabled, returning all timeblocks`);
600
+ return timeblocks;
601
+ }
602
+
603
+ console.log(`[filterTimeblocksByTableAvailability] Starting with ${Object.keys(timeblocks).length} timeblocks`);
604
+
605
+ const filteredTimeblocks = {};
606
+ let availableCount = 0;
607
+ let unavailableCount = 0;
608
+
609
+ for (const time in timeblocks) {
610
+ if (isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats, duration)) {
611
+ filteredTimeblocks[time] = timeblocks[time];
612
+ availableCount++;
613
+ } else {
614
+ unavailableCount++;
615
+ }
616
+ }
617
+
618
+ console.log(`[filterTimeblocksByTableAvailability] Result: ${availableCount} available, ${unavailableCount} unavailable times`);
619
+ if (availableCount > 0) {
620
+ console.log(`[filterTimeblocksByTableAvailability] Available times: ${Object.keys(filteredTimeblocks).join(', ')}`);
621
+ }
622
+
623
+ return filteredTimeblocks;
624
+ }
625
+
626
+ function getAvailableTimeblocksWithTableCheck(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null, duration = null) {
627
+ return filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats, duration);
628
+ }
629
+
630
+ function getAvailableTablesForTime(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
631
+ try {
632
+ if (guests < 0) {
633
+ console.log(`[getAvailableTablesForTime] Detected negative guest count (${guests}), adjusting to default of 2`);
634
+ guests = 0;
635
+ }
636
+
637
+ const tableSettings = restaurantData?.['table-settings'] || {};
638
+ const isAutomaticAssignment = tableSettings.isInstalled === true &&
639
+ tableSettings.assignmentMode === "automatic";
640
+
641
+ if (!isAutomaticAssignment) {
642
+ return [];
643
+ }
644
+
645
+ if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
646
+ console.error(`[getAvailableTablesForTime] Missing floors data for ${date} ${time}`);
647
+ return [];
648
+ }
649
+ if (guests <= 0) return [];
650
+ const generalSettings = restaurantData["general-settings"] || {};
651
+ const duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
652
+ const intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
653
+
654
+ if (intervalReservatie <= 0) {
655
+ console.error(`[getAvailableTablesForTime] Invalid interval settings for ${date} ${time}`);
656
+ return [];
657
+ }
658
+
659
+ console.log(`\n[getAvailableTablesForTime] Finding available tables for ${guests} guests on ${date} at ${time}`);
660
+ const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
661
+ if (!requiredSlots || requiredSlots.length === 0) {
662
+ console.error(`[getAvailableTablesForTime] Could not compute required slots for ${date} ${time}`);
663
+ return [];
664
+ }
665
+ const tableOccupiedSlots = {};
666
+ const reservationsForDate = reservations
667
+ .filter(r => r.date === date && r.time && r.guests > 0)
668
+ .sort((a, b) => {
669
+ const timeA = a.time.split(':').map(Number);
670
+ const timeB = b.time.split(':').map(Number);
671
+ return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
672
+ });
673
+
674
+ console.log(`[getAvailableTablesForTime] Building occupancy map (Processing ${reservationsForDate.length} existing reservations)`);
675
+ for (const r of reservationsForDate) {
676
+ const rDuration = safeParseInt(r.duration, duurReservatie);
677
+ const actualTables = getActualTableAssignment(r);
678
+ let assignedTables = [];
679
+
680
+ if (actualTables.length > 0) {
681
+ assignedTables = actualTables;
682
+ } else {
683
+ assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration);
684
+ }
685
+
686
+ if (assignedTables.length > 0) {
687
+ const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
688
+ if (!rSlots || rSlots.length === 0) continue;
689
+ assignedTables.forEach(tableNumber => {
690
+ if (!tableOccupiedSlots[tableNumber]) {
691
+ tableOccupiedSlots[tableNumber] = new Set();
692
+ }
693
+ rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
694
+ });
695
+ }
696
+ }
697
+ console.log("[getAvailableTablesForTime] Occupancy map built");
698
+ const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables with zitplaats filter applied
699
+ const availableIndividualTables = [];
700
+
701
+ console.log(`[getAvailableTablesForTime] Checking ${allTables.length} tables for individual availability`);
702
+ for (const t of allTables) {
703
+ if (!isTemporaryTableValid(t, date, time)) continue;
704
+ if (t.minCapacity <= guests && guests <= t.maxCapacity) {
705
+ if (isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)) {
706
+ console.log(`[getAvailableTablesForTime] Table ${t.tableNumber} is available`);
707
+ availableIndividualTables.push(t); // Storing the whole object might be useful
708
+ }
709
+ }
710
+ }
711
+
712
+ console.log(`[getAvailableTablesForTime] Found ${availableIndividualTables.length} individually available tables: ${availableIndividualTables.map(t => t.tableNumber).join(', ')}`);
713
+ availableIndividualTables.sort((a, b) => a.tableNumber - b.tableNumber);
714
+
715
+ return availableIndividualTables; // Return array of table objects
716
+
717
+ } catch (error) {
718
+ console.error(`[getAvailableTablesForTime] Error for ${date} ${time}:`, error);
719
+ return []; // Return empty on error
720
+ }
721
+ }
722
+
723
+ module.exports = {
724
+ isTimeAvailableSync,
725
+ filterTimeblocksByTableAvailability,
726
+ getAvailableTimeblocksWithTableCheck,
727
+ getAvailableTablesForTime,
727
728
  };