@happychef/algorithm 1.3.2 → 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.
Files changed (71) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/.github/workflows/ci-cd.yml +80 -80
  3. package/BRANCH_PROTECTION_SETUP.md +167 -167
  4. package/CHANGELOG.md +8 -8
  5. package/README.md +144 -144
  6. package/RESERVERINGEN_GIDS.md +986 -986
  7. package/__tests__/crossMidnight.test.js +63 -0
  8. package/__tests__/crossMidnightTimeblocks.test.js +312 -0
  9. package/__tests__/edgeCases.test.js +271 -0
  10. package/__tests__/filters.test.js +276 -276
  11. package/__tests__/isDateAvailable.test.js +179 -175
  12. package/__tests__/isTimeAvailable.test.js +174 -168
  13. package/__tests__/restaurantData.test.js +422 -422
  14. package/__tests__/tableHelpers.test.js +247 -247
  15. package/assignTables.js +506 -444
  16. package/changes/2025/December/PR2___change.md +14 -14
  17. package/changes/2025/December/PR3_add__change.md +20 -20
  18. package/changes/2025/December/PR4___.md +15 -15
  19. package/changes/2025/December/PR5___.md +15 -15
  20. package/changes/2025/December/PR6__del_.md +17 -17
  21. package/changes/2025/December/PR7_add__change.md +21 -21
  22. package/changes/2026/February/PR15_add__change.md +21 -21
  23. package/changes/2026/February/PR16_add__.md +20 -0
  24. package/changes/2026/February/PR16_add_getDateClosingReasons.md +31 -31
  25. package/changes/2026/January/PR10_add__change.md +21 -21
  26. package/changes/2026/January/PR11_add__change.md +19 -19
  27. package/changes/2026/January/PR12_add__.md +21 -21
  28. package/changes/2026/January/PR13_add__change.md +20 -20
  29. package/changes/2026/January/PR14_add__change.md +19 -19
  30. package/changes/2026/January/PR8_add__change.md +38 -38
  31. package/changes/2026/January/PR9_add__change.md +19 -19
  32. package/dateHelpers.js +31 -0
  33. package/filters/maxArrivalsFilter.js +114 -114
  34. package/filters/maxGroupsFilter.js +221 -221
  35. package/filters/timeFilter.js +89 -89
  36. package/getAvailableTimeblocks.js +158 -158
  37. package/getDateClosingReasons.js +193 -193
  38. package/grouping.js +162 -162
  39. package/index.js +48 -43
  40. package/isDateAvailable.js +80 -80
  41. package/isDateAvailableWithTableCheck.js +172 -172
  42. package/isTimeAvailable.js +26 -26
  43. package/jest.config.js +23 -23
  44. package/package.json +27 -27
  45. package/processing/dailyGuestCounts.js +73 -73
  46. package/processing/mealTypeCount.js +133 -133
  47. package/processing/timeblocksAvailable.js +344 -182
  48. package/reservation_data/counter.js +82 -75
  49. package/restaurant_data/exceptions.js +150 -150
  50. package/restaurant_data/openinghours.js +142 -142
  51. package/simulateTableAssignment.js +833 -726
  52. package/tableHelpers.js +209 -209
  53. package/tables/time/parseTime.js +19 -19
  54. package/tables/time/shifts.js +7 -7
  55. package/tables/utils/calculateDistance.js +13 -13
  56. package/tables/utils/isTableFreeForAllSlots.js +14 -14
  57. package/tables/utils/isTemporaryTableValid.js +39 -39
  58. package/test/test_counter.js +194 -194
  59. package/test/test_dailyCount.js +81 -81
  60. package/test/test_datesAvailable.js +106 -106
  61. package/test/test_exceptions.js +172 -172
  62. package/test/test_isDateAvailable.js +330 -330
  63. package/test/test_mealTypeCount.js +54 -54
  64. package/test/test_timesAvailable.js +88 -88
  65. package/test-detailed-filter.js +100 -100
  66. package/test-lunch-debug.js +110 -110
  67. package/test-max-arrivals-filter.js +79 -79
  68. package/test-meal-stop-fix.js +147 -147
  69. package/test-meal-stop-simple.js +93 -93
  70. package/test-timezone-debug.js +47 -47
  71. 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 test = {
19
- ...require("./assignTables"),
20
- ...require("./getAvailableTimeblocks"),
21
- ...require("./grouping"),
22
- ...require("./isDateAvailable"),
23
- ...require("./isTimeAvailable"),
24
- ...require("./simulateTableAssignment"),
25
- ...require("./isDateAvailableWithTableCheck"),
26
- ...require("./tableHelpers"),
27
- ...require("./test"),
28
- ...require("./getDateClosingReasons")
29
- };
30
-
31
- const filters = {
32
- ...require("./filters/timeFilter"),
33
- ...require("./filters/maxArrivalsFilter"),
34
- ...require("./filters/maxGroupsFilter")
35
- };
36
- // Merge all exports into one
37
- module.exports = {
38
- ...processing,
39
- ...reservation_data,
40
- ...restaurant_data,
41
- ...test,
42
- ...filters
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
+ };
@@ -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
+ };