@happychef/algorithm 1.3.0 → 1.4.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/.claude/settings.local.json +16 -0
- package/.github/workflows/ci-cd.yml +80 -80
- package/BRANCH_PROTECTION_SETUP.md +167 -167
- package/CHANGELOG.md +8 -8
- package/README.md +144 -144
- package/RESERVERINGEN_GIDS.md +986 -986
- package/__tests__/crossMidnight.test.js +63 -0
- package/__tests__/crossMidnightTimeblocks.test.js +312 -0
- package/__tests__/edgeCases.test.js +271 -0
- package/__tests__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +179 -175
- package/__tests__/isTimeAvailable.test.js +174 -168
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +506 -444
- 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 +15 -15
- package/changes/2025/December/PR5___.md +15 -15
- package/changes/2025/December/PR6__del_.md +17 -17
- package/changes/2025/December/PR7_add__change.md +21 -21
- package/changes/2026/February/PR15_add__change.md +21 -21
- package/changes/2026/February/PR16_add__.md +20 -0
- package/changes/2026/February/PR16_add_getDateClosingReasons.md +31 -31
- package/changes/2026/January/PR10_add__change.md +21 -21
- package/changes/2026/January/PR11_add__change.md +19 -19
- package/changes/2026/January/PR12_add__.md +21 -21
- package/changes/2026/January/PR13_add__change.md +20 -20
- package/changes/2026/January/PR14_add__change.md +19 -19
- package/changes/2026/January/PR8_add__change.md +38 -38
- package/changes/2026/January/PR9_add__change.md +19 -19
- package/dateHelpers.js +31 -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/getDateClosingReasons.js +193 -193
- package/grouping.js +162 -162
- package/index.js +48 -43
- package/isDateAvailable.js +80 -80
- package/isDateAvailableWithTableCheck.js +172 -172
- package/isTimeAvailable.js +26 -26
- package/jest.config.js +23 -23
- package/package.json +27 -27
- package/processing/dailyGuestCounts.js +73 -73
- package/processing/mealTypeCount.js +133 -133
- package/processing/timeblocksAvailable.js +344 -182
- package/reservation_data/counter.js +82 -75
- package/restaurant_data/exceptions.js +150 -150
- package/restaurant_data/openinghours.js +142 -142
- package/simulateTableAssignment.js +833 -726
- package/tableHelpers.js +209 -209
- 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-detailed-filter.js +100 -100
- package/test-lunch-debug.js +110 -110
- package/test-max-arrivals-filter.js +79 -79
- package/test-meal-stop-fix.js +147 -147
- package/test-meal-stop-simple.js +93 -93
- package/test-timezone-debug.js +47 -47
- package/test.js +336 -336
package/grouping.js
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
// grouping.js
|
|
2
|
-
/**
|
|
3
|
-
* Try to allocate a reservation via pinned groups.
|
|
4
|
-
* Returns { tables: number[], tableIds: string[], viaGroup: string } on success,
|
|
5
|
-
* null if grouping is enabled but no match (and strict=false),
|
|
6
|
-
* and throws if grouping is strict and no group matches.
|
|
7
|
-
*
|
|
8
|
-
* Dependencies (passed in): isTemporaryTableValid, isTableFreeForAllSlots
|
|
9
|
-
*/
|
|
10
|
-
function tryGroupTables({
|
|
11
|
-
restaurantSettings,
|
|
12
|
-
allTables,
|
|
13
|
-
guests,
|
|
14
|
-
date,
|
|
15
|
-
time,
|
|
16
|
-
requiredSlots,
|
|
17
|
-
tableOccupiedSlots,
|
|
18
|
-
isTemporaryTableValid,
|
|
19
|
-
isTableFreeForAllSlots,
|
|
20
|
-
}) {
|
|
21
|
-
const gset = restaurantSettings["grouping-settings"] || {};
|
|
22
|
-
const groupingEnabled = gset.enable === true;
|
|
23
|
-
const groupingStrict = gset.strict === true;
|
|
24
|
-
const maxSubtablesPerGroup = Number.isFinite(gset.maxSubtables)
|
|
25
|
-
? gset.maxSubtables
|
|
26
|
-
: undefined;
|
|
27
|
-
const orderStrategy = gset.orderStrategy || "smallest-first"; // 'smallest-first' | 'capacity-asc' | 'closest-fit'
|
|
28
|
-
const seed = gset.seed ?? 0;
|
|
29
|
-
|
|
30
|
-
const pinnedGroups = Array.isArray(restaurantSettings.pinnedGroups)
|
|
31
|
-
? restaurantSettings.pinnedGroups
|
|
32
|
-
: [];
|
|
33
|
-
|
|
34
|
-
if (!groupingEnabled || pinnedGroups.length === 0) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Build lookups from allTables
|
|
39
|
-
const byId = new Map(allTables.map((t) => [String(t.tableId), t]));
|
|
40
|
-
const byNumber = new Map(
|
|
41
|
-
allTables
|
|
42
|
-
.filter((t) => typeof t.tableNumber === "number")
|
|
43
|
-
.map((t) => [t.tableNumber, t])
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// Quick check: is there already a single-table fit?
|
|
47
|
-
const hasSingleFit = allTables.some(
|
|
48
|
-
(t) =>
|
|
49
|
-
t.minCapacity <= guests &&
|
|
50
|
-
guests <= t.maxCapacity &&
|
|
51
|
-
isTemporaryTableValid(t, date, time) &&
|
|
52
|
-
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
|
|
53
|
-
);
|
|
54
|
-
if (hasSingleFit) {
|
|
55
|
-
// Let caller handle single-table path; grouping is only used when no single table fits
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Stable group ordering
|
|
60
|
-
const stableGroups = [...pinnedGroups].sort((a, b) => {
|
|
61
|
-
const aid = String(a.id || a.name || "");
|
|
62
|
-
const bid = String(b.id || b.name || "");
|
|
63
|
-
return aid.localeCompare(bid);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Deterministic RNG (kept for parity; not used by default)
|
|
67
|
-
const rnd = (() => {
|
|
68
|
-
let x = Math.imul(0x9e3779b1, (seed | 0) + 0x7f4a7c15) | 0;
|
|
69
|
-
return () => (
|
|
70
|
-
(x = (x ^ (x >>> 15)) * (x | 1)),
|
|
71
|
-
(x ^= x + Math.imul(x ^ (x >>> 7), x | 61)),
|
|
72
|
-
((x ^ (x >>> 14)) >>> 0) / 4294967296
|
|
73
|
-
);
|
|
74
|
-
})();
|
|
75
|
-
|
|
76
|
-
// Subtable ordering strategy
|
|
77
|
-
const subSort = (a, b) => {
|
|
78
|
-
if (orderStrategy === "closest-fit") {
|
|
79
|
-
const slA = (a.maxCapacity || 0) - (a.minCapacity || 0);
|
|
80
|
-
const slB = (b.maxCapacity || 0) - (b.minCapacity || 0);
|
|
81
|
-
if (slA !== slB) return slA - slB;
|
|
82
|
-
}
|
|
83
|
-
if (a.maxCapacity !== b.maxCapacity) return a.maxCapacity - b.maxCapacity;
|
|
84
|
-
if (a.tableNumber !== b.tableNumber) return a.tableNumber - b.tableNumber;
|
|
85
|
-
const ida = String(a.tableId),
|
|
86
|
-
idb = String(b.tableId);
|
|
87
|
-
return ida.localeCompare(idb);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Try each pinned group
|
|
91
|
-
for (const g of stableGroups) {
|
|
92
|
-
const rawIds = Array.isArray(g.tableIds) ? g.tableIds.map(String) : [];
|
|
93
|
-
const rawNums = Array.isArray(g.tableNumbers)
|
|
94
|
-
? g.tableNumbers.filter((n) => typeof n === "number")
|
|
95
|
-
: [];
|
|
96
|
-
|
|
97
|
-
let subs = [];
|
|
98
|
-
if (rawIds.length) subs = rawIds.map((id) => byId.get(id)).filter(Boolean);
|
|
99
|
-
else if (rawNums.length)
|
|
100
|
-
subs = rawNums.map((n) => byNumber.get(n)).filter(Boolean);
|
|
101
|
-
|
|
102
|
-
if (maxSubtablesPerGroup && subs.length > maxSubtablesPerGroup) {
|
|
103
|
-
subs = subs.slice(0, maxSubtablesPerGroup);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (subs.length < 2) continue;
|
|
107
|
-
|
|
108
|
-
// All subtables must be valid & free for all required slots
|
|
109
|
-
if (
|
|
110
|
-
!subs.every(
|
|
111
|
-
(t) =>
|
|
112
|
-
isTemporaryTableValid(t, date, time) &&
|
|
113
|
-
isTableFreeForAllSlots(
|
|
114
|
-
t.tableNumber,
|
|
115
|
-
requiredSlots,
|
|
116
|
-
tableOccupiedSlots
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Capacity checks
|
|
124
|
-
const totalMax = subs.reduce((s, t) => s + (t.maxCapacity || 0), 0);
|
|
125
|
-
const totalMin = subs.reduce((s, t) => s + (t.minCapacity || 0), 0);
|
|
126
|
-
if (guests < totalMin) continue;
|
|
127
|
-
if (guests > totalMax) continue;
|
|
128
|
-
|
|
129
|
-
// Deterministic allocation:
|
|
130
|
-
const ordered = [...subs].sort(subSort);
|
|
131
|
-
const alloc = new Map(
|
|
132
|
-
ordered.map((t) => [t.tableNumber, Math.max(0, t.minCapacity || 0)])
|
|
133
|
-
);
|
|
134
|
-
let remaining = guests - [...alloc.values()].reduce((s, v) => s + v, 0);
|
|
135
|
-
if (remaining < 0) continue;
|
|
136
|
-
|
|
137
|
-
for (const t of ordered) {
|
|
138
|
-
if (remaining <= 0) break;
|
|
139
|
-
const cur = alloc.get(t.tableNumber);
|
|
140
|
-
const addable = Math.max(0, (t.maxCapacity || 0) - cur);
|
|
141
|
-
const give = Math.min(addable, remaining);
|
|
142
|
-
alloc.set(t.tableNumber, cur + give);
|
|
143
|
-
remaining -= give;
|
|
144
|
-
}
|
|
145
|
-
if (remaining !== 0) continue; // couldn’t fit exactly
|
|
146
|
-
|
|
147
|
-
// Success
|
|
148
|
-
return {
|
|
149
|
-
tables: ordered.map((t) => t.tableNumber),
|
|
150
|
-
tableIds: ordered.map((t) => t.tableId),
|
|
151
|
-
viaGroup: String(g.id || g.name || "pinned-group"),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// No match
|
|
156
|
-
if (groupingStrict) {
|
|
157
|
-
throw new Error("No matching pinned group found (strict grouping)");
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = { tryGroupTables };
|
|
1
|
+
// grouping.js
|
|
2
|
+
/**
|
|
3
|
+
* Try to allocate a reservation via pinned groups.
|
|
4
|
+
* Returns { tables: number[], tableIds: string[], viaGroup: string } on success,
|
|
5
|
+
* null if grouping is enabled but no match (and strict=false),
|
|
6
|
+
* and throws if grouping is strict and no group matches.
|
|
7
|
+
*
|
|
8
|
+
* Dependencies (passed in): isTemporaryTableValid, isTableFreeForAllSlots
|
|
9
|
+
*/
|
|
10
|
+
function tryGroupTables({
|
|
11
|
+
restaurantSettings,
|
|
12
|
+
allTables,
|
|
13
|
+
guests,
|
|
14
|
+
date,
|
|
15
|
+
time,
|
|
16
|
+
requiredSlots,
|
|
17
|
+
tableOccupiedSlots,
|
|
18
|
+
isTemporaryTableValid,
|
|
19
|
+
isTableFreeForAllSlots,
|
|
20
|
+
}) {
|
|
21
|
+
const gset = restaurantSettings["grouping-settings"] || {};
|
|
22
|
+
const groupingEnabled = gset.enable === true;
|
|
23
|
+
const groupingStrict = gset.strict === true;
|
|
24
|
+
const maxSubtablesPerGroup = Number.isFinite(gset.maxSubtables)
|
|
25
|
+
? gset.maxSubtables
|
|
26
|
+
: undefined;
|
|
27
|
+
const orderStrategy = gset.orderStrategy || "smallest-first"; // 'smallest-first' | 'capacity-asc' | 'closest-fit'
|
|
28
|
+
const seed = gset.seed ?? 0;
|
|
29
|
+
|
|
30
|
+
const pinnedGroups = Array.isArray(restaurantSettings.pinnedGroups)
|
|
31
|
+
? restaurantSettings.pinnedGroups
|
|
32
|
+
: [];
|
|
33
|
+
|
|
34
|
+
if (!groupingEnabled || pinnedGroups.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build lookups from allTables
|
|
39
|
+
const byId = new Map(allTables.map((t) => [String(t.tableId), t]));
|
|
40
|
+
const byNumber = new Map(
|
|
41
|
+
allTables
|
|
42
|
+
.filter((t) => typeof t.tableNumber === "number")
|
|
43
|
+
.map((t) => [t.tableNumber, t])
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Quick check: is there already a single-table fit?
|
|
47
|
+
const hasSingleFit = allTables.some(
|
|
48
|
+
(t) =>
|
|
49
|
+
t.minCapacity <= guests &&
|
|
50
|
+
guests <= t.maxCapacity &&
|
|
51
|
+
isTemporaryTableValid(t, date, time) &&
|
|
52
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
|
|
53
|
+
);
|
|
54
|
+
if (hasSingleFit) {
|
|
55
|
+
// Let caller handle single-table path; grouping is only used when no single table fits
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Stable group ordering
|
|
60
|
+
const stableGroups = [...pinnedGroups].sort((a, b) => {
|
|
61
|
+
const aid = String(a.id || a.name || "");
|
|
62
|
+
const bid = String(b.id || b.name || "");
|
|
63
|
+
return aid.localeCompare(bid);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Deterministic RNG (kept for parity; not used by default)
|
|
67
|
+
const rnd = (() => {
|
|
68
|
+
let x = Math.imul(0x9e3779b1, (seed | 0) + 0x7f4a7c15) | 0;
|
|
69
|
+
return () => (
|
|
70
|
+
(x = (x ^ (x >>> 15)) * (x | 1)),
|
|
71
|
+
(x ^= x + Math.imul(x ^ (x >>> 7), x | 61)),
|
|
72
|
+
((x ^ (x >>> 14)) >>> 0) / 4294967296
|
|
73
|
+
);
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
// Subtable ordering strategy
|
|
77
|
+
const subSort = (a, b) => {
|
|
78
|
+
if (orderStrategy === "closest-fit") {
|
|
79
|
+
const slA = (a.maxCapacity || 0) - (a.minCapacity || 0);
|
|
80
|
+
const slB = (b.maxCapacity || 0) - (b.minCapacity || 0);
|
|
81
|
+
if (slA !== slB) return slA - slB;
|
|
82
|
+
}
|
|
83
|
+
if (a.maxCapacity !== b.maxCapacity) return a.maxCapacity - b.maxCapacity;
|
|
84
|
+
if (a.tableNumber !== b.tableNumber) return a.tableNumber - b.tableNumber;
|
|
85
|
+
const ida = String(a.tableId),
|
|
86
|
+
idb = String(b.tableId);
|
|
87
|
+
return ida.localeCompare(idb);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Try each pinned group
|
|
91
|
+
for (const g of stableGroups) {
|
|
92
|
+
const rawIds = Array.isArray(g.tableIds) ? g.tableIds.map(String) : [];
|
|
93
|
+
const rawNums = Array.isArray(g.tableNumbers)
|
|
94
|
+
? g.tableNumbers.filter((n) => typeof n === "number")
|
|
95
|
+
: [];
|
|
96
|
+
|
|
97
|
+
let subs = [];
|
|
98
|
+
if (rawIds.length) subs = rawIds.map((id) => byId.get(id)).filter(Boolean);
|
|
99
|
+
else if (rawNums.length)
|
|
100
|
+
subs = rawNums.map((n) => byNumber.get(n)).filter(Boolean);
|
|
101
|
+
|
|
102
|
+
if (maxSubtablesPerGroup && subs.length > maxSubtablesPerGroup) {
|
|
103
|
+
subs = subs.slice(0, maxSubtablesPerGroup);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (subs.length < 2) continue;
|
|
107
|
+
|
|
108
|
+
// All subtables must be valid & free for all required slots
|
|
109
|
+
if (
|
|
110
|
+
!subs.every(
|
|
111
|
+
(t) =>
|
|
112
|
+
isTemporaryTableValid(t, date, time) &&
|
|
113
|
+
isTableFreeForAllSlots(
|
|
114
|
+
t.tableNumber,
|
|
115
|
+
requiredSlots,
|
|
116
|
+
tableOccupiedSlots
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Capacity checks
|
|
124
|
+
const totalMax = subs.reduce((s, t) => s + (t.maxCapacity || 0), 0);
|
|
125
|
+
const totalMin = subs.reduce((s, t) => s + (t.minCapacity || 0), 0);
|
|
126
|
+
if (guests < totalMin) continue;
|
|
127
|
+
if (guests > totalMax) continue;
|
|
128
|
+
|
|
129
|
+
// Deterministic allocation:
|
|
130
|
+
const ordered = [...subs].sort(subSort);
|
|
131
|
+
const alloc = new Map(
|
|
132
|
+
ordered.map((t) => [t.tableNumber, Math.max(0, t.minCapacity || 0)])
|
|
133
|
+
);
|
|
134
|
+
let remaining = guests - [...alloc.values()].reduce((s, v) => s + v, 0);
|
|
135
|
+
if (remaining < 0) continue;
|
|
136
|
+
|
|
137
|
+
for (const t of ordered) {
|
|
138
|
+
if (remaining <= 0) break;
|
|
139
|
+
const cur = alloc.get(t.tableNumber);
|
|
140
|
+
const addable = Math.max(0, (t.maxCapacity || 0) - cur);
|
|
141
|
+
const give = Math.min(addable, remaining);
|
|
142
|
+
alloc.set(t.tableNumber, cur + give);
|
|
143
|
+
remaining -= give;
|
|
144
|
+
}
|
|
145
|
+
if (remaining !== 0) continue; // couldn’t fit exactly
|
|
146
|
+
|
|
147
|
+
// Success
|
|
148
|
+
return {
|
|
149
|
+
tables: ordered.map((t) => t.tableNumber),
|
|
150
|
+
tableIds: ordered.map((t) => t.tableId),
|
|
151
|
+
viaGroup: String(g.id || g.name || "pinned-group"),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// No match
|
|
156
|
+
if (groupingStrict) {
|
|
157
|
+
throw new Error("No matching pinned group found (strict grouping)");
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { tryGroupTables };
|
package/index.js
CHANGED
|
@@ -1,43 +1,48 @@
|
|
|
1
|
-
// algorithm/index.js
|
|
2
|
-
|
|
3
|
-
const processing = {
|
|
4
|
-
...require("./processing/dailyGuestCounts"),
|
|
5
|
-
...require("./processing/mealTypeCount"),
|
|
6
|
-
...require("./processing/timeblocksAvailable")
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const reservation_data = {
|
|
10
|
-
...require("./reservation_data/counter")
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const restaurant_data = {
|
|
14
|
-
...require("./restaurant_data/exceptions"),
|
|
15
|
-
...require("./restaurant_data/openinghours")
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
...require("./
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
...require("./
|
|
24
|
-
...require("./
|
|
25
|
-
...require("./
|
|
26
|
-
...require("./
|
|
27
|
-
...require("./
|
|
28
|
-
...require("./
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...require("./
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
...
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
...
|
|
43
|
-
|
|
1
|
+
// algorithm/index.js
|
|
2
|
+
|
|
3
|
+
const processing = {
|
|
4
|
+
...require("./processing/dailyGuestCounts"),
|
|
5
|
+
...require("./processing/mealTypeCount"),
|
|
6
|
+
...require("./processing/timeblocksAvailable")
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const reservation_data = {
|
|
10
|
+
...require("./reservation_data/counter")
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const restaurant_data = {
|
|
14
|
+
...require("./restaurant_data/exceptions"),
|
|
15
|
+
...require("./restaurant_data/openinghours")
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const dateHelpers = {
|
|
19
|
+
...require("./dateHelpers")
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const test = {
|
|
23
|
+
...require("./assignTables"),
|
|
24
|
+
...require("./getAvailableTimeblocks"),
|
|
25
|
+
...require("./grouping"),
|
|
26
|
+
...require("./isDateAvailable"),
|
|
27
|
+
...require("./isTimeAvailable"),
|
|
28
|
+
...require("./simulateTableAssignment"),
|
|
29
|
+
...require("./isDateAvailableWithTableCheck"),
|
|
30
|
+
...require("./tableHelpers"),
|
|
31
|
+
...require("./test"),
|
|
32
|
+
...require("./getDateClosingReasons")
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const filters = {
|
|
36
|
+
...require("./filters/timeFilter"),
|
|
37
|
+
...require("./filters/maxArrivalsFilter"),
|
|
38
|
+
...require("./filters/maxGroupsFilter")
|
|
39
|
+
};
|
|
40
|
+
// Merge all exports into one
|
|
41
|
+
module.exports = {
|
|
42
|
+
...processing,
|
|
43
|
+
...reservation_data,
|
|
44
|
+
...restaurant_data,
|
|
45
|
+
...dateHelpers,
|
|
46
|
+
...test,
|
|
47
|
+
...filters
|
|
48
|
+
};
|
package/isDateAvailable.js
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
// isDateAvailable.js
|
|
2
|
-
|
|
3
|
-
const { getAvailableTimeblocks } = require('./getAvailableTimeblocks');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parses a time string in "HH:MM" format into minutes since midnight.
|
|
7
|
-
* @param {string} timeStr - Time string in "HH:MM" format.
|
|
8
|
-
* @returns {number} Minutes since midnight.
|
|
9
|
-
*/
|
|
10
|
-
function parseTime(timeStr) {
|
|
11
|
-
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
12
|
-
return hours * 60 + minutes;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Checks if a date is within the allowed future range defined by dagenInToekomst.
|
|
17
|
-
* @param {Object} data - The main data object (to access general settings).
|
|
18
|
-
* @param {string} dateStr - The date string (YYYY-MM-DD).
|
|
19
|
-
* @returns {boolean} true if within range, false otherwise.
|
|
20
|
-
*/
|
|
21
|
-
function isDateWithinAllowedRange(data, dateStr) {
|
|
22
|
-
// Get dagenInToekomst
|
|
23
|
-
let dagenInToekomst = 90;
|
|
24
|
-
if (
|
|
25
|
-
data['general-settings'] &&
|
|
26
|
-
data['general-settings'].dagenInToekomst &&
|
|
27
|
-
parseInt(data['general-settings'].dagenInToekomst, 10) > 0
|
|
28
|
-
) {
|
|
29
|
-
dagenInToekomst = parseInt(data['general-settings'].dagenInToekomst, 10);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const timeZone = 'Europe/Amsterdam';
|
|
33
|
-
|
|
34
|
-
const now = new Date();
|
|
35
|
-
const currentTimeInTimeZone = new Date(
|
|
36
|
-
now.toLocaleString('en-US', { timeZone: timeZone })
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const maxAllowedDate = new Date(currentTimeInTimeZone.getTime());
|
|
40
|
-
maxAllowedDate.setDate(maxAllowedDate.getDate() + dagenInToekomst);
|
|
41
|
-
maxAllowedDate.setHours(23, 59, 59, 999);
|
|
42
|
-
|
|
43
|
-
const [year, month, day] = dateStr.split('-').map(Number);
|
|
44
|
-
const targetDate = new Date(Date.UTC(year, month - 1, day));
|
|
45
|
-
const targetDateInTimeZone = new Date(
|
|
46
|
-
targetDate.toLocaleString('en-US', { timeZone: timeZone })
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
return targetDateInTimeZone <= maxAllowedDate;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Checks if a date is available for a reservation of a specified number of guests.
|
|
54
|
-
* This updated version uses `getAvailableTimeblocks` to ensure that it never returns
|
|
55
|
-
* true if no actual time slots are available, including for today's date.
|
|
56
|
-
* @param {Object} data - The main data object containing settings and meal information.
|
|
57
|
-
* @param {string} dateStr - The date string in "YYYY-MM-DD" format.
|
|
58
|
-
* @param {Array} reservations - An array of reservation objects.
|
|
59
|
-
* @param {number} guests - The number of guests for the reservation.
|
|
60
|
-
* @param {Array} blockedSlots - Optional array of blocked time slots to exclude.
|
|
61
|
-
* @param {string|null} giftcard - Optional giftcard to filter times by meal.
|
|
62
|
-
* @param {boolean} isAdmin - Optional flag to bypass time restrictions for admin users.
|
|
63
|
-
* @returns {boolean} - Returns true if the date has at least one available timeblock, false otherwise.
|
|
64
|
-
*/
|
|
65
|
-
function isDateAvailable(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false, duration = null) {
|
|
66
|
-
// Check if date is within allowed range (skip for admin)
|
|
67
|
-
if (!isAdmin && !isDateWithinAllowedRange(data, dateStr)) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Get available timeblocks using the existing logic
|
|
72
|
-
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, giftcard, isAdmin, duration);
|
|
73
|
-
|
|
74
|
-
// Return true only if we have at least one available timeblock
|
|
75
|
-
return Object.keys(availableTimeblocks).length > 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
module.exports = {
|
|
79
|
-
isDateAvailable,
|
|
80
|
-
};
|
|
1
|
+
// isDateAvailable.js
|
|
2
|
+
|
|
3
|
+
const { getAvailableTimeblocks } = require('./getAvailableTimeblocks');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses a time string in "HH:MM" format into minutes since midnight.
|
|
7
|
+
* @param {string} timeStr - Time string in "HH:MM" format.
|
|
8
|
+
* @returns {number} Minutes since midnight.
|
|
9
|
+
*/
|
|
10
|
+
function parseTime(timeStr) {
|
|
11
|
+
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
12
|
+
return hours * 60 + minutes;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a date is within the allowed future range defined by dagenInToekomst.
|
|
17
|
+
* @param {Object} data - The main data object (to access general settings).
|
|
18
|
+
* @param {string} dateStr - The date string (YYYY-MM-DD).
|
|
19
|
+
* @returns {boolean} true if within range, false otherwise.
|
|
20
|
+
*/
|
|
21
|
+
function isDateWithinAllowedRange(data, dateStr) {
|
|
22
|
+
// Get dagenInToekomst
|
|
23
|
+
let dagenInToekomst = 90;
|
|
24
|
+
if (
|
|
25
|
+
data['general-settings'] &&
|
|
26
|
+
data['general-settings'].dagenInToekomst &&
|
|
27
|
+
parseInt(data['general-settings'].dagenInToekomst, 10) > 0
|
|
28
|
+
) {
|
|
29
|
+
dagenInToekomst = parseInt(data['general-settings'].dagenInToekomst, 10);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const timeZone = 'Europe/Amsterdam';
|
|
33
|
+
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const currentTimeInTimeZone = new Date(
|
|
36
|
+
now.toLocaleString('en-US', { timeZone: timeZone })
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const maxAllowedDate = new Date(currentTimeInTimeZone.getTime());
|
|
40
|
+
maxAllowedDate.setDate(maxAllowedDate.getDate() + dagenInToekomst);
|
|
41
|
+
maxAllowedDate.setHours(23, 59, 59, 999);
|
|
42
|
+
|
|
43
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
44
|
+
const targetDate = new Date(Date.UTC(year, month - 1, day));
|
|
45
|
+
const targetDateInTimeZone = new Date(
|
|
46
|
+
targetDate.toLocaleString('en-US', { timeZone: timeZone })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return targetDateInTimeZone <= maxAllowedDate;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if a date is available for a reservation of a specified number of guests.
|
|
54
|
+
* This updated version uses `getAvailableTimeblocks` to ensure that it never returns
|
|
55
|
+
* true if no actual time slots are available, including for today's date.
|
|
56
|
+
* @param {Object} data - The main data object containing settings and meal information.
|
|
57
|
+
* @param {string} dateStr - The date string in "YYYY-MM-DD" format.
|
|
58
|
+
* @param {Array} reservations - An array of reservation objects.
|
|
59
|
+
* @param {number} guests - The number of guests for the reservation.
|
|
60
|
+
* @param {Array} blockedSlots - Optional array of blocked time slots to exclude.
|
|
61
|
+
* @param {string|null} giftcard - Optional giftcard to filter times by meal.
|
|
62
|
+
* @param {boolean} isAdmin - Optional flag to bypass time restrictions for admin users.
|
|
63
|
+
* @returns {boolean} - Returns true if the date has at least one available timeblock, false otherwise.
|
|
64
|
+
*/
|
|
65
|
+
function isDateAvailable(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false, duration = null) {
|
|
66
|
+
// Check if date is within allowed range (skip for admin)
|
|
67
|
+
if (!isAdmin && !isDateWithinAllowedRange(data, dateStr)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get available timeblocks using the existing logic
|
|
72
|
+
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, giftcard, isAdmin, duration);
|
|
73
|
+
|
|
74
|
+
// Return true only if we have at least one available timeblock
|
|
75
|
+
return Object.keys(availableTimeblocks).length > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
isDateAvailable,
|
|
80
|
+
};
|