@happychef/algorithm 1.0.0
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/assignTables.js +398 -0
- package/filters/maxArrivalsFilter.js +115 -0
- package/filters/maxGroupsFilter.js +222 -0
- package/filters/timeFilter.js +90 -0
- package/getAvailableTimeblocks.js +109 -0
- package/grouping.js +162 -0
- package/index.js +47 -0
- package/isDateAvailable.js +77 -0
- package/isDateAvailableWithTableCheck.js +169 -0
- package/isTimeAvailable.js +22 -0
- package/package.json +18 -0
- package/processing/dailyGuestCounts.js +73 -0
- package/processing/mealTypeCount.js +133 -0
- package/processing/timeblocksAvailable.js +120 -0
- package/reservation_data/counter.js +65 -0
- package/restaurant_data/exceptions.js +149 -0
- package/restaurant_data/openinghours.js +123 -0
- package/simulateTableAssignment.js +594 -0
- package/tableHelpers.js +178 -0
- package/test/test_counter.js +194 -0
- package/test/test_dailyCount.js +81 -0
- package/test/test_datesAvailable.js +106 -0
- package/test/test_exceptions.js +173 -0
- package/test/test_isDateAvailable.js +330 -0
- package/test/test_mealTypeCount.js +54 -0
- package/test/test_timesAvailable.js +88 -0
- package/test.js +336 -0
|
@@ -0,0 +1,594 @@
|
|
|
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
|
+
* Backtracking function to find a combination of tables (multi-table assignment).
|
|
139
|
+
*/
|
|
140
|
+
function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet, best, requiredSlots, tableOccupiedSlots, reservationDateStr, reservationTimeStr) {
|
|
141
|
+
// Base case: All guests are seated
|
|
142
|
+
if (guestsNeeded <= 0) {
|
|
143
|
+
// Calculate total distance for the current combination
|
|
144
|
+
let distanceSum = 0;
|
|
145
|
+
for (let i = 0; i < currentSet.length; i++) {
|
|
146
|
+
for (let j = i + 1; j < currentSet.length; j++) {
|
|
147
|
+
distanceSum += calculateDistance(currentSet[i], currentSet[j]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Update best solution if this one is better (fewer tables, then lower distance)
|
|
151
|
+
if (currentSet.length < best.tableCount || (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
|
|
152
|
+
best.minDistance = distanceSum;
|
|
153
|
+
best.tables = [...currentSet]; // Store a copy
|
|
154
|
+
best.tableCount = currentSet.length;
|
|
155
|
+
console.log(`Found new best table combination: ${best.tables.map(t => t.tableNumber).join(', ')} for ${guestsNeeded} guests`);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (startIndex >= tables.length) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) {
|
|
165
|
+
return; // Pruning based on table count
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let maxPossibleCapacity = 0;
|
|
169
|
+
for (let i = startIndex; i < tables.length; i++) {
|
|
170
|
+
const tbl = tables[i];
|
|
171
|
+
// Only consider valid & free tables for potential capacity
|
|
172
|
+
if (isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr) &&
|
|
173
|
+
isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
174
|
+
{
|
|
175
|
+
maxPossibleCapacity += tbl.maxCapacity;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (maxPossibleCapacity < guestsNeeded) {
|
|
180
|
+
return; // Impossible to seat remaining guests
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (let i = startIndex; i < tables.length; i++) {
|
|
184
|
+
const tbl = tables[i];
|
|
185
|
+
|
|
186
|
+
// --- Core Checks ---
|
|
187
|
+
// 1. Check if temporary table is valid for this date/time
|
|
188
|
+
if (!isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr)) {
|
|
189
|
+
continue; // Skip invalid temporary table
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 2. Check if table is free for all required slots
|
|
193
|
+
if (!isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots)) {
|
|
194
|
+
continue; // Skip occupied table
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. Check if table contributes meaningfully
|
|
198
|
+
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
199
|
+
if (canSeat < tbl.minCapacity && canSeat < guestsNeeded) {
|
|
200
|
+
continue; // Don't use a table for fewer than its min capacity unless it fulfills the remainder
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (canSeat <= 0) continue; // Table doesn't help
|
|
204
|
+
|
|
205
|
+
// --- Recurse ---
|
|
206
|
+
currentSet.push(tbl); // Add table to current combination
|
|
207
|
+
console.log(`Trying table ${tbl.tableNumber} (${tbl.minCapacity}-${tbl.maxCapacity}) for ${canSeat}/${guestsNeeded} guests`);
|
|
208
|
+
|
|
209
|
+
findMultiTableCombination(
|
|
210
|
+
tables,
|
|
211
|
+
guestsNeeded - canSeat, // Reduce guests needed
|
|
212
|
+
i + 1, // Explore next tables
|
|
213
|
+
currentSet,
|
|
214
|
+
best,
|
|
215
|
+
requiredSlots,
|
|
216
|
+
tableOccupiedSlots,
|
|
217
|
+
reservationDateStr,
|
|
218
|
+
reservationTimeStr
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
currentSet.pop(); // Backtrack: remove table to explore other combinations
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null) {
|
|
226
|
+
console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
|
|
227
|
+
|
|
228
|
+
// FIXED: More robust parsing of settings with detailed logging
|
|
229
|
+
const generalSettings = restaurantData["general-settings"] || {};
|
|
230
|
+
console.log("[assignTablesForGivenTime] General settings:", generalSettings);
|
|
231
|
+
|
|
232
|
+
// Check what format the data is in
|
|
233
|
+
console.log("[assignTablesForGivenTime] duurReservatie format:", typeof generalSettings.duurReservatie);
|
|
234
|
+
console.log("[assignTablesForGivenTime] intervalReservatie format:", typeof generalSettings.intervalReservatie);
|
|
235
|
+
|
|
236
|
+
// Check for MongoDB format
|
|
237
|
+
if (typeof generalSettings.duurReservatie === 'object' && generalSettings.duurReservatie?.$numberInt) {
|
|
238
|
+
console.log("[assignTablesForGivenTime] duurReservatie is in MongoDB format:", generalSettings.duurReservatie?.$numberInt);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof generalSettings.intervalReservatie === 'object' && generalSettings.intervalReservatie?.$numberInt) {
|
|
242
|
+
console.log("[assignTablesForGivenTime] intervalReservatie is in MongoDB format:", generalSettings.intervalReservatie?.$numberInt);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// DEFAULT VALUES - this approach mirrors the TypeScript implementation
|
|
246
|
+
let duurReservatie = 120; // Default: 2 hours in minutes
|
|
247
|
+
let intervalReservatie = 15; // Default: 15 minute intervals
|
|
248
|
+
|
|
249
|
+
// Use safeParseInt for robust parsing
|
|
250
|
+
duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
251
|
+
intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
252
|
+
|
|
253
|
+
console.log(`[assignTablesForGivenTime] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
|
|
254
|
+
|
|
255
|
+
if (intervalReservatie <= 0) {
|
|
256
|
+
console.error("Invalid interval settings.");
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
261
|
+
if (!requiredSlots || requiredSlots.length === 0) {
|
|
262
|
+
console.error(`Could not compute required slots for ${time}`);
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fetch all tables using the imported helper
|
|
267
|
+
const allTables = getAllTables(restaurantData, selectedZitplaats);
|
|
268
|
+
console.log(`[assignTablesForGivenTime] Checking ${allTables.length} tables`);
|
|
269
|
+
|
|
270
|
+
// --- Try single-table assignment first ---
|
|
271
|
+
for (const t of allTables) {
|
|
272
|
+
// Use imported validation function
|
|
273
|
+
if (!isTemporaryTableValid(t, date, time)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (t.minCapacity <= guests &&
|
|
278
|
+
guests <= t.maxCapacity &&
|
|
279
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
280
|
+
{
|
|
281
|
+
console.log(`[assignTablesForGivenTime] Assigned single table ${t.tableNumber} for ${guests} guests`);
|
|
282
|
+
return [t.tableNumber];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(`[assignTablesForGivenTime] No single table found, trying combinations...`);
|
|
287
|
+
|
|
288
|
+
// --- Try multi-table assignment ---
|
|
289
|
+
const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
|
|
290
|
+
findMultiTableCombination(
|
|
291
|
+
allTables,
|
|
292
|
+
guests,
|
|
293
|
+
0, // Start index
|
|
294
|
+
[], // Initial empty set
|
|
295
|
+
best, // Best solution object
|
|
296
|
+
requiredSlots,
|
|
297
|
+
tableOccupiedSlots,
|
|
298
|
+
date, // Pass context
|
|
299
|
+
time // Pass context
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (best.tables.length > 0) {
|
|
303
|
+
console.log(`[assignTablesForGivenTime] Found multi-table solution: ${best.tables.map(t => t.tableNumber).join(', ')}`);
|
|
304
|
+
} else {
|
|
305
|
+
console.log(`[assignTablesForGivenTime] No table combination found for ${guests} guests`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return best.tables.map(t => t.tableNumber);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
|
|
312
|
+
console.log(`\n[isTimeAvailableSync] Checking ${date} ${time} for ${guests} guests`);
|
|
313
|
+
|
|
314
|
+
if (guests < 0) {
|
|
315
|
+
console.log(`[isTimeAvailableSync] Detected negative guest count (${guests}), adjusting to default of 2`);
|
|
316
|
+
guests = 2; // Use a reasonable default
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const tableSettings = restaurantData?.['table-settings'] || {};
|
|
320
|
+
const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
|
|
321
|
+
tableSettings.assignmentMode === "automatic";
|
|
322
|
+
|
|
323
|
+
console.log(`[isTimeAvailableSync] Table assignment enabled? ${isTableAssignmentEnabled}`);
|
|
324
|
+
console.log(`- Table settings installed: ${tableSettings.isInstalled === true}`);
|
|
325
|
+
console.log(`- Assignment mode: ${tableSettings.assignmentMode}`);
|
|
326
|
+
|
|
327
|
+
if (!isTableAssignmentEnabled) {
|
|
328
|
+
console.log(`[isTimeAvailableSync] No table assignment needed, returning true`);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
// Basic data check
|
|
334
|
+
if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
|
|
335
|
+
console.error(`[isTimeAvailableSync] Missing floors data for ${date} ${time}`);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (guests <= 0) {
|
|
340
|
+
console.error(`[isTimeAvailableSync] Invalid guest count: ${guests}`);
|
|
341
|
+
return false; // Cannot reserve for 0 or fewer guests
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const generalSettings = restaurantData["general-settings"] || {};
|
|
345
|
+
let duurReservatie = 120; // Default: 2 hours in minutes
|
|
346
|
+
let intervalReservatie = 15; // Default: 15 minute intervals
|
|
347
|
+
duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
348
|
+
intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
349
|
+
|
|
350
|
+
console.log(`[isTimeAvailableSync] Using duration: ${duurReservatie}min, interval: ${intervalReservatie}min`);
|
|
351
|
+
|
|
352
|
+
if (intervalReservatie <= 0) {
|
|
353
|
+
console.error(`[isTimeAvailableSync] Invalid interval settings for ${date} ${time}`);
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
358
|
+
if (!requiredSlots || requiredSlots.length === 0) {
|
|
359
|
+
console.error(`[isTimeAvailableSync] Could not compute required slots for ${date} ${time}`);
|
|
360
|
+
return false; // Cannot proceed if slots are invalid
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- Build Occupancy Map using ACTUAL table assignments when available ---
|
|
364
|
+
const tableOccupiedSlots = {}; // { tableNumber: Set<slotMinutes> }
|
|
365
|
+
const reservationsForDate = reservations
|
|
366
|
+
.filter(r => r.date === date && r.time && r.guests > 0)
|
|
367
|
+
.sort((a, b) => {
|
|
368
|
+
// Simple time string comparison
|
|
369
|
+
const timeA = a.time.split(':').map(Number);
|
|
370
|
+
const timeB = b.time.split(':').map(Number);
|
|
371
|
+
return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
console.log(`[isTimeAvailableSync] Processing ${reservationsForDate.length} existing reservations on ${date}`);
|
|
375
|
+
|
|
376
|
+
for (const r of reservationsForDate) {
|
|
377
|
+
// NEW: Try to get actual table assignment first
|
|
378
|
+
const actualTables = getActualTableAssignment(r);
|
|
379
|
+
let assignedTables = [];
|
|
380
|
+
|
|
381
|
+
if (actualTables.length > 0) {
|
|
382
|
+
// Use actual table assignment from reservation data
|
|
383
|
+
assignedTables = actualTables;
|
|
384
|
+
console.log(`[isTimeAvailableSync] Using actual table assignment for reservation: ${assignedTables.join(', ')}`);
|
|
385
|
+
} else {
|
|
386
|
+
// Fall back to simulation for backwards compatibility
|
|
387
|
+
console.log(`[isTimeAvailableSync] No actual table data, simulating assignment for reservation`);
|
|
388
|
+
assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Update the occupancy map based on the actual or simulated assignment
|
|
392
|
+
if (assignedTables.length > 0) {
|
|
393
|
+
const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
|
|
394
|
+
if (!rSlots || rSlots.length === 0) continue; // Skip if slots invalid
|
|
395
|
+
|
|
396
|
+
assignedTables.forEach(tableNumber => {
|
|
397
|
+
if (!tableOccupiedSlots[tableNumber]) {
|
|
398
|
+
tableOccupiedSlots[tableNumber] = new Set();
|
|
399
|
+
}
|
|
400
|
+
rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
console.log(`[isTimeAvailableSync] Marked tables ${assignedTables.join(', ')} as occupied for time ${r.time}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log(`[isTimeAvailableSync] Occupancy map built, checking availability for new reservation`);
|
|
408
|
+
|
|
409
|
+
const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
|
|
410
|
+
console.log(`[isTimeAvailableSync] Checking ${allTables.length} tables for new reservation`);
|
|
411
|
+
|
|
412
|
+
// 1. Try single table assignment
|
|
413
|
+
for (const t of allTables) {
|
|
414
|
+
if (!isTemporaryTableValid(t, date, time)) continue; // Check temporary validity
|
|
415
|
+
|
|
416
|
+
if (t.minCapacity <= guests &&
|
|
417
|
+
guests <= t.maxCapacity &&
|
|
418
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
419
|
+
{
|
|
420
|
+
console.log(`[isTimeAvailableSync] Found single table ${t.tableNumber} for ${guests} guests at ${time}`);
|
|
421
|
+
return true; // Available
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
console.log(`[isTimeAvailableSync] No single table found, trying combinations...`);
|
|
426
|
+
|
|
427
|
+
// 2. Try multi-table assignment
|
|
428
|
+
const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
|
|
429
|
+
findMultiTableCombination(
|
|
430
|
+
allTables,
|
|
431
|
+
guests,
|
|
432
|
+
0, [], best,
|
|
433
|
+
requiredSlots,
|
|
434
|
+
tableOccupiedSlots, // Use the final occupancy map
|
|
435
|
+
date, time
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const result = best.tables.length > 0;
|
|
439
|
+
if (result) {
|
|
440
|
+
console.log(`[isTimeAvailableSync] Multi-table assignment possible: ${best.tables.map(t => t.tableNumber).join(', ')}`);
|
|
441
|
+
} else {
|
|
442
|
+
console.log(`[isTimeAvailableSync] No table combination available`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return result; // Available if a combination was found
|
|
446
|
+
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error(`[isTimeAvailableSync] Error during check for ${date} ${time}:`, error);
|
|
449
|
+
return false; // Assume unavailable on error
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null) {
|
|
454
|
+
console.log(`[filterTimeblocksByTableAvailability] Filtering timeblocks for ${date} with ${guests} guests`);
|
|
455
|
+
|
|
456
|
+
if (guests < 0) {
|
|
457
|
+
console.log(`[filterTimeblocksByTableAvailability] Detected negative guest count (${guests}), adjusting to default of 2`);
|
|
458
|
+
guests = 0; // Use a reasonable default
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const tableSettings = restaurantData?.['table-settings'] || {};
|
|
462
|
+
const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
|
|
463
|
+
tableSettings.assignmentMode === "automatic";
|
|
464
|
+
|
|
465
|
+
if (!isTableAssignmentEnabled) {
|
|
466
|
+
console.log(`[filterTimeblocksByTableAvailability] No table assignment enabled, returning all timeblocks`);
|
|
467
|
+
return timeblocks;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(`[filterTimeblocksByTableAvailability] Starting with ${Object.keys(timeblocks).length} timeblocks`);
|
|
471
|
+
|
|
472
|
+
const filteredTimeblocks = {};
|
|
473
|
+
let availableCount = 0;
|
|
474
|
+
let unavailableCount = 0;
|
|
475
|
+
|
|
476
|
+
for (const time in timeblocks) {
|
|
477
|
+
if (isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats)) {
|
|
478
|
+
filteredTimeblocks[time] = timeblocks[time];
|
|
479
|
+
availableCount++;
|
|
480
|
+
} else {
|
|
481
|
+
unavailableCount++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
console.log(`[filterTimeblocksByTableAvailability] Result: ${availableCount} available, ${unavailableCount} unavailable times`);
|
|
486
|
+
if (availableCount > 0) {
|
|
487
|
+
console.log(`[filterTimeblocksByTableAvailability] Available times: ${Object.keys(filteredTimeblocks).join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return filteredTimeblocks;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function getAvailableTimeblocksWithTableCheck(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats = null) {
|
|
494
|
+
return filterTimeblocksByTableAvailability(restaurantData, date, timeblocks, guests, reservations, selectedZitplaats);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getAvailableTablesForTime(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
|
|
498
|
+
try {
|
|
499
|
+
if (guests < 0) {
|
|
500
|
+
console.log(`[getAvailableTablesForTime] Detected negative guest count (${guests}), adjusting to default of 2`);
|
|
501
|
+
guests = 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const tableSettings = restaurantData?.['table-settings'] || {};
|
|
505
|
+
const isAutomaticAssignment = tableSettings.isInstalled === true &&
|
|
506
|
+
tableSettings.assignmentMode === "automatic";
|
|
507
|
+
|
|
508
|
+
if (!isAutomaticAssignment) {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
|
|
513
|
+
console.error(`[getAvailableTablesForTime] Missing floors data for ${date} ${time}`);
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
if (guests <= 0) return [];
|
|
517
|
+
const generalSettings = restaurantData["general-settings"] || {};
|
|
518
|
+
const duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
519
|
+
const intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
520
|
+
|
|
521
|
+
if (intervalReservatie <= 0) {
|
|
522
|
+
console.error(`[getAvailableTablesForTime] Invalid interval settings for ${date} ${time}`);
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log(`\n[getAvailableTablesForTime] Finding available tables for ${guests} guests on ${date} at ${time}`);
|
|
527
|
+
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
528
|
+
if (!requiredSlots || requiredSlots.length === 0) {
|
|
529
|
+
console.error(`[getAvailableTablesForTime] Could not compute required slots for ${date} ${time}`);
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const tableOccupiedSlots = {};
|
|
533
|
+
const reservationsForDate = reservations
|
|
534
|
+
.filter(r => r.date === date && r.time && r.guests > 0)
|
|
535
|
+
.sort((a, b) => {
|
|
536
|
+
const timeA = a.time.split(':').map(Number);
|
|
537
|
+
const timeB = b.time.split(':').map(Number);
|
|
538
|
+
return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
console.log(`[getAvailableTablesForTime] Building occupancy map (Processing ${reservationsForDate.length} existing reservations)`);
|
|
542
|
+
for (const r of reservationsForDate) {
|
|
543
|
+
const actualTables = getActualTableAssignment(r);
|
|
544
|
+
let assignedTables = [];
|
|
545
|
+
|
|
546
|
+
if (actualTables.length > 0) {
|
|
547
|
+
assignedTables = actualTables;
|
|
548
|
+
} else {
|
|
549
|
+
assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, null);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (assignedTables.length > 0) {
|
|
553
|
+
const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
|
|
554
|
+
if (!rSlots || rSlots.length === 0) continue;
|
|
555
|
+
assignedTables.forEach(tableNumber => {
|
|
556
|
+
if (!tableOccupiedSlots[tableNumber]) {
|
|
557
|
+
tableOccupiedSlots[tableNumber] = new Set();
|
|
558
|
+
}
|
|
559
|
+
rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
console.log("[getAvailableTablesForTime] Occupancy map built");
|
|
564
|
+
const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables with zitplaats filter applied
|
|
565
|
+
const availableIndividualTables = [];
|
|
566
|
+
|
|
567
|
+
console.log(`[getAvailableTablesForTime] Checking ${allTables.length} tables for individual availability`);
|
|
568
|
+
for (const t of allTables) {
|
|
569
|
+
if (!isTemporaryTableValid(t, date, time)) continue;
|
|
570
|
+
if (t.minCapacity <= guests && guests <= t.maxCapacity) {
|
|
571
|
+
if (isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)) {
|
|
572
|
+
console.log(`[getAvailableTablesForTime] Table ${t.tableNumber} is available`);
|
|
573
|
+
availableIndividualTables.push(t); // Storing the whole object might be useful
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
console.log(`[getAvailableTablesForTime] Found ${availableIndividualTables.length} individually available tables: ${availableIndividualTables.map(t => t.tableNumber).join(', ')}`);
|
|
579
|
+
availableIndividualTables.sort((a, b) => a.tableNumber - b.tableNumber);
|
|
580
|
+
|
|
581
|
+
return availableIndividualTables; // Return array of table objects
|
|
582
|
+
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error(`[getAvailableTablesForTime] Error for ${date} ${time}:`, error);
|
|
585
|
+
return []; // Return empty on error
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
isTimeAvailableSync,
|
|
591
|
+
filterTimeblocksByTableAvailability,
|
|
592
|
+
getAvailableTimeblocksWithTableCheck,
|
|
593
|
+
getAvailableTablesForTime,
|
|
594
|
+
};
|