@happychef/algorithm 1.2.13 → 1.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci-cd.yml +0 -154
- package/BRANCH_PROTECTION_SETUP.md +167 -167
- package/README.md +144 -144
- package/__tests__/filters.test.js +276 -276
- package/__tests__/isDateAvailable.test.js +175 -175
- package/__tests__/isTimeAvailable.test.js +168 -168
- package/__tests__/restaurantData.test.js +422 -422
- package/__tests__/tableHelpers.test.js +247 -247
- package/assignTables.js +33 -13
- package/changes/2026/January/PR11_add__change.md +20 -0
- package/getAvailableTimeblocks.js +2 -2
- package/isDateAvailable.js +2 -2
- package/isDateAvailableWithTableCheck.js +5 -4
- package/jest.config.js +23 -23
- package/package.json +1 -1
- package/processing/timeblocksAvailable.js +2 -2
- package/reservation_data/counter.js +11 -1
- package/simulateTableAssignment.js +35 -18
- package/test-detailed-filter.js +100 -100
- package/test-lunch-debug.js +110 -110
- package/test-max-arrivals-filter.js +79 -79
- package/test-timezone-debug.js +47 -47
|
@@ -1,247 +1,247 @@
|
|
|
1
|
-
const {
|
|
2
|
-
parseTime,
|
|
3
|
-
getMealTypeByTime,
|
|
4
|
-
getAllTables,
|
|
5
|
-
isTemporaryTableValid,
|
|
6
|
-
shifts
|
|
7
|
-
} = require('../tableHelpers');
|
|
8
|
-
|
|
9
|
-
describe('tableHelpers - parseTime', () => {
|
|
10
|
-
test('should parse valid time string correctly', () => {
|
|
11
|
-
expect(parseTime('12:30')).toBe(750); // 12*60 + 30 = 750
|
|
12
|
-
expect(parseTime('00:00')).toBe(0);
|
|
13
|
-
expect(parseTime('23:59')).toBe(1439);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('should return NaN for invalid time strings', () => {
|
|
17
|
-
expect(parseTime('')).toBeNaN();
|
|
18
|
-
expect(parseTime(null)).toBeNaN();
|
|
19
|
-
expect(parseTime(undefined)).toBeNaN();
|
|
20
|
-
expect(parseTime('25:00')).toBeNaN(); // Invalid hour
|
|
21
|
-
expect(parseTime('12:60')).toBeNaN(); // Invalid minute
|
|
22
|
-
expect(parseTime('12')).toBeNaN(); // Missing colon
|
|
23
|
-
expect(parseTime('ab:cd')).toBeNaN(); // Not numbers
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('should handle edge cases', () => {
|
|
27
|
-
expect(parseTime('0:0')).toBe(0);
|
|
28
|
-
expect(parseTime('1:1')).toBe(61);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('tableHelpers - getMealTypeByTime', () => {
|
|
33
|
-
test('should identify breakfast times correctly', () => {
|
|
34
|
-
expect(getMealTypeByTime('07:00')).toBe('breakfast');
|
|
35
|
-
expect(getMealTypeByTime('09:30')).toBe('breakfast');
|
|
36
|
-
expect(getMealTypeByTime('10:59')).toBe('breakfast');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('should identify lunch times correctly', () => {
|
|
40
|
-
expect(getMealTypeByTime('11:00')).toBe('lunch');
|
|
41
|
-
expect(getMealTypeByTime('12:30')).toBe('lunch');
|
|
42
|
-
expect(getMealTypeByTime('15:59')).toBe('lunch');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('should identify dinner times correctly', () => {
|
|
46
|
-
expect(getMealTypeByTime('16:00')).toBe('dinner');
|
|
47
|
-
expect(getMealTypeByTime('18:30')).toBe('dinner');
|
|
48
|
-
expect(getMealTypeByTime('22:59')).toBe('dinner');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('should return null for times outside defined shifts', () => {
|
|
52
|
-
expect(getMealTypeByTime('06:59')).toBeNull();
|
|
53
|
-
expect(getMealTypeByTime('23:00')).toBeNull();
|
|
54
|
-
expect(getMealTypeByTime('02:00')).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('should return null for invalid time strings', () => {
|
|
58
|
-
expect(getMealTypeByTime('invalid')).toBeNull();
|
|
59
|
-
expect(getMealTypeByTime('')).toBeNull();
|
|
60
|
-
expect(getMealTypeByTime(null)).toBeNull();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('tableHelpers - getAllTables', () => {
|
|
65
|
-
test('should extract tables correctly from restaurant data', () => {
|
|
66
|
-
const restaurantData = {
|
|
67
|
-
floors: [
|
|
68
|
-
{
|
|
69
|
-
tables: [
|
|
70
|
-
{
|
|
71
|
-
objectType: 'Tafel',
|
|
72
|
-
id: 'table1',
|
|
73
|
-
tableNumber: 1,
|
|
74
|
-
minCapacity: 2,
|
|
75
|
-
maxCapacity: 4,
|
|
76
|
-
priority: 1,
|
|
77
|
-
x: 10,
|
|
78
|
-
y: 20,
|
|
79
|
-
isTemporary: false
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
objectType: 'Tafel',
|
|
83
|
-
id: 'table2',
|
|
84
|
-
tableNumber: { $numberInt: '2' },
|
|
85
|
-
minCapacity: { $numberInt: '4' },
|
|
86
|
-
maxCapacity: { $numberInt: '6' },
|
|
87
|
-
priority: { $numberInt: '2' },
|
|
88
|
-
x: { $numberInt: '30' },
|
|
89
|
-
y: { $numberInt: '40' },
|
|
90
|
-
isTemporary: false
|
|
91
|
-
}
|
|
92
|
-
]
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const tables = getAllTables(restaurantData);
|
|
98
|
-
expect(tables).toHaveLength(2);
|
|
99
|
-
expect(tables[0].tableNumber).toBe(1);
|
|
100
|
-
expect(tables[0].maxCapacity).toBe(4);
|
|
101
|
-
expect(tables[1].tableNumber).toBe(2);
|
|
102
|
-
expect(tables[1].maxCapacity).toBe(6);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('should sort tables by maxCapacity, priority, and minCapacity', () => {
|
|
106
|
-
const restaurantData = {
|
|
107
|
-
floors: [
|
|
108
|
-
{
|
|
109
|
-
tables: [
|
|
110
|
-
{
|
|
111
|
-
objectType: 'Tafel',
|
|
112
|
-
id: 'table1',
|
|
113
|
-
tableNumber: 1,
|
|
114
|
-
minCapacity: 2,
|
|
115
|
-
maxCapacity: 6,
|
|
116
|
-
priority: 1,
|
|
117
|
-
x: 10,
|
|
118
|
-
y: 20
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
objectType: 'Tafel',
|
|
122
|
-
id: 'table2',
|
|
123
|
-
tableNumber: 2,
|
|
124
|
-
minCapacity: 2,
|
|
125
|
-
maxCapacity: 4,
|
|
126
|
-
priority: 1,
|
|
127
|
-
x: 30,
|
|
128
|
-
y: 40
|
|
129
|
-
}
|
|
130
|
-
]
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const tables = getAllTables(restaurantData);
|
|
136
|
-
expect(tables[0].tableNumber).toBe(2); // Smaller maxCapacity comes first
|
|
137
|
-
expect(tables[1].tableNumber).toBe(1);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('should handle empty or missing data gracefully', () => {
|
|
141
|
-
expect(getAllTables({})).toEqual([]);
|
|
142
|
-
expect(getAllTables({ floors: [] })).toEqual([]);
|
|
143
|
-
expect(getAllTables(null)).toEqual([]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('should filter out non-Tafel objects', () => {
|
|
147
|
-
const restaurantData = {
|
|
148
|
-
floors: [
|
|
149
|
-
{
|
|
150
|
-
tables: [
|
|
151
|
-
{
|
|
152
|
-
objectType: 'Wall',
|
|
153
|
-
id: 'wall1',
|
|
154
|
-
tableNumber: 1
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
objectType: 'Tafel',
|
|
158
|
-
id: 'table1',
|
|
159
|
-
tableNumber: 2,
|
|
160
|
-
minCapacity: 2,
|
|
161
|
-
maxCapacity: 4,
|
|
162
|
-
priority: 1,
|
|
163
|
-
x: 10,
|
|
164
|
-
y: 20
|
|
165
|
-
}
|
|
166
|
-
]
|
|
167
|
-
}
|
|
168
|
-
]
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const tables = getAllTables(restaurantData);
|
|
172
|
-
expect(tables).toHaveLength(1);
|
|
173
|
-
expect(tables[0].tableNumber).toBe(2);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('tableHelpers - isTemporaryTableValid', () => {
|
|
178
|
-
test('should return true for non-temporary tables', () => {
|
|
179
|
-
const table = {
|
|
180
|
-
tableNumber: 1,
|
|
181
|
-
isTemporary: false
|
|
182
|
-
};
|
|
183
|
-
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(true);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('should validate temporary table within date range and correct meal type', () => {
|
|
187
|
-
const table = {
|
|
188
|
-
tableNumber: 1,
|
|
189
|
-
isTemporary: true,
|
|
190
|
-
startDate: '2025-06-01',
|
|
191
|
-
endDate: '2025-06-30',
|
|
192
|
-
application: 'lunch'
|
|
193
|
-
};
|
|
194
|
-
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(true);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test('should reject temporary table outside date range', () => {
|
|
198
|
-
const table = {
|
|
199
|
-
tableNumber: 1,
|
|
200
|
-
isTemporary: true,
|
|
201
|
-
startDate: '2025-06-01',
|
|
202
|
-
endDate: '2025-06-30',
|
|
203
|
-
application: 'lunch'
|
|
204
|
-
};
|
|
205
|
-
expect(isTemporaryTableValid(table, '2025-07-01', '12:00')).toBe(false);
|
|
206
|
-
expect(isTemporaryTableValid(table, '2025-05-31', '12:00')).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('should reject temporary table with wrong meal type', () => {
|
|
210
|
-
const table = {
|
|
211
|
-
tableNumber: 1,
|
|
212
|
-
isTemporary: true,
|
|
213
|
-
startDate: '2025-06-01',
|
|
214
|
-
endDate: '2025-06-30',
|
|
215
|
-
application: 'dinner'
|
|
216
|
-
};
|
|
217
|
-
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(false); // lunch time
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('should reject temporary table with missing date range', () => {
|
|
221
|
-
const table = {
|
|
222
|
-
tableNumber: 1,
|
|
223
|
-
isTemporary: true,
|
|
224
|
-
application: 'lunch'
|
|
225
|
-
};
|
|
226
|
-
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(false);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test('should reject temporary table with invalid time', () => {
|
|
230
|
-
const table = {
|
|
231
|
-
tableNumber: 1,
|
|
232
|
-
isTemporary: true,
|
|
233
|
-
startDate: '2025-06-01',
|
|
234
|
-
endDate: '2025-06-30',
|
|
235
|
-
application: 'lunch'
|
|
236
|
-
};
|
|
237
|
-
expect(isTemporaryTableValid(table, '2025-06-15', 'invalid')).toBe(false);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('tableHelpers - shifts constant', () => {
|
|
242
|
-
test('should have correct shift definitions', () => {
|
|
243
|
-
expect(shifts.breakfast).toEqual({ start: '07:00', end: '11:00' });
|
|
244
|
-
expect(shifts.lunch).toEqual({ start: '11:00', end: '16:00' });
|
|
245
|
-
expect(shifts.dinner).toEqual({ start: '16:00', end: '23:00' });
|
|
246
|
-
});
|
|
247
|
-
});
|
|
1
|
+
const {
|
|
2
|
+
parseTime,
|
|
3
|
+
getMealTypeByTime,
|
|
4
|
+
getAllTables,
|
|
5
|
+
isTemporaryTableValid,
|
|
6
|
+
shifts
|
|
7
|
+
} = require('../tableHelpers');
|
|
8
|
+
|
|
9
|
+
describe('tableHelpers - parseTime', () => {
|
|
10
|
+
test('should parse valid time string correctly', () => {
|
|
11
|
+
expect(parseTime('12:30')).toBe(750); // 12*60 + 30 = 750
|
|
12
|
+
expect(parseTime('00:00')).toBe(0);
|
|
13
|
+
expect(parseTime('23:59')).toBe(1439);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should return NaN for invalid time strings', () => {
|
|
17
|
+
expect(parseTime('')).toBeNaN();
|
|
18
|
+
expect(parseTime(null)).toBeNaN();
|
|
19
|
+
expect(parseTime(undefined)).toBeNaN();
|
|
20
|
+
expect(parseTime('25:00')).toBeNaN(); // Invalid hour
|
|
21
|
+
expect(parseTime('12:60')).toBeNaN(); // Invalid minute
|
|
22
|
+
expect(parseTime('12')).toBeNaN(); // Missing colon
|
|
23
|
+
expect(parseTime('ab:cd')).toBeNaN(); // Not numbers
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should handle edge cases', () => {
|
|
27
|
+
expect(parseTime('0:0')).toBe(0);
|
|
28
|
+
expect(parseTime('1:1')).toBe(61);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('tableHelpers - getMealTypeByTime', () => {
|
|
33
|
+
test('should identify breakfast times correctly', () => {
|
|
34
|
+
expect(getMealTypeByTime('07:00')).toBe('breakfast');
|
|
35
|
+
expect(getMealTypeByTime('09:30')).toBe('breakfast');
|
|
36
|
+
expect(getMealTypeByTime('10:59')).toBe('breakfast');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should identify lunch times correctly', () => {
|
|
40
|
+
expect(getMealTypeByTime('11:00')).toBe('lunch');
|
|
41
|
+
expect(getMealTypeByTime('12:30')).toBe('lunch');
|
|
42
|
+
expect(getMealTypeByTime('15:59')).toBe('lunch');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should identify dinner times correctly', () => {
|
|
46
|
+
expect(getMealTypeByTime('16:00')).toBe('dinner');
|
|
47
|
+
expect(getMealTypeByTime('18:30')).toBe('dinner');
|
|
48
|
+
expect(getMealTypeByTime('22:59')).toBe('dinner');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should return null for times outside defined shifts', () => {
|
|
52
|
+
expect(getMealTypeByTime('06:59')).toBeNull();
|
|
53
|
+
expect(getMealTypeByTime('23:00')).toBeNull();
|
|
54
|
+
expect(getMealTypeByTime('02:00')).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should return null for invalid time strings', () => {
|
|
58
|
+
expect(getMealTypeByTime('invalid')).toBeNull();
|
|
59
|
+
expect(getMealTypeByTime('')).toBeNull();
|
|
60
|
+
expect(getMealTypeByTime(null)).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('tableHelpers - getAllTables', () => {
|
|
65
|
+
test('should extract tables correctly from restaurant data', () => {
|
|
66
|
+
const restaurantData = {
|
|
67
|
+
floors: [
|
|
68
|
+
{
|
|
69
|
+
tables: [
|
|
70
|
+
{
|
|
71
|
+
objectType: 'Tafel',
|
|
72
|
+
id: 'table1',
|
|
73
|
+
tableNumber: 1,
|
|
74
|
+
minCapacity: 2,
|
|
75
|
+
maxCapacity: 4,
|
|
76
|
+
priority: 1,
|
|
77
|
+
x: 10,
|
|
78
|
+
y: 20,
|
|
79
|
+
isTemporary: false
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
objectType: 'Tafel',
|
|
83
|
+
id: 'table2',
|
|
84
|
+
tableNumber: { $numberInt: '2' },
|
|
85
|
+
minCapacity: { $numberInt: '4' },
|
|
86
|
+
maxCapacity: { $numberInt: '6' },
|
|
87
|
+
priority: { $numberInt: '2' },
|
|
88
|
+
x: { $numberInt: '30' },
|
|
89
|
+
y: { $numberInt: '40' },
|
|
90
|
+
isTemporary: false
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const tables = getAllTables(restaurantData);
|
|
98
|
+
expect(tables).toHaveLength(2);
|
|
99
|
+
expect(tables[0].tableNumber).toBe(1);
|
|
100
|
+
expect(tables[0].maxCapacity).toBe(4);
|
|
101
|
+
expect(tables[1].tableNumber).toBe(2);
|
|
102
|
+
expect(tables[1].maxCapacity).toBe(6);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should sort tables by maxCapacity, priority, and minCapacity', () => {
|
|
106
|
+
const restaurantData = {
|
|
107
|
+
floors: [
|
|
108
|
+
{
|
|
109
|
+
tables: [
|
|
110
|
+
{
|
|
111
|
+
objectType: 'Tafel',
|
|
112
|
+
id: 'table1',
|
|
113
|
+
tableNumber: 1,
|
|
114
|
+
minCapacity: 2,
|
|
115
|
+
maxCapacity: 6,
|
|
116
|
+
priority: 1,
|
|
117
|
+
x: 10,
|
|
118
|
+
y: 20
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
objectType: 'Tafel',
|
|
122
|
+
id: 'table2',
|
|
123
|
+
tableNumber: 2,
|
|
124
|
+
minCapacity: 2,
|
|
125
|
+
maxCapacity: 4,
|
|
126
|
+
priority: 1,
|
|
127
|
+
x: 30,
|
|
128
|
+
y: 40
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const tables = getAllTables(restaurantData);
|
|
136
|
+
expect(tables[0].tableNumber).toBe(2); // Smaller maxCapacity comes first
|
|
137
|
+
expect(tables[1].tableNumber).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('should handle empty or missing data gracefully', () => {
|
|
141
|
+
expect(getAllTables({})).toEqual([]);
|
|
142
|
+
expect(getAllTables({ floors: [] })).toEqual([]);
|
|
143
|
+
expect(getAllTables(null)).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should filter out non-Tafel objects', () => {
|
|
147
|
+
const restaurantData = {
|
|
148
|
+
floors: [
|
|
149
|
+
{
|
|
150
|
+
tables: [
|
|
151
|
+
{
|
|
152
|
+
objectType: 'Wall',
|
|
153
|
+
id: 'wall1',
|
|
154
|
+
tableNumber: 1
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
objectType: 'Tafel',
|
|
158
|
+
id: 'table1',
|
|
159
|
+
tableNumber: 2,
|
|
160
|
+
minCapacity: 2,
|
|
161
|
+
maxCapacity: 4,
|
|
162
|
+
priority: 1,
|
|
163
|
+
x: 10,
|
|
164
|
+
y: 20
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const tables = getAllTables(restaurantData);
|
|
172
|
+
expect(tables).toHaveLength(1);
|
|
173
|
+
expect(tables[0].tableNumber).toBe(2);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('tableHelpers - isTemporaryTableValid', () => {
|
|
178
|
+
test('should return true for non-temporary tables', () => {
|
|
179
|
+
const table = {
|
|
180
|
+
tableNumber: 1,
|
|
181
|
+
isTemporary: false
|
|
182
|
+
};
|
|
183
|
+
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('should validate temporary table within date range and correct meal type', () => {
|
|
187
|
+
const table = {
|
|
188
|
+
tableNumber: 1,
|
|
189
|
+
isTemporary: true,
|
|
190
|
+
startDate: '2025-06-01',
|
|
191
|
+
endDate: '2025-06-30',
|
|
192
|
+
application: 'lunch'
|
|
193
|
+
};
|
|
194
|
+
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('should reject temporary table outside date range', () => {
|
|
198
|
+
const table = {
|
|
199
|
+
tableNumber: 1,
|
|
200
|
+
isTemporary: true,
|
|
201
|
+
startDate: '2025-06-01',
|
|
202
|
+
endDate: '2025-06-30',
|
|
203
|
+
application: 'lunch'
|
|
204
|
+
};
|
|
205
|
+
expect(isTemporaryTableValid(table, '2025-07-01', '12:00')).toBe(false);
|
|
206
|
+
expect(isTemporaryTableValid(table, '2025-05-31', '12:00')).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('should reject temporary table with wrong meal type', () => {
|
|
210
|
+
const table = {
|
|
211
|
+
tableNumber: 1,
|
|
212
|
+
isTemporary: true,
|
|
213
|
+
startDate: '2025-06-01',
|
|
214
|
+
endDate: '2025-06-30',
|
|
215
|
+
application: 'dinner'
|
|
216
|
+
};
|
|
217
|
+
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(false); // lunch time
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should reject temporary table with missing date range', () => {
|
|
221
|
+
const table = {
|
|
222
|
+
tableNumber: 1,
|
|
223
|
+
isTemporary: true,
|
|
224
|
+
application: 'lunch'
|
|
225
|
+
};
|
|
226
|
+
expect(isTemporaryTableValid(table, '2025-06-15', '12:00')).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('should reject temporary table with invalid time', () => {
|
|
230
|
+
const table = {
|
|
231
|
+
tableNumber: 1,
|
|
232
|
+
isTemporary: true,
|
|
233
|
+
startDate: '2025-06-01',
|
|
234
|
+
endDate: '2025-06-30',
|
|
235
|
+
application: 'lunch'
|
|
236
|
+
};
|
|
237
|
+
expect(isTemporaryTableValid(table, '2025-06-15', 'invalid')).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('tableHelpers - shifts constant', () => {
|
|
242
|
+
test('should have correct shift definitions', () => {
|
|
243
|
+
expect(shifts.breakfast).toEqual({ start: '07:00', end: '11:00' });
|
|
244
|
+
expect(shifts.lunch).toEqual({ start: '11:00', end: '16:00' });
|
|
245
|
+
expect(shifts.dinner).toEqual({ start: '16:00', end: '23:00' });
|
|
246
|
+
});
|
|
247
|
+
});
|
package/assignTables.js
CHANGED
|
@@ -299,10 +299,19 @@ async function assignTablesIfPossible({
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
// 5) Get duration
|
|
303
|
-
|
|
302
|
+
// 5) Get duration from reservation or settings
|
|
303
|
+
let duurReservatie = parseInt(
|
|
304
304
|
restaurantSettings["general-settings"]?.duurReservatie?.$numberInt || restaurantSettings["general-settings"]?.duurReservatie || 120
|
|
305
305
|
);
|
|
306
|
+
// Use reservation specific duration if provided
|
|
307
|
+
if (reservation.duration) {
|
|
308
|
+
const rawDur = reservation.duration.$numberInt ?? reservation.duration;
|
|
309
|
+
const parsedDur = parseInt(rawDur, 10);
|
|
310
|
+
if (!isNaN(parsedDur) && parsedDur > 0) {
|
|
311
|
+
duurReservatie = parsedDur;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
306
315
|
const intervalReservatie = parseInt(
|
|
307
316
|
restaurantSettings["general-settings"]?.intervalReservatie?.$numberInt || restaurantSettings["general-settings"]?.intervalReservatie || 30
|
|
308
317
|
);
|
|
@@ -322,7 +331,17 @@ async function assignTablesIfPossible({
|
|
|
322
331
|
// No need to skip the current reservation as it's not yet inserted
|
|
323
332
|
|
|
324
333
|
// compute that reservation's time slots
|
|
325
|
-
|
|
334
|
+
// Use reservation specific duration if available, else default
|
|
335
|
+
let rDuration = duurReservatie;
|
|
336
|
+
if (r.duration) {
|
|
337
|
+
// Handle both direct value and MongoDB $numberInt
|
|
338
|
+
// Use ?? to correctly handle 0 values
|
|
339
|
+
const rawDur = r.duration.$numberInt ?? r.duration;
|
|
340
|
+
const parsed = parseInt(rawDur, 10);
|
|
341
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
342
|
+
rDuration = parsed;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
326
345
|
const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
|
|
327
346
|
|
|
328
347
|
if (r.tables) {
|
|
@@ -397,18 +416,19 @@ async function assignTablesIfPossible({
|
|
|
397
416
|
return false;
|
|
398
417
|
}
|
|
399
418
|
|
|
400
|
-
// 11)
|
|
419
|
+
// 11) If a linked floor is specified for the Zitplaats, ONLY use that floor (no fallback)
|
|
401
420
|
if (preferredFloorTables && preferredFloorTables.length > 0) {
|
|
402
|
-
console.log(`[Zitplaats] Trying
|
|
403
|
-
if (tryAssignTables(preferredFloorTables, '
|
|
404
|
-
return; // Success on
|
|
421
|
+
console.log(`[Zitplaats] Trying linked floor only (${preferredFloorTables.length} tables)`);
|
|
422
|
+
if (tryAssignTables(preferredFloorTables, 'LinkedFloor')) {
|
|
423
|
+
return; // Success on linked floor
|
|
424
|
+
}
|
|
425
|
+
console.log(`[Zitplaats] No availability on linked floor - not falling back to other floors`);
|
|
426
|
+
// Do NOT fall back to all floors when a floor link is specified
|
|
427
|
+
} else {
|
|
428
|
+
// 12) Try all floors only when no floor link is specified
|
|
429
|
+
if (tryAssignTables(allTables, 'AllFloors')) {
|
|
430
|
+
return; // Success
|
|
405
431
|
}
|
|
406
|
-
console.log(`[Zitplaats] No availability on preferred floor, falling back to all floors`);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// 12) Try all floors
|
|
410
|
-
if (tryAssignTables(allTables, 'AllFloors')) {
|
|
411
|
-
return; // Success
|
|
412
432
|
}
|
|
413
433
|
|
|
414
434
|
// 13) No valid table combo found
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# PR 11 - Duration
|
|
2
|
+
|
|
3
|
+
**Actions:**
|
|
4
|
+
|
|
5
|
+
## Changes Summary
|
|
6
|
+
ADDED:
|
|
7
|
+
- New function `duration(start, end)` added to calculate the time difference between two datetime strings.
|
|
8
|
+
- New import `datetime` from the `datetime` module to support date and time operations.
|
|
9
|
+
|
|
10
|
+
NO_REMOVALS
|
|
11
|
+
|
|
12
|
+
CHANGED:
|
|
13
|
+
- Modified the `getAvailableTimeblocks` function to change the calculation of `uurOpVoorhand` from `4` to `0`, affecting the time window for available timeblocks.
|
|
14
|
+
- Updated the `getAvailableTimeblocks` function to adjust the `duration` parameter handling, now using `duration` directly instead of a hardcoded value, impacting the timeblock duration logic.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
**Author:** Fakhar-Rashid
|
|
19
|
+
**Date:** 2026-01-13
|
|
20
|
+
**PR Link:** https://github.com/thibaultvandesompele2/15-happy-algorithm/pull/11
|
|
@@ -30,7 +30,7 @@ function parseDateTimeInTimeZone(dateStr, timeStr, timeZone) {
|
|
|
30
30
|
* @param {boolean} isAdmin - Optional flag to bypass time restrictions for admin users.
|
|
31
31
|
* @returns {Object} - Returns a pruned object of available time blocks or shifts, or an empty object if out of range.
|
|
32
32
|
*/
|
|
33
|
-
function getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false) {
|
|
33
|
+
function getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false, duration = null) {
|
|
34
34
|
// Get 'uurOpVoorhand' from general settings
|
|
35
35
|
let uurOpVoorhand = 0;
|
|
36
36
|
if (
|
|
@@ -79,7 +79,7 @@ function getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlot
|
|
|
79
79
|
currentTimeInTimeZone.toDateString() === targetDateInTimeZone.toDateString();
|
|
80
80
|
|
|
81
81
|
// Get available time blocks or shifts
|
|
82
|
-
const availableTimeblocks = timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots, giftcard, isAdmin);
|
|
82
|
+
const availableTimeblocks = timeblocksAvailable(data, dateStr, reservations, guests, blockedSlots, giftcard, isAdmin, duration);
|
|
83
83
|
|
|
84
84
|
// If the date is today and uurOpVoorhand is greater than zero, prune time blocks (skip for admin)
|
|
85
85
|
if (!isAdmin && isToday && uurOpVoorhand >= 0) {
|
package/isDateAvailable.js
CHANGED
|
@@ -62,14 +62,14 @@ function isDateWithinAllowedRange(data, dateStr) {
|
|
|
62
62
|
* @param {boolean} isAdmin - Optional flag to bypass time restrictions for admin users.
|
|
63
63
|
* @returns {boolean} - Returns true if the date has at least one available timeblock, false otherwise.
|
|
64
64
|
*/
|
|
65
|
-
function isDateAvailable(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false) {
|
|
65
|
+
function isDateAvailable(data, dateStr, reservations, guests, blockedSlots = [], giftcard = null, isAdmin = false, duration = null) {
|
|
66
66
|
// Check if date is within allowed range (skip for admin)
|
|
67
67
|
if (!isAdmin && !isDateWithinAllowedRange(data, dateStr)) {
|
|
68
68
|
return false;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Get available timeblocks using the existing logic
|
|
72
|
-
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, giftcard, isAdmin);
|
|
72
|
+
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, giftcard, isAdmin, duration);
|
|
73
73
|
|
|
74
74
|
// Return true only if we have at least one available timeblock
|
|
75
75
|
return Object.keys(availableTimeblocks).length > 0;
|
|
@@ -74,7 +74,7 @@ function timeHasGiftcard(data, dateStr, timeStr, giftcard) {
|
|
|
74
74
|
* @param {boolean} isAdmin - Optional flag to bypass time restrictions for admin users.
|
|
75
75
|
* @returns {boolean} - Returns true if the date passes all checks to be available.
|
|
76
76
|
*/
|
|
77
|
-
function isDateAvailableWithTableCheck(data, dateStr, reservations, guests, blockedSlots = [], selectedGiftcard = null, isAdmin = false) {
|
|
77
|
+
function isDateAvailableWithTableCheck(data, dateStr, reservations, guests, blockedSlots = [], selectedGiftcard = null, isAdmin = false, duration = null) {
|
|
78
78
|
// 0) Optionally filter by selected menu item's date range (only when normalOpeningTimes = false)
|
|
79
79
|
const currentItem = typeof window !== 'undefined' ? window.currentReservationTicketItem : null;
|
|
80
80
|
if (
|
|
@@ -114,7 +114,7 @@ function isDateAvailableWithTableCheck(data, dateStr, reservations, guests, bloc
|
|
|
114
114
|
console.log(`Table assignment is ${isTableAssignmentEnabled ? 'ENABLED' : 'DISABLED'} for date ${dateStr}`);
|
|
115
115
|
|
|
116
116
|
// 1) First, do the standard day-level checks (including simple giftcard check).
|
|
117
|
-
const basicDateAvailable = isDateAvailable(data, dateStr, reservations, guests, blockedSlots, selectedGiftcard, isAdmin);
|
|
117
|
+
const basicDateAvailable = isDateAvailable(data, dateStr, reservations, guests, blockedSlots, selectedGiftcard, isAdmin, duration);
|
|
118
118
|
if (!basicDateAvailable) {
|
|
119
119
|
console.log(`Date ${dateStr} fails basic availability check`);
|
|
120
120
|
return false;
|
|
@@ -128,7 +128,7 @@ function isDateAvailableWithTableCheck(data, dateStr, reservations, guests, bloc
|
|
|
128
128
|
|
|
129
129
|
// 2) Get all available timeblocks for this date
|
|
130
130
|
console.log(`Getting available timeblocks for ${dateStr}`);
|
|
131
|
-
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, selectedGiftcard, isAdmin);
|
|
131
|
+
const availableTimeblocks = getAvailableTimeblocks(data, dateStr, reservations, guests, blockedSlots, selectedGiftcard, isAdmin, duration);
|
|
132
132
|
console.log(`Found ${Object.keys(availableTimeblocks).length} available timeblocks before table check`);
|
|
133
133
|
|
|
134
134
|
// If no timeblocks are available at all, exit early
|
|
@@ -153,7 +153,8 @@ function isDateAvailableWithTableCheck(data, dateStr, reservations, guests, bloc
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
console.log(`Checking table availability for ${time} on ${dateStr} for ${guests} guests`);
|
|
156
|
-
|
|
156
|
+
// Pass null for selectedZitplaats, then duration
|
|
157
|
+
if (isTimeAvailableSync(data, dateStr, time, guests, reservations, null, duration)) {
|
|
157
158
|
console.log(`Found available time ${time} with table assignment!`);
|
|
158
159
|
atLeastOneAvailable = true;
|
|
159
160
|
break;
|