@happychef/algorithm 1.2.10 → 1.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci-cd.yml +133 -2
- package/BRANCH_PROTECTION_SETUP.md +167 -0
- package/CHANGELOG.md +8 -8
- package/RESERVERINGEN_GIDS.md +986 -986
- package/assignTables.js +424 -398
- package/changes/2025/December/PR2___change.md +14 -14
- package/changes/2025/December/PR3_add__change.md +20 -20
- package/changes/2025/December/PR4___.md +16 -0
- package/changes/2025/December/PR5___.md +16 -0
- package/changes/2025/December/PR6__del_.md +18 -0
- package/changes/2025/December/PR7_add__change.md +22 -0
- package/changes/2026/January/PR8_add__change.md +39 -0
- package/changes/2026/January/PR9_add__change.md +20 -0
- package/filters/maxArrivalsFilter.js +114 -114
- package/filters/maxGroupsFilter.js +221 -221
- package/filters/timeFilter.js +89 -89
- package/getAvailableTimeblocks.js +158 -158
- package/grouping.js +162 -162
- package/index.js +42 -42
- package/isDateAvailable.js +80 -80
- package/isDateAvailableWithTableCheck.js +171 -171
- package/isTimeAvailable.js +25 -25
- package/package.json +27 -27
- package/processing/dailyGuestCounts.js +73 -73
- package/processing/mealTypeCount.js +133 -133
- package/processing/timeblocksAvailable.js +167 -167
- package/reservation_data/counter.js +64 -64
- package/restaurant_data/exceptions.js +149 -149
- package/restaurant_data/openinghours.js +123 -123
- package/simulateTableAssignment.js +709 -699
- package/tableHelpers.js +178 -178
- package/tables/time/parseTime.js +19 -19
- package/tables/time/shifts.js +7 -7
- package/tables/utils/calculateDistance.js +13 -13
- package/tables/utils/isTableFreeForAllSlots.js +14 -14
- package/tables/utils/isTemporaryTableValid.js +39 -39
- package/test/test_counter.js +194 -194
- package/test/test_dailyCount.js +81 -81
- package/test/test_datesAvailable.js +106 -106
- package/test/test_exceptions.js +172 -172
- package/test/test_isDateAvailable.js +330 -330
- package/test/test_mealTypeCount.js +54 -54
- package/test/test_timesAvailable.js +88 -88
- package/test-meal-stop-fix.js +147 -147
- package/test-meal-stop-simple.js +93 -93
- 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,
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// --- Try
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
console.log(`[
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
};
|