@happychef/algorithm 1.4.0 → 1.4.1

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.
@@ -1,82 +1,75 @@
1
1
  // counter.js
2
2
 
3
- const { getPrevDateStr } = require('../dateHelpers');
4
-
5
3
  /**
6
4
  * Parses a time string in "HH:MM" format into minutes since midnight.
7
5
  * @param {string} timeStr - The time string in "HH:MM" format.
8
6
  * @returns {number} The time in minutes since midnight.
9
7
  */
10
8
  function parseTime(timeStr) {
11
- const [hours, minutes] = timeStr.split(':').map(Number);
12
- return hours * 60 + minutes;
13
- }
14
-
15
- function getDuurReservatie(data) {
16
- let duurReservatie = 120;
17
- if (
18
- data['general-settings'] &&
19
- data['general-settings'].duurReservatie &&
20
- parseInt(data['general-settings'].duurReservatie, 10) > 0
21
- ) {
22
- duurReservatie = parseInt(data['general-settings'].duurReservatie, 10);
9
+ const [hours, minutes] = timeStr.split(':').map(Number);
10
+ return hours * 60 + minutes;
23
11
  }
24
12
 
25
- return duurReservatie;
26
- }
13
+ function getDuurReservatie(data) {
14
+ let duurReservatie = 120;
15
+ if (
16
+ data['general-settings'] &&
17
+ data['general-settings'].duurReservatie &&
18
+ parseInt(data['general-settings'].duurReservatie, 10) > 0
19
+ ) {
20
+ duurReservatie = parseInt(data['general-settings'].duurReservatie, 10);
21
+ }
27
22
 
28
- /**
29
- * Parses a reservation's duration, falling back to the default.
30
- */
31
- function getReservationDuration(reservation, defaultDuration) {
32
- if (reservation.duration) {
33
- const rawDur = reservation.duration.$numberInt ?? reservation.duration;
34
- const parsed = parseInt(rawDur, 10);
35
- if (!isNaN(parsed) && parsed > 0) {
36
- return parsed;
37
- }
23
+ return duurReservatie;
38
24
  }
39
- return defaultDuration;
40
- }
41
-
42
- /**
43
- * Calculates the total number of guests for reservations that cover a specific hour on a specific date.
44
- * Also includes guests from previous-day reservations whose duration extends past midnight.
45
- * @param {Object} data - The main data object containing general settings.
46
- * @param {Array} reservations - An array of reservation objects.
47
- * @param {string} hour - The hour to check in "HH:MM" format.
48
- * @param {string} dateStr - The date string in "YYYY-MM-DD" format.
49
- * @returns {number} The total number of guests for that hour on the specified date.
50
- */
51
- function getGuestCountAtHour(data, reservations, hour, dateStr) {
52
- const duurReservatie = getDuurReservatie(data);
53
- const targetTime = parseTime(hour);
54
- const prevDateStr = getPrevDateStr(dateStr);
55
-
56
- let totalGuests = 0;
25
+
26
+ /**
27
+ * Calculates the total number of guests for reservations that cover a specific hour on a specific date.
28
+ * @param {Object} data - The main data object containing general settings.
29
+ * @param {Array} reservations - An array of reservation objects.
30
+ * @param {string} hour - The hour to check in "HH:MM" format.
31
+ * @param {string} dateStr - The date string in "YYYY-MM-DD" format.
32
+ * @returns {number} The total number of guests for that hour on the specified date.
33
+ */
34
+ function getGuestCountAtHour(data, reservations, hour, dateStr) {
35
+ // Get 'duurReservatie' from general settings, default to 120 if not set or zero
36
+ let duurReservatie = getDuurReservatie(data)
37
+
38
+ // Convert the target hour to minutes since midnight
39
+ const targetTime = parseTime(hour);
40
+
41
+ let totalGuests = 0;
42
+
43
+ for (const reservation of reservations) {
44
+ // Only consider reservations on the specified date
45
+ if (reservation.date !== dateStr) {
46
+ continue;
47
+ }
48
+
49
+ const startTime = parseTime(reservation.time);
57
50
 
58
- for (const reservation of reservations) {
59
- const startTime = parseTime(reservation.time);
60
- const rDuration = getReservationDuration(reservation, duurReservatie);
61
- const endTime = startTime + rDuration;
51
+ // Use reservation specific duration if available, else default
52
+ let rDuration = duurReservatie;
53
+ if (reservation.duration) {
54
+ const rawDur = reservation.duration.$numberInt ?? reservation.duration;
55
+ const parsed = parseInt(rawDur, 10);
56
+ if (!isNaN(parsed) && parsed > 0) {
57
+ rDuration = parsed;
58
+ }
59
+ }
62
60
 
63
- if (reservation.date === dateStr) {
64
- // Same-day reservation
65
- if (targetTime >= startTime && targetTime < endTime) {
66
- totalGuests += parseInt(reservation.guests, 10);
67
- }
68
- } else if (reservation.date === prevDateStr && endTime > 1440) {
69
- // Previous-day reservation that crosses midnight
70
- const spilloverEnd = endTime - 1440;
71
- if (targetTime < spilloverEnd) {
72
- totalGuests += parseInt(reservation.guests, 10);
73
- }
74
- }
61
+ const endTime = startTime + rDuration;
62
+ // Check if the target time is within the reservation time range
63
+ // Start time is inclusive, end time is exclusive
64
+ if (targetTime >= startTime && targetTime < endTime) {
65
+ totalGuests += parseInt(reservation.guests, 10);
66
+ }
67
+ }
68
+
69
+ return totalGuests;
75
70
  }
76
-
77
- return totalGuests;
78
- }
79
-
80
- module.exports = {
81
- getGuestCountAtHour,
82
- };
71
+
72
+ module.exports = {
73
+ getGuestCountAtHour,
74
+ };
75
+
@@ -38,6 +38,11 @@ function getMealTypeByTime(timeStr) {
38
38
  return mealType;
39
39
  }
40
40
  }
41
+ // Times at or past dinner end are considered dinner (cross-midnight support for bowling)
42
+ const dinnerEnd = parseTime(shifts.dinner.end);
43
+ if (time >= dinnerEnd) {
44
+ return 'dinner';
45
+ }
41
46
  return null;
42
47
  }
43
48
 
@@ -104,6 +109,24 @@ function getDataByDateAndMeal(data, dateStr, mealType) {
104
109
 
105
110
  adjustMealData(mealData, data['general-settings']);
106
111
 
112
+ // Extend dinner end time past midnight for bowling venues
113
+ const hoursOpenedPastMidnight = Number(mealData.hoursOpenedPastMidnight) || 0;
114
+ if (hoursOpenedPastMidnight > 0 && mealType === 'dinner') {
115
+ const newEndMinutes = 1440 + (hoursOpenedPastMidnight * 60); // midnight + hours
116
+ const hours = String(Math.floor(newEndMinutes / 60)).padStart(2, '0');
117
+ const minutes = String(newEndMinutes % 60).padStart(2, '0');
118
+ mealData.endTime = `${hours}:${minutes}`;
119
+ } else if (mealType === 'dinner') {
120
+ // For bowling accounts, ensure dinner extends to at least 24:00 (midnight)
121
+ const isBowling = data?.account_type === 'bowling' || data?.accountType === 'bowling' || data?.['general-settings']?.account_type === 'bowling';
122
+ if (isBowling) {
123
+ const currentEndMinutes = parseTime(mealData.endTime);
124
+ if (currentEndMinutes < 1440) {
125
+ mealData.endTime = '24:00';
126
+ }
127
+ }
128
+ }
129
+
107
130
  // Add duurReservatie to endTime
108
131
  addDuurReservatieToEndTime(mealData, data);
109
132
 
@@ -8,7 +8,6 @@ const {
8
8
  getAllTables,
9
9
  isTemporaryTableValid,
10
10
  } = require('./tableHelpers');
11
- const { getNextDateStr, getPrevDateStr } = require('./dateHelpers');
12
11
 
13
12
 
14
13
  /**
@@ -84,7 +83,8 @@ function getActualTableAssignment(reservation) {
84
83
  * Converts a reservation's start time and duration into discrete time slots (in minutes from midnight).
85
84
  */
86
85
  function computeRequiredSlots(timeString, durationMinutes, intervalMinutes) {
87
- const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
86
+ // Support cross-midnight times (e.g., "24:00", "25:00") for bowling venues
87
+ const timePattern = /^([0-2]\d):([0-5]\d)$/;
88
88
  if (!timeString || !timePattern.test(timeString)) {
89
89
  console.error(`Invalid time format for computeRequiredSlots: ${timeString}. Expected 'HH:MM'.`);
90
90
  return [];
@@ -524,59 +524,6 @@ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, s
524
524
  }
525
525
  }
526
526
 
527
- // Cross-date: include previous-day reservations that spill past midnight
528
- const prevDateStr = getPrevDateStr(date);
529
- const prevDayReservations = reservations
530
- .filter(r => r.date === prevDateStr && r.time && r.guests > 0);
531
-
532
- for (const r of prevDayReservations) {
533
- const rDuration = safeParseInt(r.duration, defaultDuurReservatie);
534
- const startMinutes = r.time.split(':').map(Number);
535
- const startMin = startMinutes[0] * 60 + startMinutes[1];
536
- if (startMin + rDuration > 1440) {
537
- const actualTables = getActualTableAssignment(r);
538
- let assignedTables = actualTables.length > 0 ? actualTables :
539
- assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration);
540
- if (assignedTables.length > 0) {
541
- const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
542
- if (rSlots && rSlots.length > 0) {
543
- assignedTables.forEach(tableNumber => {
544
- if (!tableOccupiedSlots[tableNumber]) {
545
- tableOccupiedSlots[tableNumber] = new Set();
546
- }
547
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
548
- });
549
- }
550
- }
551
- }
552
- }
553
-
554
- // Cross-date: if new reservation crosses midnight, include next-day reservations offset by +1440
555
- const crossesMidnight = requiredSlots.some(s => s >= 1440);
556
- if (crossesMidnight) {
557
- const nextDateStr = getNextDateStr(date);
558
- const nextDayReservations = reservations
559
- .filter(r => r.date === nextDateStr && r.time && r.guests > 0);
560
-
561
- for (const r of nextDayReservations) {
562
- const rDuration = safeParseInt(r.duration, defaultDuurReservatie);
563
- const actualTables = getActualTableAssignment(r);
564
- let assignedTables = actualTables.length > 0 ? actualTables :
565
- assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration);
566
- if (assignedTables.length > 0) {
567
- const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
568
- if (rSlots && rSlots.length > 0) {
569
- assignedTables.forEach(tableNumber => {
570
- if (!tableOccupiedSlots[tableNumber]) {
571
- tableOccupiedSlots[tableNumber] = new Set();
572
- }
573
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot + 1440));
574
- });
575
- }
576
- }
577
- }
578
- }
579
-
580
527
  console.log(`[isTimeAvailableSync] Occupancy map built, checking availability for new reservation`);
581
528
 
582
529
  const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
@@ -747,59 +694,6 @@ function getAvailableTablesForTime(restaurantData, date, time, guests, reservati
747
694
  });
748
695
  }
749
696
  }
750
- // Cross-date: include previous-day reservations that spill past midnight
751
- const prevDateStr = getPrevDateStr(date);
752
- const prevDayReservationsGAT = reservations
753
- .filter(r => r.date === prevDateStr && r.time && r.guests > 0);
754
-
755
- for (const r of prevDayReservationsGAT) {
756
- const rDuration = safeParseInt(r.duration, duurReservatie);
757
- const startMinutes = r.time.split(':').map(Number);
758
- const startMin = startMinutes[0] * 60 + startMinutes[1];
759
- if (startMin + rDuration > 1440) {
760
- const actualTables = getActualTableAssignment(r);
761
- let assignedTables = actualTables.length > 0 ? actualTables :
762
- assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration);
763
- if (assignedTables.length > 0) {
764
- const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
765
- if (rSlots && rSlots.length > 0) {
766
- assignedTables.forEach(tableNumber => {
767
- if (!tableOccupiedSlots[tableNumber]) {
768
- tableOccupiedSlots[tableNumber] = new Set();
769
- }
770
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot));
771
- });
772
- }
773
- }
774
- }
775
- }
776
-
777
- // Cross-date: if new reservation crosses midnight, include next-day reservations offset by +1440
778
- const crossesMidnightGAT = requiredSlots.some(s => s >= 1440);
779
- if (crossesMidnightGAT) {
780
- const nextDateStr = getNextDateStr(date);
781
- const nextDayReservationsGAT = reservations
782
- .filter(r => r.date === nextDateStr && r.time && r.guests > 0);
783
-
784
- for (const r of nextDayReservationsGAT) {
785
- const rDuration = safeParseInt(r.duration, duurReservatie);
786
- const actualTables = getActualTableAssignment(r);
787
- let assignedTables = actualTables.length > 0 ? actualTables :
788
- assignTablesForGivenTime(restaurantData, r.date, r.time, r.guests, tableOccupiedSlots, r.zitplaats || null, rDuration);
789
- if (assignedTables.length > 0) {
790
- const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
791
- if (rSlots && rSlots.length > 0) {
792
- assignedTables.forEach(tableNumber => {
793
- if (!tableOccupiedSlots[tableNumber]) {
794
- tableOccupiedSlots[tableNumber] = new Set();
795
- }
796
- rSlots.forEach(slot => tableOccupiedSlots[tableNumber].add(slot + 1440));
797
- });
798
- }
799
- }
800
- }
801
- }
802
-
803
697
  console.log("[getAvailableTablesForTime] Occupancy map built");
804
698
  const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables with zitplaats filter applied
805
699
  const availableIndividualTables = [];
package/tableHelpers.js CHANGED
@@ -12,7 +12,7 @@ function parseTime(timeStr) {
12
12
  if (parts.length !== 2) return NaN;
13
13
  const hours = parseInt(parts[0], 10);
14
14
  const minutes = parseInt(parts[1], 10);
15
- if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
15
+ if (isNaN(hours) || isNaN(minutes) || hours < 0 || minutes < 0 || minutes > 59) {
16
16
  return NaN;
17
17
  }
18
18
  return hours * 60 + minutes;
@@ -43,7 +43,10 @@ function getMealTypeByTime(timeStr) {
43
43
  return mealType;
44
44
  }
45
45
  }
46
- return null; // Return null if no matching shift is found
46
+ // Times at or past dinner end are considered dinner (cross-midnight support for bowling)
47
+ const dinnerEnd = parseTime(shifts.dinner.end);
48
+ if (!isNaN(dinnerEnd) && time >= dinnerEnd) return 'dinner';
49
+ return null;
47
50
  }
48
51
 
49
52
 
@@ -1,100 +1,100 @@
1
- // Detailed filter test - simulating getAvailableTimeblocks logic
2
- const { timeblocksAvailable } = require('./processing/timeblocksAvailable');
3
- const { getMealTypeByTime, parseTime } = require('./tableHelpers');
4
-
5
- const restaurantData = {
6
- "_id": "demo",
7
- "openinghours-lunch": {
8
- "schemeSettings": {
9
- "Monday": {
10
- "enabled": true,
11
- "startTime": "14:15",
12
- "endTime": "15:00",
13
- "maxCapacityEnabled": true,
14
- "maxCapacity": "10"
15
- }
16
- }
17
- },
18
- "general-settings": {
19
- "zitplaatsen": 13,
20
- "uurOpVoorhand": 0,
21
- "dagenInToekomst": 365,
22
- "intervalReservatie": 30,
23
- "duurReservatie": 60,
24
- "ontbijtStop": "",
25
- "lunchStop": "",
26
- "dinerStop": ""
27
- },
28
- "max-arrivals-lunch": {
29
- "14:15": 10,
30
- "14:30": 10,
31
- "14:45": 10
32
- }
33
- };
34
-
35
- const testDate = "2025-12-08";
36
- const guests = 2;
37
- const reservations = [];
38
- const uurOpVoorhand = 0;
39
- const isAdmin = false;
40
-
41
- console.log('=== DETAILED FILTER TEST ===\n');
42
-
43
- // Get timeblocks
44
- const availableTimeblocks = timeblocksAvailable(restaurantData, testDate, reservations, guests, [], null, isAdmin);
45
- console.log('Initial timeblocks:', Object.keys(availableTimeblocks));
46
-
47
- // Simulate the time filtering logic
48
- const now = new Date();
49
- const [year, month, day] = testDate.split('-').map(Number);
50
- const targetDateInTimeZone = new Date(year, month - 1, day);
51
- const isToday = now.toDateString() === targetDateInTimeZone.toDateString();
52
-
53
- console.log('\nTime filter check:');
54
- console.log('Is today:', isToday);
55
- console.log('uurOpVoorhand:', uurOpVoorhand);
56
- console.log('Current time:', now.toString());
57
-
58
- if (isToday && uurOpVoorhand >= 0) {
59
- const cutoffTime = new Date(now.getTime());
60
- cutoffTime.setHours(cutoffTime.getHours() + uurOpVoorhand);
61
- console.log('Cutoff time:', cutoffTime.toString());
62
-
63
- for (const key of Object.keys(availableTimeblocks)) {
64
- const timeStr = key;
65
- const [hours, minutes] = timeStr.split(':').map(Number);
66
- const timeBlockDateTime = new Date(year, month - 1, day, hours, minutes);
67
-
68
- console.log(`\n Checking ${timeStr}:`);
69
- console.log(` timeBlockDateTime: ${timeBlockDateTime.toString()}`);
70
- console.log(` cutoffTime: ${cutoffTime.toString()}`);
71
- console.log(` timeBlockDateTime < cutoffTime: ${timeBlockDateTime < cutoffTime}`);
72
- console.log(` Would be deleted: ${timeBlockDateTime < cutoffTime ? 'YES' : 'NO'}`);
73
- }
74
- }
75
-
76
- // Check lunchStop filter
77
- console.log('\n\nLunch Stop filter check:');
78
- const settings = restaurantData['general-settings'];
79
- const lunchStop = settings.lunchStop || null;
80
- console.log('lunchStop value:', JSON.stringify(lunchStop));
81
- console.log('lunchStop truthy:', !!lunchStop);
82
-
83
- if (lunchStop) {
84
- console.log('LunchStop is set, would apply filter');
85
- for (const time in availableTimeblocks) {
86
- const mealType = getMealTypeByTime(time);
87
- console.log(` ${time}: mealType=${mealType}`);
88
-
89
- if (mealType === 'lunch') {
90
- const timeMinutes = parseTime(time);
91
- const stopMinutes = parseTime(lunchStop);
92
- console.log(` timeMinutes=${timeMinutes}, stopMinutes=${stopMinutes}`);
93
- console.log(` Would remove: ${timeMinutes >= stopMinutes ? 'YES' : 'NO'}`);
94
- }
95
- }
96
- } else {
97
- console.log('LunchStop is NOT set, no filter applied');
98
- }
99
-
100
- console.log('\n=== END TEST ===');
1
+ // Detailed filter test - simulating getAvailableTimeblocks logic
2
+ const { timeblocksAvailable } = require('./processing/timeblocksAvailable');
3
+ const { getMealTypeByTime, parseTime } = require('./tableHelpers');
4
+
5
+ const restaurantData = {
6
+ "_id": "demo",
7
+ "openinghours-lunch": {
8
+ "schemeSettings": {
9
+ "Monday": {
10
+ "enabled": true,
11
+ "startTime": "14:15",
12
+ "endTime": "15:00",
13
+ "maxCapacityEnabled": true,
14
+ "maxCapacity": "10"
15
+ }
16
+ }
17
+ },
18
+ "general-settings": {
19
+ "zitplaatsen": 13,
20
+ "uurOpVoorhand": 0,
21
+ "dagenInToekomst": 365,
22
+ "intervalReservatie": 30,
23
+ "duurReservatie": 60,
24
+ "ontbijtStop": "",
25
+ "lunchStop": "",
26
+ "dinerStop": ""
27
+ },
28
+ "max-arrivals-lunch": {
29
+ "14:15": 10,
30
+ "14:30": 10,
31
+ "14:45": 10
32
+ }
33
+ };
34
+
35
+ const testDate = "2025-12-08";
36
+ const guests = 2;
37
+ const reservations = [];
38
+ const uurOpVoorhand = 0;
39
+ const isAdmin = false;
40
+
41
+ console.log('=== DETAILED FILTER TEST ===\n');
42
+
43
+ // Get timeblocks
44
+ const availableTimeblocks = timeblocksAvailable(restaurantData, testDate, reservations, guests, [], null, isAdmin);
45
+ console.log('Initial timeblocks:', Object.keys(availableTimeblocks));
46
+
47
+ // Simulate the time filtering logic
48
+ const now = new Date();
49
+ const [year, month, day] = testDate.split('-').map(Number);
50
+ const targetDateInTimeZone = new Date(year, month - 1, day);
51
+ const isToday = now.toDateString() === targetDateInTimeZone.toDateString();
52
+
53
+ console.log('\nTime filter check:');
54
+ console.log('Is today:', isToday);
55
+ console.log('uurOpVoorhand:', uurOpVoorhand);
56
+ console.log('Current time:', now.toString());
57
+
58
+ if (isToday && uurOpVoorhand >= 0) {
59
+ const cutoffTime = new Date(now.getTime());
60
+ cutoffTime.setHours(cutoffTime.getHours() + uurOpVoorhand);
61
+ console.log('Cutoff time:', cutoffTime.toString());
62
+
63
+ for (const key of Object.keys(availableTimeblocks)) {
64
+ const timeStr = key;
65
+ const [hours, minutes] = timeStr.split(':').map(Number);
66
+ const timeBlockDateTime = new Date(year, month - 1, day, hours, minutes);
67
+
68
+ console.log(`\n Checking ${timeStr}:`);
69
+ console.log(` timeBlockDateTime: ${timeBlockDateTime.toString()}`);
70
+ console.log(` cutoffTime: ${cutoffTime.toString()}`);
71
+ console.log(` timeBlockDateTime < cutoffTime: ${timeBlockDateTime < cutoffTime}`);
72
+ console.log(` Would be deleted: ${timeBlockDateTime < cutoffTime ? 'YES' : 'NO'}`);
73
+ }
74
+ }
75
+
76
+ // Check lunchStop filter
77
+ console.log('\n\nLunch Stop filter check:');
78
+ const settings = restaurantData['general-settings'];
79
+ const lunchStop = settings.lunchStop || null;
80
+ console.log('lunchStop value:', JSON.stringify(lunchStop));
81
+ console.log('lunchStop truthy:', !!lunchStop);
82
+
83
+ if (lunchStop) {
84
+ console.log('LunchStop is set, would apply filter');
85
+ for (const time in availableTimeblocks) {
86
+ const mealType = getMealTypeByTime(time);
87
+ console.log(` ${time}: mealType=${mealType}`);
88
+
89
+ if (mealType === 'lunch') {
90
+ const timeMinutes = parseTime(time);
91
+ const stopMinutes = parseTime(lunchStop);
92
+ console.log(` timeMinutes=${timeMinutes}, stopMinutes=${stopMinutes}`);
93
+ console.log(` Would remove: ${timeMinutes >= stopMinutes ? 'YES' : 'NO'}`);
94
+ }
95
+ }
96
+ } else {
97
+ console.log('LunchStop is NOT set, no filter applied');
98
+ }
99
+
100
+ console.log('\n=== END TEST ===');