@happychef/algorithm 1.0.0 → 1.0.2
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/getAvailableTimeblocks.js +3 -2
- package/index.js +23 -8
- package/isDateAvailable.js +3 -2
- package/isTimeAvailable.js +3 -2
- package/package.json +1 -1
- package/processing/timeblocksAvailable.js +12 -1
- package/tables/assignment/assignTablesForGivenTime.js +79 -0
- package/tables/assignment/filterTimeblocksByTableAvailability.js +48 -0
- package/tables/availability/findMultiTableCombination.js +100 -0
- package/tables/availability/getAvailableTablesForTime.js +106 -0
- package/tables/availability/isTimeAvailableSync.js +128 -0
- package/tables/data/extractTableNumbers.js +17 -0
- package/tables/data/getActualTableAssignment.js +16 -0
- package/tables/data/getAllTables.js +84 -0
- package/tables/main.js +33 -0
- package/tables/time/computeRequiredSlots.js +27 -0
- package/tables/time/getMealTypeByTime.js +26 -0
- package/tables/time/parseTime.js +19 -0
- package/tables/time/shifts.js +7 -0
- package/tables/utils/calculateDistance.js +13 -0
- package/tables/utils/isTableFreeForAllSlots.js +14 -0
- package/tables/utils/isTemporaryTableValid.js +39 -0
- package/tables/utils/safeParseInt.js +29 -0
|
@@ -28,9 +28,10 @@ function parseDateTimeInTimeZone(dateStr, timeStr, timeZone) {
|
|
|
28
28
|
* @param {string} dateStr - The date string in "YYYY-MM-DD" format.
|
|
29
29
|
* @param {Array} reservations - An array of reservation objects.
|
|
30
30
|
* @param {number} guests - The number of guests for the reservation.
|
|
31
|
+
* @param {Array} blockedSlots - Optional array of blocked time slots to exclude.
|
|
31
32
|
* @returns {Object} - Returns a pruned object of available time blocks or shifts, or an empty object if out of range.
|
|
32
33
|
*/
|
|
33
|
-
function getAvailableTimeblocks(data, dateStr, reservations, guests) {
|
|
34
|
+
function getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots = []) {
|
|
34
35
|
// Get 'uurOpVoorhand' from general settings
|
|
35
36
|
let uurOpVoorhand = 4;
|
|
36
37
|
if (
|
|
@@ -83,7 +84,7 @@ function getAvailableTimeblocks(data, dateStr, reservations, guests) {
|
|
|
83
84
|
currentTimeInTimeZone.toDateString() === targetDateInTimeZone.toDateString();
|
|
84
85
|
|
|
85
86
|
// Get available time blocks or shifts
|
|
86
|
-
const availableTimeblocks = timeblocksAvailable(data, dateStr, reservations, guests);
|
|
87
|
+
const availableTimeblocks = timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots);
|
|
87
88
|
|
|
88
89
|
// If the date is today and uurOpVoorhand is greater than zero, prune time blocks
|
|
89
90
|
if (isToday && uurOpVoorhand >= 0) {
|
package/index.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
const processing = {
|
|
4
4
|
...require("./processing/dailyGuestCounts"),
|
|
5
5
|
...require("./processing/mealTypeCount"),
|
|
6
|
-
...require("./processing/timeblocksAvailable")
|
|
6
|
+
...require("./processing/timeblocksAvailable"),
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
const reservation_data = {
|
|
10
|
-
...require("./reservation_data/counter")
|
|
10
|
+
...require("./reservation_data/counter"),
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const restaurant_data = {
|
|
14
14
|
...require("./restaurant_data/exceptions"),
|
|
15
|
-
...require("./restaurant_data/openinghours")
|
|
15
|
+
...require("./restaurant_data/openinghours"),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const test = {
|
|
@@ -24,24 +24,39 @@ const test = {
|
|
|
24
24
|
...require("./simulateTableAssignment"),
|
|
25
25
|
...require("./isDateAvailableWithTableCheck"),
|
|
26
26
|
...require("./tableHelpers"),
|
|
27
|
-
...require("./test")
|
|
27
|
+
...require("./test"),
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
const filters = {
|
|
31
31
|
...require("./filters/timeFilter"),
|
|
32
32
|
...require("./filters/maxArrivalsFilter"),
|
|
33
|
-
...require("./filters/maxGroupsFilter")
|
|
33
|
+
...require("./filters/maxGroupsFilter"),
|
|
34
34
|
};
|
|
35
|
+
|
|
36
|
+
// ✅ Table-related logic and utilities
|
|
35
37
|
const tables = {
|
|
36
38
|
...require("./tables/main.js"),
|
|
37
|
-
|
|
39
|
+
...require("./tables/availability/isTimeAvailableSync"),
|
|
40
|
+
...require("./tables/availability/getAvailableTablesForTime"),
|
|
41
|
+
...require("./tables/availability/findMultiTableCombination"),
|
|
42
|
+
...require("./tables/assignment/filterTimeblocksByTableAvailability"),
|
|
43
|
+
...require("./tables/assignment/assignTablesForGivenTime"),
|
|
44
|
+
...require("./tables/utils/safeParseInt"),
|
|
45
|
+
...require("./tables/utils/isTemporaryTableValid"),
|
|
46
|
+
...require("./tables/utils/isTableFreeForAllSlots"),
|
|
47
|
+
...require("./tables/utils/calculateDistance"),
|
|
48
|
+
...require("./tables/time/shifts").shifts,
|
|
49
|
+
...require("./tables/time/parseTime"),
|
|
50
|
+
...require("./tables/time/getMealTypeByTime"),
|
|
51
|
+
...require("./tables/time/computeRequiredSlots"),
|
|
38
52
|
};
|
|
39
|
-
|
|
53
|
+
|
|
54
|
+
// ✅ Export everything
|
|
40
55
|
module.exports = {
|
|
41
56
|
...processing,
|
|
42
57
|
...reservation_data,
|
|
43
58
|
...restaurant_data,
|
|
44
59
|
...test,
|
|
45
60
|
...filters,
|
|
46
|
-
...tables
|
|
61
|
+
...tables,
|
|
47
62
|
};
|
package/isDateAvailable.js
CHANGED
|
@@ -57,16 +57,17 @@ function isDateWithinAllowedRange(data, dateStr) {
|
|
|
57
57
|
* @param {string} dateStr - The date string in "YYYY-MM-DD" format.
|
|
58
58
|
* @param {Array} reservations - An array of reservation objects.
|
|
59
59
|
* @param {number} guests - The number of guests for the reservation.
|
|
60
|
+
* @param {Array} blockedSlots - Optional array of blocked time slots to exclude.
|
|
60
61
|
* @returns {boolean} - Returns true if the date has at least one available timeblock, false otherwise.
|
|
61
62
|
*/
|
|
62
|
-
function isDateAvailable(data, dateStr, reservations, guests) {
|
|
63
|
+
function isDateAvailable(data, dateStr, reservations, guests, blockedSlots = []) {
|
|
63
64
|
// Check if date is within allowed range
|
|
64
65
|
if (!isDateWithinAllowedRange(data, dateStr)) {
|
|
65
66
|
return false;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// Get available timeblocks using the existing logic
|
|
69
|
-
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests);
|
|
70
|
+
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots);
|
|
70
71
|
|
|
71
72
|
// Return true only if we have at least one available timeblock
|
|
72
73
|
return Object.keys(availableTimeblocks).length > 0;
|
package/isTimeAvailable.js
CHANGED
|
@@ -7,11 +7,12 @@ const { timeblocksAvailable } = require('./processing/timeblocksAvailable');
|
|
|
7
7
|
* @param {string} timeStr - The time string in "HH:MM" format.
|
|
8
8
|
* @param {Array} reservations - An array of existing reservation objects.
|
|
9
9
|
* @param {number} guests - The number of guests for the new reservation request.
|
|
10
|
+
* @param {Array} blockedSlots - Optional array of blocked time slots to exclude.
|
|
10
11
|
* @returns {boolean} - Returns true if the time is available, false otherwise.
|
|
11
12
|
*/
|
|
12
|
-
function isTimeAvailable(data, dateStr, timeStr, reservations, guests) {
|
|
13
|
+
function isTimeAvailable(data, dateStr, timeStr, reservations, guests, blockedSlots = []) {
|
|
13
14
|
// Get all available timeblocks for the specified date and guest count
|
|
14
|
-
const availableTimeblocks = timeblocksAvailable(data, dateStr, reservations, guests);
|
|
15
|
+
const availableTimeblocks = timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots);
|
|
15
16
|
|
|
16
17
|
// Check if the specific timeStr is one of the available keys
|
|
17
18
|
return Object.prototype.hasOwnProperty.call(availableTimeblocks, timeStr);
|
package/package.json
CHANGED
|
@@ -58,7 +58,7 @@ function fitsWithinMeal(data, dateStr, startTimeStr, duurReservatie) {
|
|
|
58
58
|
return startTime >= mealStartTime && startTime + duurReservatie <= mealEndTime;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function timeblocksAvailable(data, dateStr, reservations, guests) {
|
|
61
|
+
function timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots = []) {
|
|
62
62
|
const duurReservatie = getDuurReservatie(data);
|
|
63
63
|
const intervalReservatie = getInterval(data);
|
|
64
64
|
|
|
@@ -112,6 +112,17 @@ function timeblocksAvailable(data, dateStr, reservations, guests) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Filter out blocked time slots
|
|
116
|
+
if (blockedSlots && blockedSlots.length > 0) {
|
|
117
|
+
for (const blockedSlot of blockedSlots) {
|
|
118
|
+
// Check if the blocked slot matches the current date
|
|
119
|
+
if (blockedSlot.date === dateStr && blockedSlot.time) {
|
|
120
|
+
// Remove the blocked time from available timeblocks
|
|
121
|
+
delete availableTimeblocks[blockedSlot.time];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
115
126
|
return availableTimeblocks;
|
|
116
127
|
}
|
|
117
128
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const safeParseInt = require('../utils/safeParseInt');
|
|
2
|
+
const computeRequiredSlots = require('../time/computeRequiredSlots');
|
|
3
|
+
const getAllTables = require('../data/getAllTables');
|
|
4
|
+
const isTemporaryTableValid = require('../utils/isTemporaryTableValid');
|
|
5
|
+
const findMultiTableCombination = require('../availability/findMultiTableCombination');
|
|
6
|
+
const isTableFreeForAllSlots = require('../utils/isTableFreeForAllSlots');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attempts to assign tables for a single reservation at a given time.
|
|
10
|
+
* Returns an array of table numbers (single-table or multi-table).
|
|
11
|
+
*/
|
|
12
|
+
function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null) {
|
|
13
|
+
|
|
14
|
+
// FIXED: More robust parsing of settings with detailed logging
|
|
15
|
+
const generalSettings = restaurantData["general-settings"] || {};
|
|
16
|
+
|
|
17
|
+
// Check what format the data is in
|
|
18
|
+
// Check for MongoDB format
|
|
19
|
+
if (typeof generalSettings.duurReservatie === 'object' && generalSettings.duurReservatie?.$numberInt) {
|
|
20
|
+
}
|
|
21
|
+
if (typeof generalSettings.intervalReservatie === 'object' && generalSettings.intervalReservatie?.$numberInt) {
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// DEFAULT VALUES - this approach mirrors the TypeScript implementation
|
|
25
|
+
let duurReservatie = 120; // Default: 2 hours in minutes
|
|
26
|
+
let intervalReservatie = 15; // Default: 15 minute intervals
|
|
27
|
+
|
|
28
|
+
// Use safeParseInt for robust parsing
|
|
29
|
+
duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
30
|
+
intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
31
|
+
|
|
32
|
+
if (intervalReservatie <= 0) {
|
|
33
|
+
console.error("Invalid interval settings.");
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
38
|
+
if (!requiredSlots || requiredSlots.length === 0) {
|
|
39
|
+
console.error(`Could not compute required slots for ${time}`);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fetch all tables
|
|
44
|
+
const allTables = getAllTables(restaurantData, selectedZitplaats);
|
|
45
|
+
|
|
46
|
+
// Try single-table assignment first
|
|
47
|
+
for (const t of allTables) {
|
|
48
|
+
if (!isTemporaryTableValid(t, date, time)) continue;
|
|
49
|
+
|
|
50
|
+
if (t.minCapacity <= guests &&
|
|
51
|
+
guests <= t.maxCapacity &&
|
|
52
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
53
|
+
{
|
|
54
|
+
return [t.tableNumber];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try multi-table assignment
|
|
59
|
+
const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
|
|
60
|
+
findMultiTableCombination(
|
|
61
|
+
allTables,
|
|
62
|
+
guests,
|
|
63
|
+
0,
|
|
64
|
+
[],
|
|
65
|
+
best,
|
|
66
|
+
requiredSlots,
|
|
67
|
+
tableOccupiedSlots,
|
|
68
|
+
date,
|
|
69
|
+
time
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (best.tables.length > 0) {
|
|
73
|
+
} else {
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return best.tables.map(t => t.tableNumber);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = assignTablesForGivenTime;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Import directly to avoid circular dependency with tables/main.js
|
|
2
|
+
const isTimeAvailableSync = require('../availability/isTimeAvailableSync');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Filters timeblocks based on availability for a given guest count and date.
|
|
6
|
+
*/
|
|
7
|
+
function filterTimeblocksByTableAvailability(
|
|
8
|
+
restaurantData,
|
|
9
|
+
date,
|
|
10
|
+
timeblocks,
|
|
11
|
+
guests,
|
|
12
|
+
reservations,
|
|
13
|
+
selectedZitplaats = null
|
|
14
|
+
) {
|
|
15
|
+
if (guests < 0) {
|
|
16
|
+
guests = 0; // Use a reasonable default
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const tableSettings = restaurantData?.['table-settings'] || {};
|
|
20
|
+
const isTableAssignmentEnabled =
|
|
21
|
+
tableSettings.isInstalled === true &&
|
|
22
|
+
tableSettings.assignmentMode === 'automatic';
|
|
23
|
+
|
|
24
|
+
if (!isTableAssignmentEnabled) {
|
|
25
|
+
return timeblocks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const filteredTimeblocks = {};
|
|
29
|
+
let availableCount = 0;
|
|
30
|
+
let unavailableCount = 0;
|
|
31
|
+
|
|
32
|
+
for (const time in timeblocks) {
|
|
33
|
+
if (isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats)) {
|
|
34
|
+
filteredTimeblocks[time] = timeblocks[time];
|
|
35
|
+
availableCount++;
|
|
36
|
+
} else {
|
|
37
|
+
unavailableCount++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (availableCount > 0) {
|
|
42
|
+
// no-op, preserve original structure
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return filteredTimeblocks;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = filterTimeblocksByTableAvailability;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const isTemporaryTableValid = require('../utils/isTemporaryTableValid');
|
|
2
|
+
const isTableFreeForAllSlots = require('../utils/isTableFreeForAllSlots');
|
|
3
|
+
const calculateDistance = require('../utils/calculateDistance');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Backtracking function to find a combination of tables (multi-table assignment).
|
|
7
|
+
*/
|
|
8
|
+
function findMultiTableCombination(
|
|
9
|
+
tables,
|
|
10
|
+
guestsNeeded,
|
|
11
|
+
startIndex,
|
|
12
|
+
currentSet,
|
|
13
|
+
best,
|
|
14
|
+
requiredSlots,
|
|
15
|
+
tableOccupiedSlots,
|
|
16
|
+
reservationDateStr,
|
|
17
|
+
reservationTimeStr
|
|
18
|
+
) {
|
|
19
|
+
// Base case: All guests are seated
|
|
20
|
+
if (guestsNeeded <= 0) {
|
|
21
|
+
// Calculate total distance for the current combination
|
|
22
|
+
let distanceSum = 0;
|
|
23
|
+
for (let i = 0; i < currentSet.length; i++) {
|
|
24
|
+
for (let j = i + 1; j < currentSet.length; j++) {
|
|
25
|
+
distanceSum += calculateDistance(currentSet[i], currentSet[j]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Update best solution if this one is better (fewer tables, then lower distance)
|
|
29
|
+
if (currentSet.length < best.tableCount || (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
|
|
30
|
+
best.minDistance = distanceSum;
|
|
31
|
+
best.tables = [...currentSet]; // Store a copy
|
|
32
|
+
best.tableCount = currentSet.length;
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (startIndex >= tables.length) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) {
|
|
42
|
+
return; // Pruning based on table count
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let maxPossibleCapacity = 0;
|
|
46
|
+
for (let i = startIndex; i < tables.length; i++) {
|
|
47
|
+
const tbl = tables[i];
|
|
48
|
+
// Only consider valid & free tables for potential capacity
|
|
49
|
+
if (isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr) &&
|
|
50
|
+
isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
51
|
+
{
|
|
52
|
+
maxPossibleCapacity += tbl.maxCapacity;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (maxPossibleCapacity < guestsNeeded) {
|
|
57
|
+
return; // Impossible to seat remaining guests
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = startIndex; i < tables.length; i++) {
|
|
61
|
+
const tbl = tables[i];
|
|
62
|
+
|
|
63
|
+
// 1. Check if temporary table is valid for this date/time
|
|
64
|
+
if (!isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Check if table is free for all required slots
|
|
69
|
+
if (!isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Check if table contributes meaningfully
|
|
74
|
+
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
75
|
+
if (canSeat < tbl.minCapacity && canSeat < guestsNeeded) {
|
|
76
|
+
continue; // Don't use a table for fewer than its min capacity unless it fulfills the remainder
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (canSeat <= 0) continue; // Table doesn't help
|
|
80
|
+
|
|
81
|
+
// Recurse
|
|
82
|
+
currentSet.push(tbl);
|
|
83
|
+
|
|
84
|
+
findMultiTableCombination(
|
|
85
|
+
tables,
|
|
86
|
+
guestsNeeded - canSeat,
|
|
87
|
+
i + 1,
|
|
88
|
+
currentSet,
|
|
89
|
+
best,
|
|
90
|
+
requiredSlots,
|
|
91
|
+
tableOccupiedSlots,
|
|
92
|
+
reservationDateStr,
|
|
93
|
+
reservationTimeStr
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
currentSet.pop(); // Backtrack
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = findMultiTableCombination;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const safeParseInt = require('../utils/safeParseInt');
|
|
2
|
+
const computeRequiredSlots = require('../time/computeRequiredSlots');
|
|
3
|
+
const getAllTables = require('../data/getAllTables');
|
|
4
|
+
const isTemporaryTableValid = require('../utils/isTemporaryTableValid');
|
|
5
|
+
const getActualTableAssignment = require('../data/getActualTableAssignment');
|
|
6
|
+
const assignTablesForGivenTime = require('../assignment/assignTablesForGivenTime');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns array of available individual table objects for a specific time.
|
|
10
|
+
*/
|
|
11
|
+
function getAvailableTablesForTime(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
|
|
12
|
+
try {
|
|
13
|
+
if (guests < 0) {
|
|
14
|
+
guests = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tableSettings = restaurantData?.['table-settings'] || {};
|
|
18
|
+
const isAutomaticAssignment = tableSettings.isInstalled === true &&
|
|
19
|
+
tableSettings.assignmentMode === "automatic";
|
|
20
|
+
|
|
21
|
+
if (!isAutomaticAssignment) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
if (guests <= 0) return [];
|
|
29
|
+
|
|
30
|
+
const generalSettings = restaurantData["general-settings"] || {};
|
|
31
|
+
const duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
32
|
+
const intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
33
|
+
|
|
34
|
+
if (intervalReservatie <= 0) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
39
|
+
if (!requiredSlots || requiredSlots.length === 0) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tableOccupiedSlots = {};
|
|
44
|
+
const reservationsForDate = reservations
|
|
45
|
+
.filter(r => r.date === date && r.time && r.guests > 0)
|
|
46
|
+
.sort((a, b) => {
|
|
47
|
+
const timeA = a.time.split(':').map(Number);
|
|
48
|
+
const timeB = b.time.split(':').map(Number);
|
|
49
|
+
return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
for (const r of reservationsForDate) {
|
|
53
|
+
const actualTables = getActualTableAssignment(r);
|
|
54
|
+
let assignedTables = [];
|
|
55
|
+
|
|
56
|
+
if (actualTables.length > 0) {
|
|
57
|
+
assignedTables = actualTables;
|
|
58
|
+
} else {
|
|
59
|
+
assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (assignedTables.length > 0) {
|
|
63
|
+
const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
|
|
64
|
+
if (!rSlots || rSlots.length === 0) continue;
|
|
65
|
+
assignedTables.forEach(tableNumber => {
|
|
66
|
+
if (!tableOccupiedSlots[tableNumber]) {
|
|
67
|
+
tableOccupiedSlots[tableNumber] = new Set();
|
|
68
|
+
}
|
|
69
|
+
rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allTables = getAllTables(restaurantData, selectedZitplaats);
|
|
75
|
+
const availableIndividualTables = [];
|
|
76
|
+
|
|
77
|
+
for (const t of allTables) {
|
|
78
|
+
if (!isTemporaryTableValid(t, date, time)) continue;
|
|
79
|
+
if (t.minCapacity <= guests && guests <= t.maxCapacity) {
|
|
80
|
+
if (tableOccupiedSlots && requiredSlots && !Array.isArray(requiredSlots)) {
|
|
81
|
+
// (no-op guard, keeps logic consistent)
|
|
82
|
+
}
|
|
83
|
+
const isFree = (function(){
|
|
84
|
+
const occupied = tableOccupiedSlots[t.tableNumber] || new Set();
|
|
85
|
+
for (const s of requiredSlots) {
|
|
86
|
+
if (occupied.has(s)) return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
if (isFree) {
|
|
92
|
+
availableIndividualTables.push(t);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
availableIndividualTables.sort((a, b) => a.tableNumber - b.tableNumber);
|
|
98
|
+
|
|
99
|
+
return availableIndividualTables;
|
|
100
|
+
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return []; // Return empty on error
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = getAvailableTablesForTime;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const safeParseInt = require('../utils/safeParseInt');
|
|
2
|
+
const computeRequiredSlots = require('../time/computeRequiredSlots');
|
|
3
|
+
const isTemporaryTableValid = require('../utils/isTemporaryTableValid');
|
|
4
|
+
const getAllTables = require('../data/getAllTables');
|
|
5
|
+
const getActualTableAssignment = require('../data/getActualTableAssignment');
|
|
6
|
+
const assignTablesForGivenTime = require('../assignment/assignTablesForGivenTime');
|
|
7
|
+
const isTableFreeForAllSlots = require('../utils/isTableFreeForAllSlots');
|
|
8
|
+
const findMultiTableCombination = require('./findMultiTableCombination');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Synchronous availability check using table assignment simulation and/or actual assignments.
|
|
12
|
+
*/
|
|
13
|
+
function isTimeAvailableSync(restaurantData, date, time, guests, reservations, selectedZitplaats = null) {
|
|
14
|
+
|
|
15
|
+
if (guests < 0) {
|
|
16
|
+
guests = 2; // Use a reasonable default
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const tableSettings = restaurantData?.['table-settings'] || {};
|
|
20
|
+
const isTableAssignmentEnabled = tableSettings.isInstalled === true &&
|
|
21
|
+
tableSettings.assignmentMode === "automatic";
|
|
22
|
+
|
|
23
|
+
if (!isTableAssignmentEnabled) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Basic data check
|
|
29
|
+
if (!restaurantData?.floors || !Array.isArray(restaurantData.floors)) {
|
|
30
|
+
console.error(`[isTimeAvailableSync] Missing floors data for ${date} ${time}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (guests <= 0) {
|
|
35
|
+
console.error(`[isTimeAvailableSync] Invalid guest count: ${guests}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const generalSettings = restaurantData["general-settings"] || {};
|
|
40
|
+
let duurReservatie = 120; // Default: 2 hours in minutes
|
|
41
|
+
let intervalReservatie = 15; // Default: 15 minute intervals
|
|
42
|
+
duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
43
|
+
intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
44
|
+
|
|
45
|
+
if (intervalReservatie <= 0) {
|
|
46
|
+
console.error(`[isTimeAvailableSync] Invalid interval settings for ${date} ${time}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
51
|
+
if (!requiredSlots || requiredSlots.length === 0) {
|
|
52
|
+
console.error(`[isTimeAvailableSync] Could not compute required slots for ${date} ${time}`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build Occupancy Map using ACTUAL table assignments when available
|
|
57
|
+
const tableOccupiedSlots = {}; // { tableNumber: Set<slotMinutes> }
|
|
58
|
+
const reservationsForDate = reservations
|
|
59
|
+
.filter(r => r.date === date && r.time && r.guests > 0)
|
|
60
|
+
.sort((a, b) => {
|
|
61
|
+
const timeA = a.time.split(':').map(Number);
|
|
62
|
+
const timeB = b.time.split(':').map(Number);
|
|
63
|
+
return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
for (const r of reservationsForDate) {
|
|
67
|
+
// Try to get actual table assignment first
|
|
68
|
+
const actualTables = getActualTableAssignment(r);
|
|
69
|
+
let assignedTables = [];
|
|
70
|
+
|
|
71
|
+
if (actualTables.length > 0) {
|
|
72
|
+
assignedTables = actualTables;
|
|
73
|
+
} else {
|
|
74
|
+
// Fall back to simulation for backwards compatibility
|
|
75
|
+
assignedTables = assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update the occupancy map based on the actual or simulated assignment
|
|
79
|
+
if (assignedTables.length > 0) {
|
|
80
|
+
const rSlots = computeRequiredSlots(r.time, duurReservatie, intervalReservatie);
|
|
81
|
+
if (!rSlots || rSlots.length === 0) continue;
|
|
82
|
+
|
|
83
|
+
assignedTables.forEach(tableNumber => {
|
|
84
|
+
if (!tableOccupiedSlots[tableNumber]) {
|
|
85
|
+
tableOccupiedSlots[tableNumber] = new Set();
|
|
86
|
+
}
|
|
87
|
+
rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const allTables = getAllTables(restaurantData, selectedZitplaats);
|
|
93
|
+
|
|
94
|
+
// 1. Try single table assignment
|
|
95
|
+
for (const t of allTables) {
|
|
96
|
+
if (!isTemporaryTableValid(t, date, time)) continue;
|
|
97
|
+
|
|
98
|
+
if (t.minCapacity <= guests &&
|
|
99
|
+
guests <= t.maxCapacity &&
|
|
100
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
101
|
+
{
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Try multi-table assignment
|
|
107
|
+
const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
|
|
108
|
+
findMultiTableCombination(
|
|
109
|
+
allTables,
|
|
110
|
+
guests,
|
|
111
|
+
0, [], best,
|
|
112
|
+
requiredSlots,
|
|
113
|
+
tableOccupiedSlots,
|
|
114
|
+
date, time
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const result = best.tables.length > 0;
|
|
118
|
+
if (result) {
|
|
119
|
+
} else {
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return false; // Assume unavailable on error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = isTimeAvailableSync;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const safeParseInt = require('../utils/safeParseInt');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts actual table numbers from a reservation's tables array.
|
|
5
|
+
* Handles MongoDB $numberInt format.
|
|
6
|
+
*/
|
|
7
|
+
function extractTableNumbers(tablesArray) {
|
|
8
|
+
if (!Array.isArray(tablesArray)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return tablesArray
|
|
13
|
+
.map(table => safeParseInt(table, null))
|
|
14
|
+
.filter(tableNum => tableNum !== null);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = extractTableNumbers;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const extractTableNumbers = require('./extractTableNumbers');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets actual table assignments from reservation data if available.
|
|
5
|
+
* @param {Object} reservation - The reservation object
|
|
6
|
+
* @returns {Array} Array of table numbers that are actually assigned, or empty array if no data
|
|
7
|
+
*/
|
|
8
|
+
function getActualTableAssignment(reservation) {
|
|
9
|
+
if (reservation.tables && Array.isArray(reservation.tables)) {
|
|
10
|
+
const tableNumbers = extractTableNumbers(reservation.tables);
|
|
11
|
+
return tableNumbers;
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = getActualTableAssignment;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts and processes table data from the restaurantData object.
|
|
3
|
+
* Includes temporary table properties, zitplaats filtering, and sorts tables.
|
|
4
|
+
* @param {Object} restaurantData - The main restaurant data object.
|
|
5
|
+
* @param {string} selectedZitplaats - Optional zitplaats value to filter tables by.
|
|
6
|
+
* @returns {Array} An array of processed table objects.
|
|
7
|
+
*/
|
|
8
|
+
function getAllTables(restaurantData, selectedZitplaats = null) {
|
|
9
|
+
let allTables = [];
|
|
10
|
+
if (restaurantData?.floors && Array.isArray(restaurantData.floors)) {
|
|
11
|
+
restaurantData.floors.forEach(floor => {
|
|
12
|
+
if (floor?.tables && Array.isArray(floor.tables)) {
|
|
13
|
+
floor.tables.forEach(tbl => {
|
|
14
|
+
// Ensure table number, capacities, priority exist before parsing
|
|
15
|
+
const tableNumberRaw = tbl.tableNumber?.$numberInt ?? tbl.tableNumber;
|
|
16
|
+
const minCapacityRaw = tbl.minCapacity?.$numberInt ?? tbl.minCapacity;
|
|
17
|
+
const maxCapacityRaw = tbl.maxCapacity?.$numberInt ?? tbl.maxCapacity;
|
|
18
|
+
const priorityRaw = tbl.priority?.$numberInt ?? tbl.priority;
|
|
19
|
+
const xRaw = tbl.x?.$numberInt ?? tbl.x;
|
|
20
|
+
const yRaw = tbl.y?.$numberInt ?? tbl.y;
|
|
21
|
+
|
|
22
|
+
// Skip tables that don't match the selected zitplaats (if one is selected)
|
|
23
|
+
if (selectedZitplaats && selectedZitplaats.trim() !== '' && tbl.zitplaats !== selectedZitplaats) {
|
|
24
|
+
return; // Skip this table
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (tbl.objectType === "Tafel" &&
|
|
28
|
+
tableNumberRaw !== undefined &&
|
|
29
|
+
minCapacityRaw !== undefined &&
|
|
30
|
+
maxCapacityRaw !== undefined &&
|
|
31
|
+
priorityRaw !== undefined &&
|
|
32
|
+
xRaw !== undefined &&
|
|
33
|
+
yRaw !== undefined)
|
|
34
|
+
{
|
|
35
|
+
allTables.push({
|
|
36
|
+
tableId: tbl.id,
|
|
37
|
+
tableNumber: parseInt(tableNumberRaw, 10),
|
|
38
|
+
minCapacity: parseInt(minCapacityRaw, 10),
|
|
39
|
+
maxCapacity: parseInt(maxCapacityRaw, 10),
|
|
40
|
+
priority: parseInt(priorityRaw, 10),
|
|
41
|
+
x: parseInt(xRaw, 10),
|
|
42
|
+
y: parseInt(yRaw, 10),
|
|
43
|
+
isTemporary: tbl.isTemporary === true, // Ensure boolean
|
|
44
|
+
startDate: tbl.startDate || null, // Expects 'YYYY-MM-DD'
|
|
45
|
+
endDate: tbl.endDate || null, // Expects 'YYYY-MM-DD'
|
|
46
|
+
application: tbl.application || null, // Expects 'breakfast', 'lunch', or 'dinner'
|
|
47
|
+
zitplaats: tbl.zitplaats || null // Add zitplaats to the table object
|
|
48
|
+
});
|
|
49
|
+
} else if (tbl.objectType === "Tafel") {
|
|
50
|
+
console.warn(`Skipping table due to missing essential properties: ${JSON.stringify(tbl)}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
console.warn("Restaurant data is missing 'floors' array.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sort tables
|
|
60
|
+
allTables.sort((a, b) => {
|
|
61
|
+
if (a.maxCapacity !== b.maxCapacity) {
|
|
62
|
+
return a.maxCapacity - b.maxCapacity;
|
|
63
|
+
}
|
|
64
|
+
if (a.priority !== b.priority) {
|
|
65
|
+
// Assuming lower priority number means higher priority
|
|
66
|
+
return a.priority - b.priority;
|
|
67
|
+
}
|
|
68
|
+
return a.minCapacity - b.minCapacity;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Filter out tables where parsing failed (resulted in NaN)
|
|
72
|
+
allTables = allTables.filter(t =>
|
|
73
|
+
!isNaN(t.tableNumber) &&
|
|
74
|
+
!isNaN(t.minCapacity) &&
|
|
75
|
+
!isNaN(t.maxCapacity) &&
|
|
76
|
+
!isNaN(t.priority) &&
|
|
77
|
+
!isNaN(t.x) &&
|
|
78
|
+
!isNaN(t.y)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return allTables;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = getAllTables;
|
package/tables/main.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Central public API for all table logic (replaces simulateTableAssignment.js)
|
|
2
|
+
|
|
3
|
+
// Import primitives directly from their files to avoid circular deps.
|
|
4
|
+
const isTimeAvailableSync = require('./availability/isTimeAvailableSync');
|
|
5
|
+
const getAvailableTablesForTime = require('./availability/getAvailableTablesForTime');
|
|
6
|
+
const filterTimeblocksByTableAvailability = require('./assignment/filterTimeblocksByTableAvailability');
|
|
7
|
+
|
|
8
|
+
// Keep backward compatibility: expose getAvailableTimeblocksWithTableCheck
|
|
9
|
+
// as an alias of filterTimeblocksByTableAvailability so existing imports continue to work.
|
|
10
|
+
const getAvailableTimeblocksWithTableCheck = (
|
|
11
|
+
restaurantData,
|
|
12
|
+
date,
|
|
13
|
+
timeblocks,
|
|
14
|
+
guests,
|
|
15
|
+
reservations,
|
|
16
|
+
selectedZitplaats = null
|
|
17
|
+
) =>
|
|
18
|
+
filterTimeblocksByTableAvailability(
|
|
19
|
+
restaurantData,
|
|
20
|
+
date,
|
|
21
|
+
timeblocks,
|
|
22
|
+
guests,
|
|
23
|
+
reservations,
|
|
24
|
+
selectedZitplaats
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
isTimeAvailableSync,
|
|
29
|
+
filterTimeblocksByTableAvailability,
|
|
30
|
+
getAvailableTimeblocksWithTableCheck,
|
|
31
|
+
getAvailableTablesForTime,
|
|
32
|
+
// assignTablesForGivenTime
|
|
33
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a reservation's start time and duration into discrete time slots (in minutes from midnight).
|
|
3
|
+
*/
|
|
4
|
+
function computeRequiredSlots(timeString, durationMinutes, intervalMinutes) {
|
|
5
|
+
const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
6
|
+
if (!timeString || !timePattern.test(timeString)) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const [hour, minute] = timeString.split(":").map(Number);
|
|
11
|
+
const startMinutes = hour * 60 + minute;
|
|
12
|
+
|
|
13
|
+
if (intervalMinutes <= 0) {
|
|
14
|
+
return [startMinutes];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const slotCount = Math.ceil(durationMinutes / intervalMinutes);
|
|
18
|
+
|
|
19
|
+
const slots = [];
|
|
20
|
+
for (let i = 0; i < slotCount; i++) {
|
|
21
|
+
slots.push(startMinutes + i * intervalMinutes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return slots;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = computeRequiredSlots;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const parseTime = require('./parseTime');
|
|
2
|
+
const { shifts } = require('./shifts');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Determines the meal type ('breakfast', 'lunch', 'dinner') for a given time string ("HH:MM").
|
|
6
|
+
* Returns null if the time doesn't fall into a defined shift.
|
|
7
|
+
*/
|
|
8
|
+
function getMealTypeByTime(timeStr) {
|
|
9
|
+
const time = parseTime(timeStr);
|
|
10
|
+
if (isNaN(time)) return null;
|
|
11
|
+
|
|
12
|
+
for (const [mealType, shift] of Object.entries(shifts)) {
|
|
13
|
+
const start = parseTime(shift.start);
|
|
14
|
+
const end = parseTime(shift.end);
|
|
15
|
+
// Handle potential errors from parseTime if shift definitions are invalid
|
|
16
|
+
if (isNaN(start) || isNaN(end)) continue;
|
|
17
|
+
|
|
18
|
+
// Check if time falls within the shift range [start, end)
|
|
19
|
+
if (time >= start && time < end) {
|
|
20
|
+
return mealType;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null; // Return null if no matching shift is found
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = getMealTypeByTime;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// --- Time and Shift Helpers ---
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses a time string ("HH:MM") into minutes since midnight.
|
|
5
|
+
* Returns NaN if the format is invalid.
|
|
6
|
+
*/
|
|
7
|
+
function parseTime(timeStr) {
|
|
8
|
+
if (!timeStr || typeof timeStr !== 'string') return NaN;
|
|
9
|
+
const parts = timeStr.split(':');
|
|
10
|
+
if (parts.length !== 2) return NaN;
|
|
11
|
+
const hours = parseInt(parts[0], 10);
|
|
12
|
+
const minutes = parseInt(parts[1], 10);
|
|
13
|
+
if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
14
|
+
return NaN;
|
|
15
|
+
}
|
|
16
|
+
return hours * 60 + minutes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = parseTime;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates Euclidean distance between two tables (assuming x, y properties).
|
|
3
|
+
*/
|
|
4
|
+
function calculateDistance(tableA, tableB) {
|
|
5
|
+
if (tableA?.x === undefined || tableA?.y === undefined || tableB?.x === undefined || tableB?.y === undefined) {
|
|
6
|
+
return Infinity; // Cannot calculate distance if coordinates are missing
|
|
7
|
+
}
|
|
8
|
+
const dx = tableA.x - tableB.x;
|
|
9
|
+
const dy = tableA.y - tableB.y;
|
|
10
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = calculateDistance;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the given tableNumber is free for all requiredSlots based on the occupied map.
|
|
3
|
+
*/
|
|
4
|
+
function isTableFreeForAllSlots(tableNumber, requiredSlots, tableOccupiedSlots) {
|
|
5
|
+
const occupiedSlots = tableOccupiedSlots[tableNumber] || new Set();
|
|
6
|
+
for (const slot of requiredSlots) {
|
|
7
|
+
if (occupiedSlots.has(slot)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = isTableFreeForAllSlots;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const getMealTypeByTime = require('../time/getMealTypeByTime');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a temporary table is valid for a specific reservation date and time.
|
|
5
|
+
* @param {Object} table - The table object (must include isTemporary, startDate, endDate, application).
|
|
6
|
+
* @param {string} reservationDateStr - The date of the reservation ("YYYY-MM-DD").
|
|
7
|
+
* @param {string} reservationTimeStr - The time of the reservation ("HH:MM").
|
|
8
|
+
* @returns {boolean} True if the table is valid, false otherwise.
|
|
9
|
+
*/
|
|
10
|
+
function isTemporaryTableValid(table, reservationDateStr, reservationTimeStr) {
|
|
11
|
+
if (!table.isTemporary) {
|
|
12
|
+
return true; // Not temporary, always valid (subject to other checks like availability)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check date range
|
|
16
|
+
if (!table.startDate || !table.endDate) {
|
|
17
|
+
return false; // Invalid temporary table definition
|
|
18
|
+
}
|
|
19
|
+
// Basic date string comparison (YYYY-MM-DD format)
|
|
20
|
+
if (reservationDateStr < table.startDate || reservationDateStr > table.endDate) {
|
|
21
|
+
// console.log(`Temporary Table ${table.tableNumber} skipped: Date ${reservationDateStr} outside range ${table.startDate}-${table.endDate}.`); // Optional verbose logging
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check application (meal type/shift)
|
|
26
|
+
const reservationMealType = getMealTypeByTime(reservationTimeStr);
|
|
27
|
+
if (!reservationMealType) {
|
|
28
|
+
return false; // Cannot determine meal type for the reservation
|
|
29
|
+
}
|
|
30
|
+
if (table.application !== reservationMealType) {
|
|
31
|
+
// console.log(`Temporary Table ${table.tableNumber} skipped: Application '${table.application}' does not match reservation meal type '${reservationMealType}'.`); // Optional verbose logging
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// console.log(`Temporary Table ${table.tableNumber} is valid for ${reservationDateStr} at ${reservationTimeStr} (${reservationMealType}).`); // Optional verbose logging
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = isTemporaryTableValid;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust helper for parsing numeric values from various formats.
|
|
3
|
+
* Handles MongoDB $numberInt format and regular values.
|
|
4
|
+
*/
|
|
5
|
+
function safeParseInt(val, defaultValue) {
|
|
6
|
+
if (val === undefined || val === null) return defaultValue;
|
|
7
|
+
|
|
8
|
+
if (typeof val === 'object' && val !== null && '$numberInt' in val) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = parseInt(val.$numberInt, 10);
|
|
11
|
+
if (!isNaN(parsed)) return parsed;
|
|
12
|
+
} catch (e) {}
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof val === 'number' && !isNaN(val)) {
|
|
17
|
+
return val;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof val === 'string') {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = parseInt(val, 10);
|
|
23
|
+
if (!isNaN(parsed)) return parsed;
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
}
|
|
26
|
+
return defaultValue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = safeParseInt;
|