@happychef/algorithm 1.2.28 → 1.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assignTables.js CHANGED
@@ -236,9 +236,11 @@ async function assignTablesIfPossible({
236
236
  }
237
237
  }
238
238
 
239
- // 2) Get floors data directly from the restaurant document
240
- let floorsData = restaurantSettings.floors;
241
- if (!floorsData || !Array.isArray(floorsData) || floorsData.length === 0) {
239
+ // 2) Get floors data directly from the restaurant document (include manualFloors)
240
+ const regularFloors = Array.isArray(restaurantSettings.floors) ? restaurantSettings.floors : [];
241
+ const manualFloors = Array.isArray(restaurantSettings.manualFloors) ? restaurantSettings.manualFloors : [];
242
+ let floorsData = [...regularFloors, ...manualFloors];
243
+ if (floorsData.length === 0) {
242
244
  if (enforceTableAvailability) {
243
245
  throw new Error('No floors data found in restaurant document.');
244
246
  } else {
@@ -257,21 +259,31 @@ async function assignTablesIfPossible({
257
259
  }
258
260
  }
259
261
 
260
- // 3) Helper to collect tables from floors
262
+ // 3) Helper to safely parse int from MongoDB $numberInt or plain value
263
+ function safeInt(val, fallback) {
264
+ if (val == null) return fallback;
265
+ const raw = (typeof val === 'object' && val.$numberInt != null) ? val.$numberInt : val;
266
+ const parsed = parseInt(raw, 10);
267
+ return isNaN(parsed) ? fallback : parsed;
268
+ }
269
+
270
+ // 4) Helper to collect tables from floors
261
271
  function collectTablesFromFloors(floors) {
262
272
  const tables = [];
263
273
  floors.forEach(floor => {
264
274
  if (floor.tables && Array.isArray(floor.tables)) {
265
275
  floor.tables.forEach(tbl => {
266
276
  if (tbl.objectType === "Tafel") {
277
+ const tableNumber = safeInt(tbl.tableNumber, null);
278
+ if (tableNumber == null) return; // skip tables without a number
267
279
  tables.push({
268
280
  tableId: tbl.id,
269
- tableNumber: parseInt(tbl.tableNumber.$numberInt || tbl.tableNumber),
270
- minCapacity: parseInt(tbl.minCapacity.$numberInt || tbl.minCapacity),
271
- maxCapacity: parseInt(tbl.maxCapacity.$numberInt || tbl.maxCapacity),
272
- priority: parseInt(tbl.priority.$numberInt || tbl.priority),
273
- x: parseInt(tbl.x.$numberInt || tbl.x),
274
- y: parseInt(tbl.y.$numberInt || tbl.y),
281
+ tableNumber,
282
+ minCapacity: safeInt(tbl.minCapacity, 1),
283
+ maxCapacity: safeInt(tbl.maxCapacity, 99),
284
+ priority: safeInt(tbl.priority, 99),
285
+ x: safeInt(tbl.x, 0),
286
+ y: safeInt(tbl.y, 0),
275
287
  isTemporary: tbl.isTemporary === true,
276
288
  startDate: tbl.startDate || null,
277
289
  endDate: tbl.endDate || null,
@@ -0,0 +1,100 @@
1
+ // bundle_entry.js - Entry point for esbuild bundling
2
+ // Exposes all @happychef/algorithm functions on globalThis.HappyAlgorithm
3
+ // for use with flutter_js (QuickJS / JavaScriptCore)
4
+
5
+ const algorithm = require('./index.js');
6
+
7
+ // Intl.DateTimeFormat polyfill for QuickJS (which lacks Intl support)
8
+ if (typeof globalThis.Intl === 'undefined' || typeof globalThis.Intl.DateTimeFormat === 'undefined') {
9
+ // EU DST rules for Europe/Brussels
10
+ function getBrusselsOffsetHours(date) {
11
+ const year = date.getUTCFullYear();
12
+ const marchLast = new Date(Date.UTC(year, 2, 31));
13
+ const marchSunday = 31 - marchLast.getUTCDay();
14
+ const dstStart = Date.UTC(year, 2, marchSunday, 1, 0, 0);
15
+ const octLast = new Date(Date.UTC(year, 9, 31));
16
+ const octSunday = 31 - octLast.getUTCDay();
17
+ const dstEnd = Date.UTC(year, 9, octSunday, 1, 0, 0);
18
+ const ts = date.getTime();
19
+ return (ts >= dstStart && ts < dstEnd) ? 2 : 1;
20
+ }
21
+
22
+ if (!globalThis.Intl) globalThis.Intl = {};
23
+
24
+ globalThis.Intl.DateTimeFormat = function IntlDateTimeFormat(locale, options) {
25
+ this._options = options || {};
26
+ this.formatToParts = function(date) {
27
+ const d = date || new Date();
28
+ const tz = (this._options.timeZone || '').replace(/\//g, '/');
29
+ // Only support Europe/Brussels
30
+ const offset = getBrusselsOffsetHours(d);
31
+ const brusselsDate = new Date(d.getTime() + offset * 60 * 60 * 1000);
32
+ const parts = [];
33
+ if (this._options.hour) {
34
+ parts.push({ type: 'hour', value: String(brusselsDate.getUTCHours()).padStart(2, '0') });
35
+ }
36
+ if (this._options.minute) {
37
+ parts.push({ type: 'literal', value: ':' });
38
+ parts.push({ type: 'minute', value: String(brusselsDate.getUTCMinutes()).padStart(2, '0') });
39
+ }
40
+ return parts;
41
+ };
42
+ this.format = function(date) {
43
+ return this.formatToParts(date).map(function(p) { return p.value; }).join('');
44
+ };
45
+ };
46
+ }
47
+
48
+ // Expose all algorithm functions globally
49
+ globalThis.HappyAlgorithm = {
50
+ // Core availability functions
51
+ getAvailableTimeblocks: algorithm.getAvailableTimeblocks,
52
+ isDateAvailable: algorithm.isDateAvailable,
53
+ isDateAvailableWithTableCheck: algorithm.isDateAvailableWithTableCheck,
54
+ isTimeAvailable: algorithm.isTimeAvailable,
55
+
56
+ // Table assignment functions
57
+ isTimeAvailableSync: algorithm.isTimeAvailableSync,
58
+ filterTimeblocksByTableAvailability: algorithm.filterTimeblocksByTableAvailability,
59
+ getAvailableTimeblocksWithTableCheck: algorithm.getAvailableTimeblocksWithTableCheck,
60
+ getAvailableTablesForTime: algorithm.getAvailableTablesForTime,
61
+
62
+ // Processing functions
63
+ timeblocksAvailable: algorithm.timeblocksAvailable,
64
+ getDailyGuestCounts: algorithm.getDailyGuestCounts,
65
+
66
+ // Filter functions
67
+ filterTimeblocksByStopTimes: algorithm.filterTimeblocksByStopTimes,
68
+ filterTimeblocksByMaxArrivals: algorithm.filterTimeblocksByMaxArrivals,
69
+ filterTimeblocksByMaxGroups: algorithm.filterTimeblocksByMaxGroups,
70
+
71
+ // Table helpers
72
+ parseTime: algorithm.parseTime,
73
+ getMealTypeByTime: algorithm.getMealTypeByTime,
74
+ getAllTables: algorithm.getAllTables,
75
+ getFloorIdForSeatPlace: algorithm.getFloorIdForSeatPlace,
76
+ isTemporaryTableValid: algorithm.isTemporaryTableValid,
77
+
78
+ // Grouping
79
+ tryGroupTables: algorithm.tryGroupTables,
80
+
81
+ // Batch functions for calendar performance (avoid 30 separate JSON serializations)
82
+ batchIsDateAvailable: function(data, dates, reservations, guests, blockedSlots, giftcard, isAdmin, duration) {
83
+ var result = {};
84
+ for (var i = 0; i < dates.length; i++) {
85
+ result[dates[i]] = algorithm.isDateAvailable(data, dates[i], reservations, guests, blockedSlots, giftcard, isAdmin, duration);
86
+ }
87
+ return result;
88
+ },
89
+
90
+ batchIsDateAvailableWithTableCheck: function(data, dates, reservations, guests, blockedSlots, selectedGiftcard, isAdmin, duration, selectedZitplaats) {
91
+ var result = {};
92
+ for (var i = 0; i < dates.length; i++) {
93
+ result[dates[i]] = algorithm.isDateAvailableWithTableCheck(data, dates[i], reservations, guests, blockedSlots, selectedGiftcard, isAdmin, duration, selectedZitplaats);
94
+ }
95
+ return result;
96
+ },
97
+
98
+ // Version info
99
+ VERSION: '1.2.29'
100
+ };
@@ -0,0 +1,179 @@
1
+ // Minimal moment-timezone shim for Europe/Brussels only
2
+ // Replaces the full ~500KB moment-timezone library
3
+ // Implements only the subset used by @happychef/algorithm
4
+
5
+ const TIMEZONE = 'Europe/Brussels';
6
+
7
+ // EU DST rules: CET (UTC+1) / CEST (UTC+2)
8
+ // CEST starts last Sunday of March at 02:00 UTC
9
+ // CET starts last Sunday of October at 03:00 UTC
10
+ function getBrusselsOffset(date) {
11
+ const year = date.getUTCFullYear();
12
+ const month = date.getUTCMonth(); // 0-indexed
13
+
14
+ // Find last Sunday of March
15
+ const marchLast = new Date(Date.UTC(year, 2, 31));
16
+ const marchSunday = 31 - marchLast.getUTCDay();
17
+ const dstStart = Date.UTC(year, 2, marchSunday, 1, 0, 0); // 02:00 CET = 01:00 UTC
18
+
19
+ // Find last Sunday of October
20
+ const octLast = new Date(Date.UTC(year, 9, 31));
21
+ const octSunday = 31 - octLast.getUTCDay();
22
+ const dstEnd = Date.UTC(year, 9, octSunday, 1, 0, 0); // 03:00 CEST = 01:00 UTC
23
+
24
+ const ts = date.getTime();
25
+ if (ts >= dstStart && ts < dstEnd) {
26
+ return 2; // CEST (UTC+2)
27
+ }
28
+ return 1; // CET (UTC+1)
29
+ }
30
+
31
+ function toBrusselsDate(date) {
32
+ const offset = getBrusselsOffset(date);
33
+ return new Date(date.getTime() + offset * 60 * 60 * 1000);
34
+ }
35
+
36
+ function createMomentObject(date, offset) {
37
+ const brusselsDate = new Date(date.getTime() + offset * 60 * 60 * 1000);
38
+
39
+ const obj = {
40
+ _date: date,
41
+ _brusselsDate: brusselsDate,
42
+ _offset: offset,
43
+ _valid: true,
44
+
45
+ isValid() {
46
+ return this._valid;
47
+ },
48
+
49
+ day() {
50
+ return this._brusselsDate.getUTCDay();
51
+ },
52
+
53
+ hours() {
54
+ return this._brusselsDate.getUTCHours();
55
+ },
56
+
57
+ minutes() {
58
+ return this._brusselsDate.getUTCMinutes();
59
+ },
60
+
61
+ format(fmt) {
62
+ if (fmt === 'YYYY-MM-DD') {
63
+ const y = this._brusselsDate.getUTCFullYear();
64
+ const m = String(this._brusselsDate.getUTCMonth() + 1).padStart(2, '0');
65
+ const d = String(this._brusselsDate.getUTCDate()).padStart(2, '0');
66
+ return `${y}-${m}-${d}`;
67
+ }
68
+ if (fmt === 'HH:mm') {
69
+ const h = String(this._brusselsDate.getUTCHours()).padStart(2, '0');
70
+ const min = String(this._brusselsDate.getUTCMinutes()).padStart(2, '0');
71
+ return `${h}:${min}`;
72
+ }
73
+ return this._brusselsDate.toISOString();
74
+ },
75
+
76
+ startOf(unit) {
77
+ if (unit === 'day') {
78
+ const d = new Date(Date.UTC(
79
+ this._brusselsDate.getUTCFullYear(),
80
+ this._brusselsDate.getUTCMonth(),
81
+ this._brusselsDate.getUTCDate(),
82
+ 0, 0, 0, 0
83
+ ));
84
+ // Adjust back from Brussels to UTC
85
+ const utcDate = new Date(d.getTime() - this._offset * 60 * 60 * 1000);
86
+ return createMomentObject(utcDate, this._offset);
87
+ }
88
+ return this;
89
+ },
90
+
91
+ endOf(unit) {
92
+ if (unit === 'day') {
93
+ const d = new Date(Date.UTC(
94
+ this._brusselsDate.getUTCFullYear(),
95
+ this._brusselsDate.getUTCMonth(),
96
+ this._brusselsDate.getUTCDate(),
97
+ 23, 59, 59, 999
98
+ ));
99
+ const utcDate = new Date(d.getTime() - this._offset * 60 * 60 * 1000);
100
+ return createMomentObject(utcDate, this._offset);
101
+ }
102
+ return this;
103
+ },
104
+
105
+ isBefore(other) {
106
+ return this._date.getTime() < other._date.getTime();
107
+ },
108
+
109
+ isAfter(other) {
110
+ return this._date.getTime() > other._date.getTime();
111
+ },
112
+
113
+ tz() {
114
+ // Already in Brussels timezone, return self
115
+ return this;
116
+ },
117
+ };
118
+
119
+ return obj;
120
+ }
121
+
122
+ function parseDateStr(dateStr, format) {
123
+ if (format === 'YYYY-MM-DD') {
124
+ const parts = dateStr.split('-');
125
+ if (parts.length !== 3) return null;
126
+ const year = parseInt(parts[0], 10);
127
+ const month = parseInt(parts[1], 10) - 1;
128
+ const day = parseInt(parts[2], 10);
129
+ if (isNaN(year) || isNaN(month) || isNaN(day)) return null;
130
+ if (month < 0 || month > 11 || day < 1 || day > 31) return null;
131
+ // Create date at midnight Brussels time -> convert to UTC
132
+ const utcDate = new Date(Date.UTC(year, month, day, 0, 0, 0));
133
+ return utcDate;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ // Main moment function
139
+ function moment(dateStr, format, timezone) {
140
+ if (dateStr === undefined || dateStr === null) {
141
+ // moment() - current time
142
+ const now = new Date();
143
+ const offset = getBrusselsOffset(now);
144
+ const obj = createMomentObject(now, offset);
145
+ obj.tz = function() { return obj; };
146
+ return obj;
147
+ }
148
+
149
+ if (typeof dateStr === 'string') {
150
+ const date = parseDateStr(dateStr, format || 'YYYY-MM-DD');
151
+ if (!date) {
152
+ return { _valid: false, isValid() { return false; }, day() { return 0; }, tz() { return this; } };
153
+ }
154
+ const offset = getBrusselsOffset(date);
155
+ return createMomentObject(date, offset);
156
+ }
157
+
158
+ // Fallback
159
+ const now = new Date();
160
+ const offset = getBrusselsOffset(now);
161
+ return createMomentObject(now, offset);
162
+ }
163
+
164
+ // moment.tz(dateStr, format, timezone) or moment().tz(timezone)
165
+ moment.tz = function(dateStr, format, timezone) {
166
+ if (typeof dateStr === 'string' && typeof format === 'string' && typeof timezone === 'string') {
167
+ // moment.tz(dateStr, format, timezone)
168
+ return moment(dateStr, format, timezone);
169
+ }
170
+ if (typeof dateStr === 'string' && typeof format === 'string' && timezone === undefined) {
171
+ // moment.tz(dateStr, timezone) - dateStr is actual date, format is timezone
172
+ // This pattern: moment().tz('Europe/Brussels')
173
+ return moment(dateStr, 'YYYY-MM-DD');
174
+ }
175
+ // Fallback: current time in Brussels
176
+ return moment();
177
+ };
178
+
179
+ module.exports = moment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happychef/algorithm",
3
- "version": "1.2.28",
3
+ "version": "1.2.30",
4
4
  "description": "Restaurant and reservation algorithm utilities",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tableHelpers.js CHANGED
@@ -72,63 +72,71 @@ function getFloorIdForSeatPlace(restaurantData, seatPlace) {
72
72
  * @param {string|null} selectedZitplaats - Optional seat place to filter by linked floor.
73
73
  * @returns {Array} An array of processed table objects.
74
74
  */
75
+ /**
76
+ * Safely parse int from MongoDB $numberInt or plain value, with fallback.
77
+ */
78
+ function safeInt(val, fallback) {
79
+ if (val == null) return fallback;
80
+ if (typeof val === 'object' && val !== null && val.$numberInt != null) {
81
+ const parsed = parseInt(val.$numberInt, 10);
82
+ return isNaN(parsed) ? fallback : parsed;
83
+ }
84
+ if (typeof val === 'number' && !isNaN(val)) return val;
85
+ const parsed = parseInt(val, 10);
86
+ return isNaN(parsed) ? fallback : parsed;
87
+ }
88
+
75
89
  function getAllTables(restaurantData, selectedZitplaats) {
76
90
  let allTables = [];
77
- if (restaurantData?.floors && Array.isArray(restaurantData.floors)) {
78
- // If a zitplaats is specified and has a floor link, only use that floor
79
- let floorsToUse = restaurantData.floors;
80
- if (selectedZitplaats) {
81
- const linkedFloorId = getFloorIdForSeatPlace(restaurantData, selectedZitplaats);
82
- if (linkedFloorId) {
83
- const linkedFloor = restaurantData.floors.find(f => f.id === linkedFloorId);
84
- if (linkedFloor) {
85
- floorsToUse = [linkedFloor];
86
- console.log(`[getAllTables] Floor link found: zitplaats '${selectedZitplaats}' -> floor '${linkedFloorId}'`);
87
- }
91
+ // Combine regular floors and manualFloors
92
+ const regularFloors = Array.isArray(restaurantData?.floors) ? restaurantData.floors : [];
93
+ const manualFloors = Array.isArray(restaurantData?.manualFloors) ? restaurantData.manualFloors : [];
94
+ let allFloors = [...regularFloors, ...manualFloors];
95
+
96
+ if (allFloors.length === 0) {
97
+ console.warn("Restaurant data is missing 'floors' array.");
98
+ return allTables;
99
+ }
100
+
101
+ // If a zitplaats is specified and has a floor link, only use that floor
102
+ let floorsToUse = allFloors;
103
+ if (selectedZitplaats) {
104
+ const linkedFloorId = getFloorIdForSeatPlace(restaurantData, selectedZitplaats);
105
+ if (linkedFloorId) {
106
+ const linkedFloor = allFloors.find(f => f.id === linkedFloorId);
107
+ if (linkedFloor) {
108
+ floorsToUse = [linkedFloor];
109
+ console.log(`[getAllTables] Floor link found: zitplaats '${selectedZitplaats}' -> floor '${linkedFloorId}'`);
88
110
  }
89
111
  }
112
+ }
90
113
 
91
- floorsToUse.forEach(floor => {
92
- if (floor?.tables && Array.isArray(floor.tables)) {
93
- floor.tables.forEach(tbl => {
94
- // Ensure table number, capacities, priority exist before parsing
95
- const tableNumberRaw = tbl.tableNumber?.$numberInt ?? tbl.tableNumber;
96
- const minCapacityRaw = tbl.minCapacity?.$numberInt ?? tbl.minCapacity;
97
- const maxCapacityRaw = tbl.maxCapacity?.$numberInt ?? tbl.maxCapacity;
98
- const priorityRaw = tbl.priority?.$numberInt ?? tbl.priority;
99
- const xRaw = tbl.x?.$numberInt ?? tbl.x;
100
- const yRaw = tbl.y?.$numberInt ?? tbl.y;
101
-
102
- if (tbl.objectType === "Tafel" &&
103
- tableNumberRaw !== undefined &&
104
- minCapacityRaw !== undefined &&
105
- maxCapacityRaw !== undefined &&
106
- priorityRaw !== undefined &&
107
- xRaw !== undefined &&
108
- yRaw !== undefined)
109
- {
110
- allTables.push({
111
- tableId: tbl.id,
112
- tableNumber: parseInt(tableNumberRaw, 10),
113
- minCapacity: parseInt(minCapacityRaw, 10),
114
- maxCapacity: parseInt(maxCapacityRaw, 10),
115
- priority: parseInt(priorityRaw, 10),
116
- x: parseInt(xRaw, 10),
117
- y: parseInt(yRaw, 10),
118
- isTemporary: tbl.isTemporary === true, // Ensure boolean
119
- startDate: tbl.startDate || null, // Expects 'YYYY-MM-DD'
120
- endDate: tbl.endDate || null, // Expects 'YYYY-MM-DD'
121
- application: tbl.application || null // Expects 'breakfast', 'lunch', or 'dinner'
122
- });
123
- } else if (tbl.objectType === "Tafel") {
124
- console.warn(`Skipping table due to missing essential properties: ${JSON.stringify(tbl)}`);
114
+ floorsToUse.forEach(floor => {
115
+ if (floor?.tables && Array.isArray(floor.tables)) {
116
+ floor.tables.forEach(tbl => {
117
+ if (tbl.objectType === "Tafel") {
118
+ const tableNumber = safeInt(tbl.tableNumber, NaN);
119
+ if (isNaN(tableNumber)) {
120
+ console.warn(`Skipping table without valid tableNumber: ${JSON.stringify(tbl)}`);
121
+ return;
125
122
  }
126
- });
127
- }
128
- });
129
- } else {
130
- console.warn("Restaurant data is missing 'floors' array.");
131
- }
123
+ allTables.push({
124
+ tableId: tbl.id,
125
+ tableNumber,
126
+ minCapacity: safeInt(tbl.minCapacity, 1),
127
+ maxCapacity: safeInt(tbl.maxCapacity, 99),
128
+ priority: safeInt(tbl.priority, 99),
129
+ x: safeInt(tbl.x, 0),
130
+ y: safeInt(tbl.y, 0),
131
+ isTemporary: tbl.isTemporary === true,
132
+ startDate: tbl.startDate || null,
133
+ endDate: tbl.endDate || null,
134
+ application: tbl.application || null
135
+ });
136
+ }
137
+ });
138
+ }
139
+ });
132
140
 
133
141
  // Sort tables
134
142
  allTables.sort((a, b) => {
@@ -136,22 +144,11 @@ function getAllTables(restaurantData, selectedZitplaats) {
136
144
  return a.maxCapacity - b.maxCapacity;
137
145
  }
138
146
  if (a.priority !== b.priority) {
139
- // Assuming lower priority number means higher priority
140
147
  return a.priority - b.priority;
141
148
  }
142
149
  return a.minCapacity - b.minCapacity;
143
150
  });
144
151
 
145
- // Filter out tables where parsing failed (resulted in NaN)
146
- allTables = allTables.filter(t =>
147
- !isNaN(t.tableNumber) &&
148
- !isNaN(t.minCapacity) &&
149
- !isNaN(t.maxCapacity) &&
150
- !isNaN(t.priority) &&
151
- !isNaN(t.x) &&
152
- !isNaN(t.y)
153
- );
154
-
155
152
  return allTables;
156
153
  }
157
154