@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.
Files changed (57) hide show
  1. package/.github/workflows/ci-cd.yml +234 -234
  2. package/BRANCH_PROTECTION_SETUP.md +167 -167
  3. package/CHANGELOG.md +8 -8
  4. package/README.md +144 -144
  5. package/RESERVERINGEN_GIDS.md +986 -986
  6. package/__tests__/filters.test.js +276 -276
  7. package/__tests__/isDateAvailable.test.js +175 -175
  8. package/__tests__/isTimeAvailable.test.js +168 -168
  9. package/__tests__/restaurantData.test.js +422 -422
  10. package/__tests__/tableHelpers.test.js +247 -247
  11. package/assignTables.js +424 -424
  12. package/changes/2025/December/PR2___change.md +14 -14
  13. package/changes/2025/December/PR3_add__change.md +20 -20
  14. package/changes/2025/December/PR4___.md +15 -15
  15. package/changes/2025/December/PR5___.md +15 -15
  16. package/changes/2025/December/PR6__del_.md +17 -17
  17. package/changes/2025/December/PR7_add__change.md +21 -21
  18. package/changes/2026/January/PR8_add__change.md +39 -0
  19. package/changes/2026/January/PR9_add__change.md +20 -0
  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 +158 -158
  24. package/grouping.js +162 -162
  25. package/index.js +42 -42
  26. package/isDateAvailable.js +80 -80
  27. package/isDateAvailableWithTableCheck.js +171 -171
  28. package/isTimeAvailable.js +25 -25
  29. package/jest.config.js +23 -23
  30. package/package.json +27 -27
  31. package/processing/dailyGuestCounts.js +73 -73
  32. package/processing/mealTypeCount.js +133 -133
  33. package/processing/timeblocksAvailable.js +167 -167
  34. package/reservation_data/counter.js +64 -64
  35. package/restaurant_data/exceptions.js +149 -149
  36. package/restaurant_data/openinghours.js +123 -123
  37. package/simulateTableAssignment.js +709 -699
  38. package/tableHelpers.js +178 -178
  39. package/tables/time/parseTime.js +19 -19
  40. package/tables/time/shifts.js +7 -7
  41. package/tables/utils/calculateDistance.js +13 -13
  42. package/tables/utils/isTableFreeForAllSlots.js +14 -14
  43. package/tables/utils/isTemporaryTableValid.js +39 -39
  44. package/test/test_counter.js +194 -194
  45. package/test/test_dailyCount.js +81 -81
  46. package/test/test_datesAvailable.js +106 -106
  47. package/test/test_exceptions.js +172 -172
  48. package/test/test_isDateAvailable.js +330 -330
  49. package/test/test_mealTypeCount.js +54 -54
  50. package/test/test_timesAvailable.js +88 -88
  51. package/test-detailed-filter.js +100 -100
  52. package/test-lunch-debug.js +110 -110
  53. package/test-max-arrivals-filter.js +79 -79
  54. package/test-meal-stop-fix.js +147 -147
  55. package/test-meal-stop-simple.js +93 -93
  56. package/test-timezone-debug.js +47 -47
  57. package/test.js +336 -336
@@ -1,700 +1,710 @@
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, guestsNeeded, startIndex, currentSet, best) {
153
- // Base case: All guests seated
154
- if (guestsNeeded <= 0) {
155
- if (currentSet.length < best.tableCount) {
156
- // Calculate distance only for improved solutions
157
- let distanceSum = 0;
158
- for (let i = 0; i < currentSet.length; i++) {
159
- for (let j = i + 1; j < currentSet.length; j++) {
160
- distanceSum += calculateDistance(currentSet[i], currentSet[j]);
161
- }
162
- }
163
- if (currentSet.length < best.tableCount ||
164
- (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
165
- best.minDistance = distanceSum;
166
- best.tables = [...currentSet];
167
- best.tableCount = currentSet.length;
168
- }
169
- }
170
- return;
171
- }
172
-
173
- // AGGRESSIVE PRUNING
174
- if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) return;
175
- if (startIndex >= tables.length) return;
176
-
177
- // OPTIMIZATION: Use cached result if available (Dynamic Programming)
178
- const cacheKey = `${guestsNeeded}-${startIndex}-${currentSet.length}`;
179
- if (assignmentCache.has(cacheKey)) {
180
- const cached = assignmentCache.get(cacheKey);
181
- if (cached.tableCount < best.tableCount) {
182
- best.tableCount = cached.tableCount;
183
- best.tables = [...cached.tables];
184
- best.minDistance = cached.minDistance;
185
- }
186
- return;
187
- }
188
-
189
- // QUICKSORT-INSPIRED: Partition tables by capacity relative to guests needed
190
- const remaining = tables.slice(startIndex);
191
- const exactFit = [];
192
- const overCapacity = [];
193
- const underCapacity = [];
194
-
195
- for (const tbl of remaining) {
196
- const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
197
- if (canSeat < tbl.minCapacity && canSeat < guestsNeeded) continue;
198
-
199
- if (tbl.minCapacity <= guestsNeeded && guestsNeeded <= tbl.maxCapacity) {
200
- exactFit.push(tbl);
201
- } else if (tbl.maxCapacity > guestsNeeded) {
202
- overCapacity.push(tbl);
203
- } else {
204
- underCapacity.push(tbl);
205
- }
206
- }
207
-
208
- // STRATEGY 1: Try exact fit first (best case - single table)
209
- if (exactFit.length > 0) {
210
- // Sort by smallest capacity first (minimize waste)
211
- exactFit.sort((a, b) => a.maxCapacity - b.maxCapacity);
212
- const tbl = exactFit[0];
213
- currentSet.push(tbl);
214
- findMultiTableCombination(tables, 0, tables.length, currentSet, best);
215
- currentSet.pop();
216
-
217
- if (best.tableCount === 1) return; // Found optimal
218
- }
219
-
220
- // STRATEGY 2: Greedy approach - largest table that fits
221
- if (overCapacity.length > 0 && currentSet.length < 2) {
222
- // Sort by closest to guests needed (minimize waste)
223
- overCapacity.sort((a, b) =>
224
- Math.abs(a.maxCapacity - guestsNeeded) - Math.abs(b.maxCapacity - guestsNeeded)
225
- );
226
-
227
- // Try top 3 candidates only (not all)
228
- const candidates = overCapacity.slice(0, Math.min(3, overCapacity.length));
229
- for (const tbl of candidates) {
230
- currentSet.push(tbl);
231
- findMultiTableCombination(tables, 0, tables.length, currentSet, best);
232
- currentSet.pop();
233
-
234
- if (best.tableCount === 1) return;
235
- }
236
- }
237
-
238
- // STRATEGY 3: Two-table combinations (most common case)
239
- if (underCapacity.length >= 2 && currentSet.length === 0 && best.tableCount > 2) {
240
- // Sort by capacity descending
241
- underCapacity.sort((a, b) => b.maxCapacity - a.maxCapacity);
242
-
243
- // QUICKSORT PARTITION: Find pairs that sum close to guestsNeeded
244
- for (let i = 0; i < Math.min(5, underCapacity.length); i++) {
245
- const first = underCapacity[i];
246
- const remaining = guestsNeeded - first.maxCapacity;
247
-
248
- // Binary search for best match (O(log n))
249
- let left = i + 1, right = underCapacity.length - 1;
250
- let bestMatch = null;
251
- let bestDiff = Infinity;
252
-
253
- while (left <= right) {
254
- const mid = Math.floor((left + right) / 2);
255
- const second = underCapacity[mid];
256
- const totalCapacity = first.maxCapacity + second.maxCapacity;
257
- const diff = Math.abs(totalCapacity - guestsNeeded);
258
-
259
- if (diff < bestDiff && totalCapacity >= guestsNeeded) {
260
- bestDiff = diff;
261
- bestMatch = second;
262
- }
263
-
264
- if (totalCapacity < guestsNeeded) {
265
- left = mid + 1;
266
- } else {
267
- right = mid - 1;
268
- }
269
- }
270
-
271
- if (bestMatch) {
272
- currentSet.push(first);
273
- currentSet.push(bestMatch);
274
- findMultiTableCombination(tables,
275
- guestsNeeded - first.maxCapacity - bestMatch.maxCapacity,
276
- tables.length, currentSet, best);
277
- currentSet.pop();
278
- currentSet.pop();
279
-
280
- if (best.tableCount === 2) return; // Found optimal two-table
281
- }
282
- }
283
- }
284
-
285
- // STRATEGY 4: Limited backtracking only if no good solution found
286
- if (best.tableCount > 3 && remaining.length <= 10) {
287
- // Sort by efficiency (capacity per table)
288
- remaining.sort((a, b) => b.maxCapacity - a.maxCapacity);
289
-
290
- // Try only top 5 tables
291
- const limited = remaining.slice(0, Math.min(5, remaining.length));
292
- for (const tbl of limited) {
293
- const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
294
- if (canSeat >= tbl.minCapacity || canSeat >= guestsNeeded) {
295
- currentSet.push(tbl);
296
- const nextIdx = tables.indexOf(tbl) + 1;
297
- findMultiTableCombination(tables, guestsNeeded - canSeat, nextIdx, currentSet, best);
298
- currentSet.pop();
299
-
300
- if (best.tableCount <= 2) return;
301
- }
302
- }
303
- }
304
-
305
- // Cache result
306
- if (best.tableCount < Infinity) {
307
- assignmentCache.set(cacheKey, {
308
- tableCount: best.tableCount,
309
- tables: [...best.tables],
310
- minDistance: best.minDistance
311
- });
312
- }
313
- }
314
-
315
- function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null) {
316
- console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
317
-
318
- // Clear cache if it gets too large (prevent memory leaks)
319
- if (assignmentCache.size > 1000) {
320
- assignmentCache.clear();
321
- }
322
-
323
- // FIXED: More robust parsing of settings with detailed logging
324
- const generalSettings = restaurantData["general-settings"] || {};
325
-
326
- // DEFAULT VALUES - this approach mirrors the TypeScript implementation
327
- let duurReservatie = 120; // Default: 2 hours in minutes
328
- let intervalReservatie = 15; // Default: 15 minute intervals
329
-
330
- // Use safeParseInt for robust parsing
331
- duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
332
- intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
333
-
334
- console.log(`[assignTablesForGivenTime] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
335
-
336
- if (intervalReservatie <= 0) {
337
- console.error("Invalid interval settings.");
338
- return [];
339
- }
340
-
341
- const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
342
- if (!requiredSlots || requiredSlots.length === 0) {
343
- console.error(`Could not compute required slots for ${time}`);
344
- return [];
345
- }
346
-
347
- // Fetch all tables using the imported helper
348
- const allTables = getAllTables(restaurantData, selectedZitplaats);
349
- console.log(`[assignTablesForGivenTime] Checking ${allTables.length} tables`);
350
-
351
- // OPTIMIZATION: Pre-filter and sort tables for better performance
352
- // Filter out invalid and occupied tables first
353
- const validTables = allTables.filter(t =>
354
- isTemporaryTableValid(t, date, time) &&
355
- isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
356
- );
357
-
358
- if (validTables.length === 0) {
359
- console.log(`[assignTablesForGivenTime] No valid tables available`);
360
- return [];
361
- }
362
-
363
- // OPTIMIZATION: Sort tables by capacity (prefer exact matches first)
364
- // This helps find optimal solutions faster
365
- validTables.sort((a, b) => {
366
- // Prioritize tables that can seat exactly the number of guests
367
- const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
368
- const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
369
- if (aExact !== bExact) return aExact - bExact;
370
-
371
- // Then sort by capacity (smaller tables first to minimize waste)
372
- return a.maxCapacity - b.maxCapacity;
373
- });
374
-
375
- // --- Try single-table assignment first ---
376
- for (const t of validTables) {
377
- if (t.minCapacity <= guests && guests <= t.maxCapacity) {
378
- console.log(`[assignTablesForGivenTime] Assigned single table ${t.tableNumber} for ${guests} guests`);
379
- return [t.tableNumber];
380
- }
381
- }
382
-
383
- console.log(`[assignTablesForGivenTime] No single table found, trying combinations...`);
384
-
385
- // --- Try multi-table assignment ---
386
- const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
387
- findMultiTableCombination(
388
- validTables, // Use pre-filtered and sorted tables
389
- guests,
390
- 0, // Start index
391
- [], // Initial empty set
392
- best // Best solution object (no need for slots/occupancy - already filtered)
393
- );
394
-
395
- if (best.tables.length > 0) {
396
- console.log(`[assignTablesForGivenTime] Found multi-table solution: ${best.tables.map(t => t.tableNumber).join(', ')}`);
397
- } else {
398
- console.log(`[assignTablesForGivenTime] No table combination found for ${guests} guests`);
399
- }
400
-
401
- return best.tables.map(t => t.tableNumber);
402
- }
403
-
404
- function isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
405
- console.log(`\n[isTimeAvailableSync] Checking ${date} ${time} for ${guests} guests`);
406
-
407
- if (guests < 0) {
408
- console.log(`[isTimeAvailableSync] Detected negative guest count (${guests}), adjusting to default of 2`);
409
- guests = 2; // Use a reasonable default
410
- }
411
-
412
- const tableSettings = restaurantData?.['table-settings'] || {};
413
- const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
414
- tableSettings.assignmentMode === "automatic";
415
-
416
- console.log(`[isTimeAvailableSync] Table assignment enabled? ${isTableAssignmentEnabled}`);
417
- console.log(`- Table settings installed: ${tableSettings.isInstalled === true}`);
418
- console.log(`- Assignment mode: ${tableSettings.assignmentMode}`);
419
-
420
- if (!isTableAssignmentEnabled) {
421
- console.log(`[isTimeAvailableSync] No table assignment needed, returning true`);
422
- return true;
423
- }
424
-
425
- try {
426
- // Basic data check
427
- if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
428
- console.error(`[isTimeAvailableSync] Missing floors data for ${date} ${time}`);
429
- return false;
430
- }
431
-
432
- if (guests <= 0) {
433
- console.error(`[isTimeAvailableSync] Invalid guest count: ${guests}`);
434
- return false; // Cannot reserve for 0 or fewer guests
435
- }
436
-
437
- const generalSettings = restaurantData["general-settings"] || {};
438
- let duurReservatie = 120; // Default: 2 hours in minutes
439
- let intervalReservatie = 15; // Default: 15 minute intervals
440
- duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
441
- intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
442
-
443
- console.log(`[isTimeAvailableSync] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
444
-
445
- if (intervalReservatie <= 0) {
446
- console.error(`[isTimeAvailableSync] Invalid interval settings for ${date} ${time}`);
447
- return false;
448
- }
449
-
450
- const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
451
- if (!requiredSlots || requiredSlots.length === 0) {
452
- console.error(`[isTimeAvailableSync] Could not compute required slots for ${date} ${time}`);
453
- return false; // Cannot proceed if slots are invalid
454
- }
455
-
456
- // --- Build Occupancy Map using ACTUAL table assignments when available ---
457
- const tableOccupiedSlots = {}; // { tableNumber: Set<slotMinutes> }
458
- const reservationsForDate = reservations
459
- .filter(r => r.date === date && r.time && r.guests > 0)
460
- .sort((a, b) => {
461
- // Simple time string comparison
462
- const timeA = a.time.split(':').map(Number);
463
- const timeB = b.time.split(':').map(Number);
464
- return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
465
- });
466
-
467
- console.log(`[isTimeAvailableSync] Processing ${reservationsForDate.length} existing reservations on ${date}`);
468
-
469
- for (const r of reservationsForDate) {
470
- // NEW: Try to get actual table assignment first
471
- const actualTables = getActualTableAssignment(r);
472
- let assignedTables = [];
473
-
474
- if (actualTables.length > 0) {
475
- // Use actual table assignment from reservation data
476
- assignedTables = actualTables;
477
- console.log(`[isTimeAvailableSync] Using actual table assignment for reservation: ${assignedTables.join(', ')}`);
478
- } else {
479
- // Fall back to simulation for backwards compatibility
480
- console.log(`[isTimeAvailableSync] No actual table data, simulating assignment for reservation`);
481
- assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots);
482
- }
483
-
484
- // Update the occupancy map based on the actual or simulated assignment
485
- if (assignedTables.length > 0) {
486
- const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
487
- if (!rSlots || rSlots.length === 0) continue; // Skip if slots invalid
488
-
489
- assignedTables.forEach(tableNumber => {
490
- if (!tableOccupiedSlots[tableNumber]) {
491
- tableOccupiedSlots[tableNumber] = new Set();
492
- }
493
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
494
- });
495
-
496
- console.log(`[isTimeAvailableSync] Marked tables ${assignedTables.join(', ')} as occupied for time ${r.time}`);
497
- }
498
- }
499
-
500
- console.log(`[isTimeAvailableSync] Occupancy map built, checking availability for new reservation`);
501
-
502
- const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
503
- console.log(`[isTimeAvailableSync] Checking ${allTables.length} tables for new reservation`);
504
-
505
- // OPTIMIZATION: Pre-filter valid tables
506
- const validTables = allTables.filter(t =>
507
- isTemporaryTableValid(t, date, time) &&
508
- isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
509
- );
510
-
511
- if (validTables.length === 0) {
512
- console.log(`[isTimeAvailableSync] No valid tables available`);
513
- return false;
514
- }
515
-
516
- // OPTIMIZATION: Sort for faster exact matches
517
- validTables.sort((a, b) => {
518
- const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
519
- const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
520
- if (aExact !== bExact) return aExact - bExact;
521
- return a.maxCapacity - b.maxCapacity;
522
- });
523
-
524
- // 1. Try single table assignment
525
- for (const t of validTables) {
526
- if (t.minCapacity <= guests && guests <= t.maxCapacity) {
527
- console.log(`[isTimeAvailableSync] Found single table ${t.tableNumber} for ${guests} guests at ${time}`);
528
- return true; // Available
529
- }
530
- }
531
-
532
- console.log(`[isTimeAvailableSync] No single table found, trying combinations...`);
533
-
534
- // 2. Try multi-table assignment
535
- const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
536
- findMultiTableCombination(
537
- validTables, // Use pre-filtered tables
538
- guests,
539
- 0, // Start index
540
- [], // Empty current set
541
- best // Best solution
542
- );
543
-
544
- const result = best.tables.length > 0;
545
- if (result) {
546
- console.log(`[isTimeAvailableSync] Multi-table assignment possible: ${best.tables.map(t => t.tableNumber).join(', ')}`);
547
- } else {
548
- console.log(`[isTimeAvailableSync] No table combination available`);
549
- }
550
-
551
- return result; // Available if a combination was found
552
-
553
- } catch (error) {
554
- console.error(`[isTimeAvailableSync] Error during check for ${date} ${time}:`, error);
555
- return false; // Assume unavailable on error
556
- }
557
- }
558
-
559
- function filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null) {
560
- console.log(`[filterTimeblocksByTableAvailability] Filtering timeblocks for ${date} with ${guests} guests`);
561
-
562
- if (guests < 0) {
563
- console.log(`[filterTimeblocksByTableAvailability] Detected negative guest count (${guests}), adjusting to default of 2`);
564
- guests = 0; // Use a reasonable default
565
- }
566
-
567
- const tableSettings = restaurantData?.['table-settings'] || {};
568
- const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
569
- tableSettings.assignmentMode === "automatic";
570
-
571
- if (!isTableAssignmentEnabled) {
572
- console.log(`[filterTimeblocksByTableAvailability] No table assignment enabled, returning all timeblocks`);
573
- return timeblocks;
574
- }
575
-
576
- console.log(`[filterTimeblocksByTableAvailability] Starting with ${Object.keys(timeblocks).length} timeblocks`);
577
-
578
- const filteredTimeblocks = {};
579
- let availableCount = 0;
580
- let unavailableCount = 0;
581
-
582
- for (const time in timeblocks) {
583
- if (isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats)) {
584
- filteredTimeblocks[time] = timeblocks[time];
585
- availableCount++;
586
- } else {
587
- unavailableCount++;
588
- }
589
- }
590
-
591
- console.log(`[filterTimeblocksByTableAvailability] Result: ${availableCount} available, ${unavailableCount} unavailable times`);
592
- if (availableCount > 0) {
593
- console.log(`[filterTimeblocksByTableAvailability] Available times: ${Object.keys(filteredTimeblocks).join(', ')}`);
594
- }
595
-
596
- return filteredTimeblocks;
597
- }
598
-
599
- function getAvailableTimeblocksWithTableCheck(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null) {
600
- return filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats);
601
- }
602
-
603
- function getAvailableTablesForTime(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
604
- try {
605
- if (guests < 0) {
606
- console.log(`[getAvailableTablesForTime] Detected negative guest count (${guests}), adjusting to default of 2`);
607
- guests = 0;
608
- }
609
-
610
- const tableSettings = restaurantData?.['table-settings'] || {};
611
- const isAutomaticAssignment = tableSettings.isInstalled === true &&
612
- tableSettings.assignmentMode === "automatic";
613
-
614
- if (!isAutomaticAssignment) {
615
- return [];
616
- }
617
-
618
- if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
619
- console.error(`[getAvailableTablesForTime] Missing floors data for ${date} ${time}`);
620
- return [];
621
- }
622
- if (guests <= 0) return [];
623
- const generalSettings = restaurantData["general-settings"] || {};
624
- const duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
625
- const intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
626
-
627
- if (intervalReservatie <= 0) {
628
- console.error(`[getAvailableTablesForTime] Invalid interval settings for ${date} ${time}`);
629
- return [];
630
- }
631
-
632
- console.log(`\n[getAvailableTablesForTime] Finding available tables for ${guests} guests on ${date} at ${time}`);
633
- const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
634
- if (!requiredSlots || requiredSlots.length === 0) {
635
- console.error(`[getAvailableTablesForTime] Could not compute required slots for ${date} ${time}`);
636
- return [];
637
- }
638
- const tableOccupiedSlots = {};
639
- const reservationsForDate = reservations
640
- .filter(r => r.date === date && r.time && r.guests > 0)
641
- .sort((a, b) => {
642
- const timeA = a.time.split(':').map(Number);
643
- const timeB = b.time.split(':').map(Number);
644
- return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
645
- });
646
-
647
- console.log(`[getAvailableTablesForTime] Building occupancy map (Processing ${reservationsForDate.length} existing reservations)`);
648
- for (const r of reservationsForDate) {
649
- const actualTables = getActualTableAssignment(r);
650
- let assignedTables = [];
651
-
652
- if (actualTables.length > 0) {
653
- assignedTables = actualTables;
654
- } else {
655
- assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, null);
656
- }
657
-
658
- if (assignedTables.length > 0) {
659
- const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
660
- if (!rSlots || rSlots.length === 0) continue;
661
- assignedTables.forEach(tableNumber => {
662
- if (!tableOccupiedSlots[tableNumber]) {
663
- tableOccupiedSlots[tableNumber] = new Set();
664
- }
665
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
666
- });
667
- }
668
- }
669
- console.log("[getAvailableTablesForTime] Occupancy map built");
670
- const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables with zitplaats filter applied
671
- const availableIndividualTables = [];
672
-
673
- console.log(`[getAvailableTablesForTime] Checking ${allTables.length} tables for individual availability`);
674
- for (const t of allTables) {
675
- if (!isTemporaryTableValid(t, date, time)) continue;
676
- if (t.minCapacity <= guests && guests <= t.maxCapacity) {
677
- if (isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)) {
678
- console.log(`[getAvailableTablesForTime] Table ${t.tableNumber} is available`);
679
- availableIndividualTables.push(t); // Storing the whole object might be useful
680
- }
681
- }
682
- }
683
-
684
- console.log(`[getAvailableTablesForTime] Found ${availableIndividualTables.length} individually available tables: ${availableIndividualTables.map(t => t.tableNumber).join(', ')}`);
685
- availableIndividualTables.sort((a, b) => a.tableNumber - b.tableNumber);
686
-
687
- return availableIndividualTables; // Return array of table objects
688
-
689
- } catch (error) {
690
- console.error(`[getAvailableTablesForTime] Error for ${date} ${time}:`, error);
691
- return []; // Return empty on error
692
- }
693
- }
694
-
695
- module.exports = {
696
- isTimeAvailableSync,
697
- filterTimeblocksByTableAvailability,
698
- getAvailableTimeblocksWithTableCheck,
699
- 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
+ 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) {
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
+ console.log(`[assignTablesForGivenTime] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
345
+
346
+ if (intervalReservatie <= 0) {
347
+ console.error("Invalid interval settings.");
348
+ return [];
349
+ }
350
+
351
+ const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
352
+ if (!requiredSlots || requiredSlots.length === 0) {
353
+ console.error(`Could not compute required slots for ${time}`);
354
+ return [];
355
+ }
356
+
357
+ // Fetch all tables using the imported helper
358
+ const allTables = getAllTables(restaurantData, selectedZitplaats);
359
+ console.log(`[assignTablesForGivenTime] Checking ${allTables.length} tables`);
360
+
361
+ // OPTIMIZATION: Pre-filter and sort tables for better performance
362
+ // Filter out invalid and occupied tables first
363
+ const validTables = allTables.filter(t =>
364
+ isTemporaryTableValid(t, date, time) &&
365
+ isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
366
+ );
367
+
368
+ if (validTables.length === 0) {
369
+ console.log(`[assignTablesForGivenTime] No valid tables available`);
370
+ return [];
371
+ }
372
+
373
+ // OPTIMIZATION: Sort tables by capacity (prefer exact matches first)
374
+ // This helps find optimal solutions faster
375
+ validTables.sort((a, b) => {
376
+ // Prioritize tables that can seat exactly the number of guests
377
+ const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
378
+ const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
379
+ if (aExact !== bExact) return aExact - bExact;
380
+
381
+ // Then sort by capacity (smaller tables first to minimize waste)
382
+ return a.maxCapacity - b.maxCapacity;
383
+ });
384
+
385
+ // --- Try single-table assignment first ---
386
+ for (const t of validTables) {
387
+ if (t.minCapacity <= guests && guests <= t.maxCapacity) {
388
+ console.log(`[assignTablesForGivenTime] Assigned single table ${t.tableNumber} for ${guests} guests`);
389
+ return [t.tableNumber];
390
+ }
391
+ }
392
+
393
+ console.log(`[assignTablesForGivenTime] No single table found, trying combinations...`);
394
+
395
+ // --- Try multi-table assignment ---
396
+ const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
397
+ findMultiTableCombination(
398
+ validTables, // Use pre-filtered and sorted tables
399
+ guests,
400
+ 0, // Start index
401
+ [], // Initial empty set
402
+ best // Best solution object (no need for slots/occupancy - already filtered)
403
+ );
404
+
405
+ if (best.tables.length > 0) {
406
+ console.log(`[assignTablesForGivenTime] Found multi-table solution: ${best.tables.map(t => t.tableNumber).join(', ')}`);
407
+ } else {
408
+ console.log(`[assignTablesForGivenTime] No table combination found for ${guests} guests`);
409
+ }
410
+
411
+ return best.tables.map(t => t.tableNumber);
412
+ }
413
+
414
+ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
415
+ console.log(`\n[isTimeAvailableSync] Checking ${date} ${time} for ${guests} guests`);
416
+
417
+ if (guests < 0) {
418
+ console.log(`[isTimeAvailableSync] Detected negative guest count (${guests}), adjusting to default of 2`);
419
+ guests = 2; // Use a reasonable default
420
+ }
421
+
422
+ const tableSettings = restaurantData?.['table-settings'] || {};
423
+ const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
424
+ tableSettings.assignmentMode === "automatic";
425
+
426
+ console.log(`[isTimeAvailableSync] Table assignment enabled? ${isTableAssignmentEnabled}`);
427
+ console.log(`- Table settings installed: ${tableSettings.isInstalled === true}`);
428
+ console.log(`- Assignment mode: ${tableSettings.assignmentMode}`);
429
+
430
+ if (!isTableAssignmentEnabled) {
431
+ console.log(`[isTimeAvailableSync] No table assignment needed, returning true`);
432
+ return true;
433
+ }
434
+
435
+ try {
436
+ // Basic data check
437
+ if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
438
+ console.error(`[isTimeAvailableSync] Missing floors data for ${date} ${time}`);
439
+ return false;
440
+ }
441
+
442
+ if (guests <= 0) {
443
+ console.error(`[isTimeAvailableSync] Invalid guest count: ${guests}`);
444
+ return false; // Cannot reserve for 0 or fewer guests
445
+ }
446
+
447
+ const generalSettings = restaurantData["general-settings"] || {};
448
+ let duurReservatie = 120; // Default: 2 hours in minutes
449
+ let intervalReservatie = 15; // Default: 15 minute intervals
450
+ duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
451
+ intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
452
+
453
+ console.log(`[isTimeAvailableSync] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
454
+
455
+ if (intervalReservatie <= 0) {
456
+ console.error(`[isTimeAvailableSync] Invalid interval settings for ${date} ${time}`);
457
+ return false;
458
+ }
459
+
460
+ const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
461
+ if (!requiredSlots || requiredSlots.length === 0) {
462
+ console.error(`[isTimeAvailableSync] Could not compute required slots for ${date} ${time}`);
463
+ return false; // Cannot proceed if slots are invalid
464
+ }
465
+
466
+ // --- Build Occupancy Map using ACTUAL table assignments when available ---
467
+ const tableOccupiedSlots = {}; // { tableNumber: Set<slotMinutes> }
468
+ const reservationsForDate = reservations
469
+ .filter(r => r.date === date && r.time && r.guests > 0)
470
+ .sort((a, b) => {
471
+ // Simple time string comparison
472
+ const timeA = a.time.split(':').map(Number);
473
+ const timeB = b.time.split(':').map(Number);
474
+ return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
475
+ });
476
+
477
+ console.log(`[isTimeAvailableSync] Processing ${reservationsForDate.length} existing reservations on ${date}`);
478
+
479
+ for (const r of reservationsForDate) {
480
+ // NEW: Try to get actual table assignment first
481
+ const actualTables = getActualTableAssignment(r);
482
+ let assignedTables = [];
483
+
484
+ if (actualTables.length > 0) {
485
+ // Use actual table assignment from reservation data
486
+ assignedTables = actualTables;
487
+ console.log(`[isTimeAvailableSync] Using actual table assignment for reservation: ${assignedTables.join(', ')}`);
488
+ } else {
489
+ // Fall back to simulation for backwards compatibility
490
+ console.log(`[isTimeAvailableSync] No actual table data, simulating assignment for reservation`);
491
+ assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots);
492
+ }
493
+
494
+ // Update the occupancy map based on the actual or simulated assignment
495
+ if (assignedTables.length > 0) {
496
+ const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
497
+ if (!rSlots || rSlots.length === 0) continue; // Skip if slots invalid
498
+
499
+ assignedTables.forEach(tableNumber => {
500
+ if (!tableOccupiedSlots[tableNumber]) {
501
+ tableOccupiedSlots[tableNumber] = new Set();
502
+ }
503
+ rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
504
+ });
505
+
506
+ console.log(`[isTimeAvailableSync] Marked tables ${assignedTables.join(', ')} as occupied for time ${r.time}`);
507
+ }
508
+ }
509
+
510
+ console.log(`[isTimeAvailableSync] Occupancy map built, checking availability for new reservation`);
511
+
512
+ const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
513
+ console.log(`[isTimeAvailableSync] Checking ${allTables.length} tables for new reservation`);
514
+
515
+ // OPTIMIZATION: Pre-filter valid tables
516
+ const validTables = allTables.filter(t =>
517
+ isTemporaryTableValid(t, date, time) &&
518
+ isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
519
+ );
520
+
521
+ if (validTables.length === 0) {
522
+ console.log(`[isTimeAvailableSync] No valid tables available`);
523
+ return false;
524
+ }
525
+
526
+ // OPTIMIZATION: Sort for faster exact matches
527
+ validTables.sort((a, b) => {
528
+ const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
529
+ const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
530
+ if (aExact !== bExact) return aExact - bExact;
531
+ return a.maxCapacity - b.maxCapacity;
532
+ });
533
+
534
+ // 1. Try single table assignment
535
+ for (const t of validTables) {
536
+ if (t.minCapacity <= guests && guests <= t.maxCapacity) {
537
+ console.log(`[isTimeAvailableSync] Found single table ${t.tableNumber} for ${guests} guests at ${time}`);
538
+ return true; // Available
539
+ }
540
+ }
541
+
542
+ console.log(`[isTimeAvailableSync] No single table found, trying combinations...`);
543
+
544
+ // 2. Try multi-table assignment
545
+ const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
546
+ findMultiTableCombination(
547
+ validTables, // Use pre-filtered tables
548
+ guests,
549
+ 0, // Start index
550
+ [], // Empty current set
551
+ best // Best solution
552
+ );
553
+
554
+ const result = best.tables.length > 0;
555
+ if (result) {
556
+ console.log(`[isTimeAvailableSync] Multi-table assignment possible: ${best.tables.map(t => t.tableNumber).join(', ')}`);
557
+ } else {
558
+ console.log(`[isTimeAvailableSync] No table combination available`);
559
+ }
560
+
561
+ return result; // Available if a combination was found
562
+
563
+ } catch (error) {
564
+ console.error(`[isTimeAvailableSync] Error during check for ${date} ${time}:`, error);
565
+ return false; // Assume unavailable on error
566
+ }
567
+ }
568
+
569
+ function filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null) {
570
+ console.log(`[filterTimeblocksByTableAvailability] Filtering timeblocks for ${date} with ${guests} guests`);
571
+
572
+ if (guests < 0) {
573
+ console.log(`[filterTimeblocksByTableAvailability] Detected negative guest count (${guests}), adjusting to default of 2`);
574
+ guests = 0; // Use a reasonable default
575
+ }
576
+
577
+ const tableSettings = restaurantData?.['table-settings'] || {};
578
+ const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
579
+ tableSettings.assignmentMode === "automatic";
580
+
581
+ if (!isTableAssignmentEnabled) {
582
+ console.log(`[filterTimeblocksByTableAvailability] No table assignment enabled, returning all timeblocks`);
583
+ return timeblocks;
584
+ }
585
+
586
+ console.log(`[filterTimeblocksByTableAvailability] Starting with ${Object.keys(timeblocks).length} timeblocks`);
587
+
588
+ const filteredTimeblocks = {};
589
+ let availableCount = 0;
590
+ let unavailableCount = 0;
591
+
592
+ for (const time in timeblocks) {
593
+ if (isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats)) {
594
+ filteredTimeblocks[time] = timeblocks[time];
595
+ availableCount++;
596
+ } else {
597
+ unavailableCount++;
598
+ }
599
+ }
600
+
601
+ console.log(`[filterTimeblocksByTableAvailability] Result: ${availableCount} available, ${unavailableCount} unavailable times`);
602
+ if (availableCount > 0) {
603
+ console.log(`[filterTimeblocksByTableAvailability] Available times: ${Object.keys(filteredTimeblocks).join(', ')}`);
604
+ }
605
+
606
+ return filteredTimeblocks;
607
+ }
608
+
609
+ function getAvailableTimeblocksWithTableCheck(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null) {
610
+ return filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats);
611
+ }
612
+
613
+ function getAvailableTablesForTime(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
614
+ try {
615
+ if (guests < 0) {
616
+ console.log(`[getAvailableTablesForTime] Detected negative guest count (${guests}), adjusting to default of 2`);
617
+ guests = 0;
618
+ }
619
+
620
+ const tableSettings = restaurantData?.['table-settings'] || {};
621
+ const isAutomaticAssignment = tableSettings.isInstalled === true &&
622
+ tableSettings.assignmentMode === "automatic";
623
+
624
+ if (!isAutomaticAssignment) {
625
+ return [];
626
+ }
627
+
628
+ if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
629
+ console.error(`[getAvailableTablesForTime] Missing floors data for ${date} ${time}`);
630
+ return [];
631
+ }
632
+ if (guests <= 0) return [];
633
+ const generalSettings = restaurantData["general-settings"] || {};
634
+ const duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
635
+ const intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
636
+
637
+ if (intervalReservatie <= 0) {
638
+ console.error(`[getAvailableTablesForTime] Invalid interval settings for ${date} ${time}`);
639
+ return [];
640
+ }
641
+
642
+ console.log(`\n[getAvailableTablesForTime] Finding available tables for ${guests} guests on ${date} at ${time}`);
643
+ const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
644
+ if (!requiredSlots || requiredSlots.length === 0) {
645
+ console.error(`[getAvailableTablesForTime] Could not compute required slots for ${date} ${time}`);
646
+ return [];
647
+ }
648
+ const tableOccupiedSlots = {};
649
+ const reservationsForDate = reservations
650
+ .filter(r => r.date === date && r.time && r.guests > 0)
651
+ .sort((a, b) => {
652
+ const timeA = a.time.split(':').map(Number);
653
+ const timeB = b.time.split(':').map(Number);
654
+ return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
655
+ });
656
+
657
+ console.log(`[getAvailableTablesForTime] Building occupancy map (Processing ${reservationsForDate.length} existing reservations)`);
658
+ for (const r of reservationsForDate) {
659
+ const actualTables = getActualTableAssignment(r);
660
+ let assignedTables = [];
661
+
662
+ if (actualTables.length > 0) {
663
+ assignedTables = actualTables;
664
+ } else {
665
+ assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, null);
666
+ }
667
+
668
+ if (assignedTables.length > 0) {
669
+ const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
670
+ if (!rSlots || rSlots.length === 0) continue;
671
+ assignedTables.forEach(tableNumber => {
672
+ if (!tableOccupiedSlots[tableNumber]) {
673
+ tableOccupiedSlots[tableNumber] = new Set();
674
+ }
675
+ rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
676
+ });
677
+ }
678
+ }
679
+ console.log("[getAvailableTablesForTime] Occupancy map built");
680
+ const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables with zitplaats filter applied
681
+ const availableIndividualTables = [];
682
+
683
+ console.log(`[getAvailableTablesForTime] Checking ${allTables.length} tables for individual availability`);
684
+ for (const t of allTables) {
685
+ if (!isTemporaryTableValid(t, date, time)) continue;
686
+ if (t.minCapacity <= guests && guests <= t.maxCapacity) {
687
+ if (isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)) {
688
+ console.log(`[getAvailableTablesForTime] Table ${t.tableNumber} is available`);
689
+ availableIndividualTables.push(t); // Storing the whole object might be useful
690
+ }
691
+ }
692
+ }
693
+
694
+ console.log(`[getAvailableTablesForTime] Found ${availableIndividualTables.length} individually available tables: ${availableIndividualTables.map(t => t.tableNumber).join(', ')}`);
695
+ availableIndividualTables.sort((a, b) => a.tableNumber - b.tableNumber);
696
+
697
+ return availableIndividualTables; // Return array of table objects
698
+
699
+ } catch (error) {
700
+ console.error(`[getAvailableTablesForTime] Error for ${date} ${time}:`, error);
701
+ return []; // Return empty on error
702
+ }
703
+ }
704
+
705
+ module.exports = {
706
+ isTimeAvailableSync,
707
+ filterTimeblocksByTableAvailability,
708
+ getAvailableTimeblocksWithTableCheck,
709
+ getAvailableTablesForTime,
700
710
  };