@pisell/pisellos 2.3.72 → 2.3.74

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 (74) hide show
  1. package/dist/core/index.d.ts +7 -0
  2. package/dist/core/index.js +109 -65
  3. package/dist/modules/OpenData/types.d.ts +1 -0
  4. package/dist/modules/Order/index.d.ts +1 -1
  5. package/dist/modules/Order/utils.js +6 -1
  6. package/dist/modules/Quotation/index.d.ts +6 -0
  7. package/dist/modules/Quotation/index.js +75 -19
  8. package/dist/modules/ResourcePlanner/index.d.ts +0 -22
  9. package/dist/modules/ResourcePlanner/index.js +1 -180
  10. package/dist/modules/ResourcePlanner/localClock.d.ts +33 -0
  11. package/dist/modules/ResourcePlanner/localClock.js +183 -0
  12. package/dist/modules/ResourcePlanner/planner.d.ts +5 -14
  13. package/dist/modules/ResourcePlanner/planner.js +2019 -936
  14. package/dist/modules/ResourcePlanner/types.d.ts +346 -187
  15. package/dist/modules/ResourcePlanner/types.js +33 -1
  16. package/dist/plugins/request.d.ts +1 -0
  17. package/dist/server/index.d.ts +8 -2
  18. package/dist/server/index.js +206 -140
  19. package/dist/server/modules/floor-plan/index.d.ts +4 -0
  20. package/dist/server/modules/floor-plan/index.js +240 -98
  21. package/dist/server/modules/menu/index.js +48 -23
  22. package/dist/server/modules/order/index.d.ts +17 -7
  23. package/dist/server/modules/order/index.js +419 -207
  24. package/dist/server/modules/products/index.d.ts +8 -2
  25. package/dist/server/modules/products/index.js +167 -75
  26. package/dist/server/modules/resource/index.js +21 -13
  27. package/dist/solution/BookingByStep/index.d.ts +1 -1
  28. package/dist/solution/BookingTicket/index.js +4 -2
  29. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +2 -1
  30. package/dist/solution/BookingTicket/utils/addProductDecision.js +3 -2
  31. package/dist/solution/UnifiedBookingSales/index.d.ts +110 -73
  32. package/dist/solution/UnifiedBookingSales/index.js +3349 -971
  33. package/dist/solution/UnifiedBookingSales/remoteOccupancyCache.d.ts +49 -0
  34. package/dist/solution/UnifiedBookingSales/remoteOccupancyCache.js +430 -0
  35. package/dist/solution/UnifiedBookingSales/types.d.ts +133 -76
  36. package/dist/solution/UnifiedBookingSales/types.js +4 -9
  37. package/lib/core/index.d.ts +7 -0
  38. package/lib/core/index.js +20 -2
  39. package/lib/model/strategy/adapter/promotion/index.js +0 -51
  40. package/lib/modules/OpenData/types.d.ts +1 -0
  41. package/lib/modules/Order/index.d.ts +1 -1
  42. package/lib/modules/Order/utils.js +5 -1
  43. package/lib/modules/Quotation/index.d.ts +6 -0
  44. package/lib/modules/Quotation/index.js +49 -5
  45. package/lib/modules/ResourcePlanner/index.d.ts +0 -22
  46. package/lib/modules/ResourcePlanner/index.js +0 -123
  47. package/lib/modules/ResourcePlanner/localClock.d.ts +33 -0
  48. package/lib/modules/ResourcePlanner/localClock.js +270 -0
  49. package/lib/modules/ResourcePlanner/planner.d.ts +5 -14
  50. package/lib/modules/ResourcePlanner/planner.js +1903 -798
  51. package/lib/modules/ResourcePlanner/types.d.ts +346 -187
  52. package/lib/modules/ResourcePlanner/types.js +19 -0
  53. package/lib/plugins/request.d.ts +1 -0
  54. package/lib/server/index.d.ts +8 -2
  55. package/lib/server/index.js +52 -17
  56. package/lib/server/modules/floor-plan/index.d.ts +4 -0
  57. package/lib/server/modules/floor-plan/index.js +54 -6
  58. package/lib/server/modules/menu/index.js +31 -5
  59. package/lib/server/modules/order/index.d.ts +17 -7
  60. package/lib/server/modules/order/index.js +197 -40
  61. package/lib/server/modules/products/index.d.ts +8 -2
  62. package/lib/server/modules/products/index.js +97 -24
  63. package/lib/server/modules/resource/index.js +7 -2
  64. package/lib/solution/BookingByStep/index.d.ts +1 -1
  65. package/lib/solution/BookingTicket/index.js +2 -1
  66. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +2 -1
  67. package/lib/solution/BookingTicket/utils/addProductDecision.js +2 -0
  68. package/lib/solution/UnifiedBookingSales/index.d.ts +110 -73
  69. package/lib/solution/UnifiedBookingSales/index.js +2289 -374
  70. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.d.ts +49 -0
  71. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.js +376 -0
  72. package/lib/solution/UnifiedBookingSales/types.d.ts +133 -76
  73. package/lib/solution/UnifiedBookingSales/types.js +3 -2
  74. package/package.json +1 -1
@@ -1,1069 +1,2152 @@
1
- function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
2
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
3
- function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
- function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
5
- function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
1
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
6
2
  function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
7
3
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
8
5
  function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
9
6
  function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
10
- function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
11
- function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
12
7
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
13
- function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
- import dayjs from 'dayjs';
15
- export var DEFAULT_RESOURCE_PLANNER_CONTEXT = {
16
- products: [],
17
- resources: [],
18
- externalEvents: [],
19
- slotDurationMinutes: 30,
20
- businessStartTime: '09:00',
21
- businessEndTime: '18:00'
22
- };
23
- export var DEFAULT_RESOURCE_PLANNER_SELECTION = {
24
- productIds: [],
25
- resourceIds: [],
26
- assignments: []
8
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
9
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
10
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
11
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
12
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
13
+ import { AvailabilityError } from "./types";
14
+ import { addLocalCalendarDays, addLocalHours, addLocalMinutes, captureRuntimeOffsetMinutes, ceilLocalScalarToTenMinutes, dateOrdinalFromLocalDate, extractFixedOffsetMinutes, formatLocalDate, formatLocalDateTime, formatLocalTime, localDateTimeToScalar, localDateToScalar, localDateToScalarStart, nextDate as getNextLocalDate, parseLocalDate, parseLocalDateTime, weekdayFromLocalDate } from "./localClock";
15
+ var DEFAULT_SLOT_STEP_MINUTES = 30;
16
+ var DEFAULT_BUSINESS_HOURS = {
17
+ startTime: '09:00',
18
+ endTime: '18:00'
27
19
  };
28
- export function normalizePlannerContext() {
29
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
30
- return {
31
- products: Array.isArray(input.products) ? input.products : [],
32
- resources: Array.isArray(input.resources) ? input.resources : [],
33
- externalEvents: Array.isArray(input.externalEvents) ? input.externalEvents : [],
34
- slotDurationMinutes: input.slotDurationMinutes || DEFAULT_RESOURCE_PLANNER_CONTEXT.slotDurationMinutes,
35
- businessStartTime: input.businessStartTime || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessStartTime,
36
- businessEndTime: input.businessEndTime || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessEndTime
37
- };
20
+ function availabilityError(code, message, details) {
21
+ return new AvailabilityError(code, message, details);
38
22
  }
39
23
  function sameId(left, right) {
24
+ if (left === undefined || left === null || right === undefined || right === null) return false;
40
25
  return String(left) === String(right);
41
26
  }
42
- function dateOf(value) {
43
- return dayjs(value).format('YYYY-MM-DD');
27
+ function uniqById() {
28
+ var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
29
+ var map = new Map();
30
+ values.forEach(function (value) {
31
+ map.set(String(value), value);
32
+ });
33
+ return Array.from(map.values());
34
+ }
35
+ function normalizePartySize(partySize) {
36
+ if (partySize === undefined || partySize === null) return undefined;
37
+ var normalized = Number(partySize);
38
+ if (!Number.isInteger(normalized) || normalized <= 0) {
39
+ throw availabilityError('INVALID_TARGET_FILTER', 'partySize 必须是正整数', {
40
+ partySize: partySize
41
+ });
42
+ }
43
+ return normalized;
44
+ }
45
+ function normalizeBookingCount(bookingCount) {
46
+ if (bookingCount === undefined || bookingCount === null) return 1;
47
+ var normalized = Number(bookingCount);
48
+ if (!Number.isInteger(normalized) || normalized <= 0) {
49
+ throw availabilityError('INVALID_TARGET_FILTER', 'bookingCount 必须是正整数', {
50
+ bookingCount: bookingCount
51
+ });
52
+ }
53
+ return normalized;
54
+ }
55
+ function toDateTime(value, _timezoneName) {
56
+ try {
57
+ return localDateTimeToScalar(parseLocalDateTime(value));
58
+ } catch (_error) {
59
+ return Number.NaN;
60
+ }
61
+ }
62
+ function toDateStart(value, _timezoneName) {
63
+ try {
64
+ return localDateToScalarStart(value);
65
+ } catch (_error) {
66
+ return Number.NaN;
67
+ }
68
+ }
69
+ function toDateEnd(value, _timezoneName) {
70
+ var start = toDateStart(value);
71
+ return Number.isFinite(start) ? addLocalCalendarDays(start, 1) - 1 : Number.NaN;
72
+ }
73
+ function toDateTimeInDate(date, time, _timezoneName) {
74
+ return toDateTime("".concat(date, "T").concat(time.length === 5 ? "".concat(time, ":00") : time));
44
75
  }
45
- function timeOf(value) {
46
- return dayjs(value).format('HH:mm');
76
+ function addMinutesZoned(value, minutes, _timezoneName) {
77
+ return addLocalMinutes(value, minutes);
47
78
  }
48
- function toDateTime(date, time) {
49
- if (/^\d{4}-\d{2}-\d{2}/.test(time)) return dayjs(time);
50
- return dayjs("".concat(date, " ").concat(time.length === 5 ? "".concat(time, ":00") : time));
79
+ function ceilToTenMinutes(value, _timezoneName) {
80
+ return ceilLocalScalarToTenMinutes(value);
51
81
  }
52
- function overlaps(startA, endA, startB, endB) {
53
- return startA.isBefore(endB) && endA.isAfter(startB);
82
+ function nextDate(value, _timezoneName) {
83
+ return getNextLocalDate(value);
54
84
  }
55
- function containsRange(outerStart, outerEnd, innerStart, innerEnd) {
56
- return !innerStart.isBefore(outerStart) && !innerEnd.isAfter(outerEnd);
85
+ function formatDateTime(value, _timezoneName, fixedOffsetMinutes) {
86
+ return formatLocalDateTime(value, fixedOffsetMinutes !== null && fixedOffsetMinutes !== void 0 ? fixedOffsetMinutes : captureRuntimeOffsetMinutes());
57
87
  }
58
- function maxDayjs(left, right) {
59
- return left.isAfter(right) ? left : right;
88
+ function formatDate(value, _timezoneName) {
89
+ return formatLocalDate(value);
60
90
  }
61
- function minDayjs(left, right) {
62
- return left.isBefore(right) ? left : right;
91
+ function formatTime(value, _timezoneName) {
92
+ return formatLocalTime(value, false);
63
93
  }
64
- function addDays(start, end) {
65
- var result = [];
66
- var cursor = dayjs(start);
67
- var finalDay = dayjs(end);
68
- while (!cursor.isAfter(finalDay, 'day')) {
69
- result.push(cursor.format('YYYY-MM-DD'));
70
- cursor = cursor.add(1, 'day');
94
+ function normalizeAbsoluteRange(range, _timezoneName) {
95
+ var start = toDateTime(range.startAt);
96
+ var end = toDateTime(range.endAt);
97
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;
98
+ return {
99
+ start: start,
100
+ end: end
101
+ };
102
+ }
103
+ function getCandidateBookingRange(candidate) {
104
+ return {
105
+ // Runtime fallback keeps older caller-created candidates fail-safe during migration.
106
+ startAt: candidate.bookingStartAt || candidate.startAt,
107
+ endAt: candidate.bookingEndAt || candidate.endAt
108
+ };
109
+ }
110
+ function getCandidateRangeCacheKey(candidate) {
111
+ var bookingRange = getCandidateBookingRange(candidate);
112
+ return "".concat(candidate.key, ":").concat(bookingRange.startAt, ":").concat(bookingRange.endAt);
113
+ }
114
+ function getProductTimeRangeIdentity(range) {
115
+ var bookingRange = getCandidateBookingRange(range);
116
+ return [String(range.productId), range.startAt, range.endAt, bookingRange.startAt, bookingRange.endAt].join(':');
117
+ }
118
+ function toIndexedRange(range) {
119
+ return _objectSpread(_objectSpread({}, range), {}, {
120
+ startMs: range.start,
121
+ endMs: range.end
122
+ });
123
+ }
124
+ function normalizeNonNegativeNumber(value) {
125
+ var normalized = Number(value);
126
+ if (!Number.isFinite(normalized) || normalized < 0) return null;
127
+ return normalized;
128
+ }
129
+ function resolveCutOffPolicyIssue(policy) {
130
+ if (policy.type === 'invalid') {
131
+ return policy.reason || 'invalid_cutoff_policy';
71
132
  }
72
- return result;
73
- }
74
- function getQueryProducts(products, query) {
75
- var _query$productIds;
76
- if (!((_query$productIds = query.productIds) !== null && _query$productIds !== void 0 && _query$productIds.length)) return products;
77
- return products.filter(function (product) {
78
- return query.productIds.some(function (id) {
79
- return sameId(id, product.id);
80
- });
133
+ if ('futureDay' in policy && policy.futureDay !== undefined && normalizeNonNegativeNumber(policy.futureDay) === null) {
134
+ return 'invalid_future_day';
135
+ }
136
+ if (policy.type === 'ongoing' && policy.mode === 'after_start' && normalizeNonNegativeNumber(policy.graceMinutes) === null) {
137
+ return 'invalid_grace_minutes';
138
+ }
139
+ if (policy.type === 'advance') {
140
+ if (normalizeNonNegativeNumber(policy.unit) === null) return 'invalid_advance_unit';
141
+ if (!['days', 'hours', 'minutes'].includes(policy.unitType)) return 'invalid_advance_unit_type';
142
+ }
143
+ return null;
144
+ }
145
+ function addLeadWindow(base, unit, unitType, _timezoneName) {
146
+ if (unitType === 'days') return addLocalCalendarDays(base, unit);
147
+ if (unitType === 'hours') return addLocalHours(base, unit);
148
+ return addLocalMinutes(base, unit);
149
+ }
150
+ function evaluateCandidateTimeGate(params) {
151
+ var product = params.product,
152
+ candidateRange = params.candidateRange,
153
+ evaluationContext = params.evaluationContext,
154
+ timezoneName = params.timezoneName;
155
+ var now = evaluationContext.evaluatedAt;
156
+ if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
157
+ return {
158
+ status: 'past',
159
+ reasonCodes: ['past'],
160
+ conflictCode: 'past'
161
+ };
162
+ }
163
+ var policy = product.cutOffPolicy;
164
+ if (!policy) return null;
165
+ if (resolveCutOffPolicyIssue(policy)) {
166
+ return {
167
+ status: 'unavailable',
168
+ reasonCodes: ['booking_cutoff_invalid'],
169
+ conflictCode: 'booking_cutoff'
170
+ };
171
+ }
172
+ if ('futureDay' in policy && policy.futureDay !== undefined) {
173
+ var futureDay = normalizeNonNegativeNumber(policy.futureDay);
174
+ var lastAllowedDateOrdinal = dateOrdinalFromLocalDate(formatDate(now)) + futureDay;
175
+ if (dateOrdinalFromLocalDate(formatDate(candidateRange.start)) > lastAllowedDateOrdinal) {
176
+ return {
177
+ status: 'unavailable',
178
+ reasonCodes: ['booking_cutoff_future_day'],
179
+ conflictCode: 'booking_cutoff'
180
+ };
181
+ }
182
+ }
183
+ if (policy.type === 'ongoing') {
184
+ if (policy.mode === 'before_end') return null;
185
+ var graceMinutes = normalizeNonNegativeNumber(policy.graceMinutes);
186
+ var graceDeadline = addMinutesZoned(candidateRange.start, graceMinutes, timezoneName);
187
+ if (now < graceDeadline) return null;
188
+ return {
189
+ status: 'unavailable',
190
+ reasonCodes: ['booking_cutoff_after_start'],
191
+ conflictCode: 'booking_cutoff'
192
+ };
193
+ }
194
+ if (policy.type === 'before_start') {
195
+ if (now < candidateRange.start) return null;
196
+ return {
197
+ status: 'unavailable',
198
+ reasonCodes: ['booking_cutoff_before_start'],
199
+ conflictCode: 'booking_cutoff'
200
+ };
201
+ }
202
+ if (policy.type === 'advance') {
203
+ var leadWindow = addLeadWindow(now, normalizeNonNegativeNumber(policy.unit), policy.unitType, timezoneName);
204
+ if (leadWindow <= candidateRange.start) return null;
205
+ return {
206
+ status: 'unavailable',
207
+ reasonCodes: ['booking_cutoff_advance'],
208
+ conflictCode: 'booking_cutoff'
209
+ };
210
+ }
211
+ return {
212
+ status: 'unavailable',
213
+ reasonCodes: ['booking_cutoff_invalid'],
214
+ conflictCode: 'booking_cutoff'
215
+ };
216
+ }
217
+ function overlaps(left, right) {
218
+ return left.start < right.end && left.end > right.start;
219
+ }
220
+ function contains(outer, inner) {
221
+ return inner.start >= outer.start && inner.end <= outer.end;
222
+ }
223
+ function clipRange(base, clip) {
224
+ var start = Math.max(base.start, clip.start);
225
+ var end = Math.min(base.end, clip.end);
226
+ if (end <= start) return null;
227
+ return {
228
+ start: start,
229
+ end: end
230
+ };
231
+ }
232
+ function sortRanges(ranges) {
233
+ return _toConsumableArray(ranges).sort(function (left, right) {
234
+ return left.start - right.start;
81
235
  });
82
236
  }
83
- function getQueryResources(resources, query) {
84
- var _query$resourceIds;
85
- if (!((_query$resourceIds = query.resourceIds) !== null && _query$resourceIds !== void 0 && _query$resourceIds.length)) return resources;
86
- return resources.filter(function (resource) {
87
- return query.resourceIds.some(function (id) {
88
- return sameId(id, resource.id);
89
- });
237
+ function mergeRanges(ranges) {
238
+ var sorted = sortRanges(ranges);
239
+ var merged = [];
240
+ sorted.forEach(function (range) {
241
+ var last = merged[merged.length - 1];
242
+ if (!last) {
243
+ merged.push(range);
244
+ return;
245
+ }
246
+ if (range.start <= last.end) {
247
+ last.end = Math.max(range.end, last.end);
248
+ return;
249
+ }
250
+ merged.push(range);
90
251
  });
252
+ return merged;
91
253
  }
92
- function matchesResourceRequirement(resource, requirement) {
93
- var _requirement$resource;
94
- var isFormlessCandidate = resource.formId == null;
95
- var formMatches = requirement.formId == null || isFormlessCandidate || sameId(requirement.formId, resource.formId);
96
- var resourceMatches = !((_requirement$resource = requirement.resourceIds) !== null && _requirement$resource !== void 0 && _requirement$resource.length) || isFormlessCandidate || requirement.resourceIds.some(function (id) {
97
- return sameId(id, resource.id);
254
+ function intersectRangeLists(left, right) {
255
+ var results = [];
256
+ left.forEach(function (leftRange) {
257
+ right.forEach(function (rightRange) {
258
+ var clipped = clipRange(leftRange, rightRange);
259
+ if (clipped) results.push(clipped);
260
+ });
98
261
  });
99
- return formMatches && resourceMatches;
100
- }
101
- function resolveProductTitle(product) {
102
- var _product$raw, _product$raw2, _product$raw3;
103
- var rawTitle = ((_product$raw = product.raw) === null || _product$raw === void 0 ? void 0 : _product$raw.title) || ((_product$raw2 = product.raw) === null || _product$raw2 === void 0 ? void 0 : _product$raw2.name) || ((_product$raw3 = product.raw) === null || _product$raw3 === void 0 ? void 0 : _product$raw3.product_title);
104
- if (typeof product.title === 'string') return product.title;
105
- if (typeof rawTitle === 'string') return rawTitle;
106
- if (rawTitle && _typeof(rawTitle) === 'object') {
107
- return rawTitle.en || rawTitle.zh || rawTitle.default || Object.values(rawTitle)[0];
262
+ return mergeRanges(results);
263
+ }
264
+ function enumerateDates(startDate, endDate) {
265
+ var dates = [];
266
+ var cursor = startDate;
267
+ while (cursor <= endDate) {
268
+ dates.push(cursor);
269
+ cursor = nextDate(cursor);
108
270
  }
109
- return undefined;
110
- }
111
- export function buildDemandIntervals(products, query, context) {
112
- var dates = query.date ? [query.date] : query.dateRange ? addDays(query.dateRange.start, query.dateRange.end) : [];
113
- var selectedProducts = getQueryProducts(products, query);
114
- var intervals = [];
115
- var _iterator = _createForOfIteratorHelper(selectedProducts),
116
- _step;
117
- try {
118
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
119
- var product = _step.value;
120
- var quantity = Math.max(1, Number(product.quantity || 1));
121
- var capacityRequired = Math.max(1, Number(query.pax || product.capacityRequired || 1));
122
- var requirements = product.resourceRequirements || [];
123
- if (product.kind === 'session' || product.kind === 'venue_slot') {
124
- var schedules = product.schedule || [];
125
- var _iterator2 = _createForOfIteratorHelper(schedules),
126
- _step2;
127
- try {
128
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
129
- var schedule = _step2.value;
130
- var scheduleDate = dateOf(schedule.start_at);
131
- if (dates.length > 0 && !dates.includes(scheduleDate)) continue;
132
- var start = dayjs(schedule.start_at);
133
- var end = dayjs(schedule.end_at);
134
- intervals.push({
135
- id: "".concat(product.id, ":").concat(start.toISOString(), ":").concat(end.toISOString()),
136
- productId: product.id,
137
- source: product.kind,
138
- date: dateOf(schedule.start_at),
139
- start_at: start.toISOString(),
140
- end_at: end.toISOString(),
141
- durationMinutes: Math.max(1, end.diff(start, 'minute')),
142
- quantity: quantity,
143
- capacityRequired: capacityRequired,
144
- resourceRequirements: requirements
145
- });
146
- }
147
- } catch (err) {
148
- _iterator2.e(err);
149
- } finally {
150
- _iterator2.f();
151
- }
152
- continue;
153
- }
154
- if (product.kind === 'duration') {
155
- var duration = Math.max(1, Number(product.durationMinutes || 30));
156
- var _iterator3 = _createForOfIteratorHelper(dates),
157
- _step3;
158
- try {
159
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
160
- var currentDate = _step3.value;
161
- var _start = query.startTime ? toDateTime(currentDate, query.startTime) : toDateTime(currentDate, (context === null || context === void 0 ? void 0 : context.businessStartTime) || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessStartTime);
162
- var _end = query.startTime && query.endTime ? toDateTime(currentDate, query.endTime) : query.startTime ? _start.add(duration, 'minute') : toDateTime(currentDate, (context === null || context === void 0 ? void 0 : context.businessEndTime) || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessEndTime);
163
- if (!_end.isAfter(_start)) _end = _end.add(1, 'day');
164
- intervals.push({
165
- id: "".concat(product.id, ":").concat(currentDate, ":").concat(query.startTime || 'range'),
166
- productId: product.id,
167
- source: 'duration',
168
- date: currentDate,
169
- start_at: _start.toISOString(),
170
- end_at: _end.toISOString(),
171
- durationMinutes: duration,
172
- quantity: quantity,
173
- capacityRequired: capacityRequired,
174
- resourceRequirements: requirements
175
- });
176
- }
177
- } catch (err) {
178
- _iterator3.e(err);
179
- } finally {
180
- _iterator3.f();
181
- }
182
- }
271
+ return dates;
272
+ }
273
+ function normalizeCoverage(input) {
274
+ var startDate = input.coverage.startDate;
275
+ var endDate = input.coverage.endDate;
276
+ var isCanonicalDate = function isCanonicalDate(value) {
277
+ try {
278
+ return formatLocalDate(parseLocalDate(value)) === value;
279
+ } catch (_error) {
280
+ return false;
183
281
  }
184
- } catch (err) {
185
- _iterator.e(err);
186
- } finally {
187
- _iterator.f();
282
+ };
283
+ if (!startDate || !endDate || !isCanonicalDate(startDate) || !isCanonicalDate(endDate)) {
284
+ throw availabilityError('INVALID_DATE_RANGE', 'coverage.startDate / coverage.endDate 非法', {
285
+ coverage: input.coverage
286
+ });
188
287
  }
189
- return intervals;
288
+ if (endDate < startDate) {
289
+ throw availabilityError('INVALID_DATE_RANGE', 'coverage.endDate 不能早于 startDate', {
290
+ coverage: input.coverage
291
+ });
292
+ }
293
+ return {
294
+ dateRange: {
295
+ startDate: startDate,
296
+ endDate: endDate
297
+ },
298
+ absolute: {
299
+ start: toDateStart(startDate, input.timezone),
300
+ end: toDateEnd(nextDate(endDate, input.timezone), input.timezone)
301
+ }
302
+ };
303
+ }
304
+ function normalizeResourceWindow(resource, coverage, timezoneName) {
305
+ return mergeRanges((resource.windows || []).map(function (window) {
306
+ return normalizeAbsoluteRange(window, timezoneName);
307
+ }).filter(function (window) {
308
+ return Boolean(window);
309
+ }).map(function (window) {
310
+ return clipRange(window, coverage);
311
+ }).filter(function (window) {
312
+ return Boolean(window);
313
+ }));
314
+ }
315
+ function normalizeOccupancy(occupancy, coverage, timezoneName, fixedOffsetMinutes) {
316
+ var normalized = normalizeAbsoluteRange(occupancy, timezoneName);
317
+ if (!normalized) return null;
318
+ var clipped = clipRange(normalized, coverage);
319
+ if (!clipped) return null;
320
+ return _objectSpread(_objectSpread({}, occupancy), {}, {
321
+ startAt: formatDateTime(clipped.start, timezoneName, fixedOffsetMinutes),
322
+ endAt: formatDateTime(clipped.end, timezoneName, fixedOffsetMinutes),
323
+ pax: Math.max(1, Number(occupancy.pax || 1) || 1)
324
+ });
190
325
  }
191
- function getEventsForResource(resource, contextEvents) {
192
- var embeddedEvents = resource.times.flatMap(function (time) {
193
- return time.event_list || [];
326
+ function normalizeRequirementGroup(product, requirementGroup) {
327
+ var _requirementGroup$for;
328
+ var resourceType = 'single';
329
+ var required = requirementGroup.required !== false;
330
+ var min = required ? 1 : 0;
331
+ return _objectSpread(_objectSpread({}, requirementGroup), {}, {
332
+ id: requirementGroup.id || "group:".concat(String(product.id), ":").concat(String((_requirementGroup$for = requirementGroup.formId) !== null && _requirementGroup$for !== void 0 ? _requirementGroup$for : resourceType)),
333
+ resourceType: resourceType,
334
+ required: required,
335
+ min: min,
336
+ max: 1,
337
+ resourceIds: uniqById(requirementGroup.resourceIds || []),
338
+ capacityRequired: Math.max(1, Number(requirementGroup.capacityRequired || product.capacityRequired || 1) || 1)
194
339
  });
195
- return [].concat(_toConsumableArray(embeddedEvents), _toConsumableArray(contextEvents.filter(function (event) {
196
- var _event$resourceId;
197
- return sameId((_event$resourceId = event.resourceId) !== null && _event$resourceId !== void 0 ? _event$resourceId : event.resource_id, resource.id);
198
- }))).filter(Boolean);
199
- }
200
- function collectEventsByResource(resources, contextEvents) {
201
- var eventsByResource = new Map();
202
- resources.forEach(function (resource) {
203
- eventsByResource.set(String(resource.id), getEventsForResource(resource, contextEvents));
340
+ }
341
+ function normalizeProducts(products) {
342
+ return products.map(function (product) {
343
+ return _objectSpread(_objectSpread({}, product), {}, {
344
+ quantity: Math.max(1, Number(product.quantity || 1) || 1),
345
+ capacityRequired: Math.max(1, Number(product.capacityRequired || 1) || 1),
346
+ durationMinutes: Math.max(1, Number(product.durationMinutes || 30) || 30),
347
+ cutOffPolicy: product.cutOffPolicy ? _objectSpread({}, product.cutOffPolicy) : undefined,
348
+ requirementGroups: (product.requirementGroups || []).map(function (group) {
349
+ return normalizeRequirementGroup(product, group);
350
+ }),
351
+ sessionRules: (product.sessionRules || []).map(function (rule) {
352
+ return _objectSpread(_objectSpread({}, rule), {}, {
353
+ weekdays: rule.weekdays ? _toConsumableArray(rule.weekdays) : undefined,
354
+ dates: rule.dates ? _toConsumableArray(rule.dates) : undefined,
355
+ includeDates: rule.includeDates ? _toConsumableArray(rule.includeDates) : undefined,
356
+ excludeDates: rule.excludeDates ? _toConsumableArray(rule.excludeDates) : undefined,
357
+ scheduleRef: rule.scheduleRef ? _objectSpread({}, rule.scheduleRef) : undefined
358
+ });
359
+ }),
360
+ sessionRanges: (product.sessionRanges || []).map(function (range) {
361
+ var _range$scheduleRefs;
362
+ return _objectSpread(_objectSpread({}, range), {}, {
363
+ scheduleRefs: (_range$scheduleRefs = range.scheduleRefs) === null || _range$scheduleRefs === void 0 ? void 0 : _range$scheduleRefs.map(function (scheduleRef) {
364
+ return _objectSpread({}, scheduleRef);
365
+ })
366
+ });
367
+ })
368
+ });
204
369
  });
205
- return eventsByResource;
206
- }
207
- function getWorkingSegmentsForInterval(resource, interval) {
208
- var intervalStart = dayjs(interval.start_at);
209
- var intervalEnd = dayjs(interval.end_at);
210
- return resource.times.map(function (time) {
211
- var start = maxDayjs(dayjs(time.start_at), intervalStart);
212
- var end = minDayjs(dayjs(time.end_at), intervalEnd);
370
+ }
371
+ function normalizeResources(resources) {
372
+ return resources.map(function (resource) {
373
+ return _objectSpread(_objectSpread({}, resource), {}, {
374
+ resourceType: 'single',
375
+ capacity: Math.max(1, Number(resource.capacity || 1) || 1),
376
+ windows: (resource.windows || []).map(function (window) {
377
+ return _objectSpread({}, window);
378
+ })
379
+ });
380
+ });
381
+ }
382
+ function normalizeOperatingHours(params) {
383
+ var _params$input, _params$input2, _params$input3;
384
+ var fallbackWindows = enumerateDates(params.dateRange.startDate, params.dateRange.endDate).map(function (date) {
385
+ var start = toDateTimeInDate(date, params.businessHours.startTime, params.timezoneName);
386
+ var end = toDateTimeInDate(date, params.businessHours.endTime, params.timezoneName);
387
+ if (end <= start) {
388
+ end = toDateTimeInDate(nextDate(date), params.businessHours.endTime, params.timezoneName);
389
+ }
213
390
  return {
214
391
  start: start,
215
392
  end: end
216
393
  };
217
- }).filter(function (segment) {
218
- return segment.end.isAfter(segment.start);
219
394
  });
220
- }
221
- function mergeBusySegments(segments) {
222
- var sorted = _toConsumableArray(segments).filter(function (segment) {
223
- return segment.end.isAfter(segment.start);
224
- }).sort(function (left, right) {
225
- return left.start.valueOf() - right.start.valueOf();
395
+ var sourceWindows = params.input ? params.input.windows : fallbackWindows.map(function (range) {
396
+ return {
397
+ startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
398
+ endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
399
+ };
226
400
  });
227
- var merged = [];
228
- sorted.forEach(function (segment) {
229
- var previous = merged[merged.length - 1];
230
- if (previous && !segment.start.isAfter(previous.end)) {
231
- previous.end = maxDayjs(previous.end, segment.end);
232
- } else {
233
- merged.push(_objectSpread({}, segment));
234
- }
401
+ var windows = mergeRanges(sourceWindows.map(function (window) {
402
+ return normalizeAbsoluteRange(window, params.timezoneName);
403
+ }).filter(function (window) {
404
+ return Boolean(window);
405
+ }).map(function (window) {
406
+ return clipRange(window, params.coverage);
407
+ }).filter(function (window) {
408
+ return Boolean(window);
409
+ })).map(function (range) {
410
+ return {
411
+ startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
412
+ endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
413
+ };
235
414
  });
236
- return merged;
415
+ return {
416
+ source: ((_params$input = params.input) === null || _params$input === void 0 ? void 0 : _params$input.source) || params.fallbackSource,
417
+ scheduleIds: uniqById(((_params$input2 = params.input) === null || _params$input2 === void 0 ? void 0 : _params$input2.scheduleIds) || []),
418
+ missingScheduleIds: uniqById(((_params$input3 = params.input) === null || _params$input3 === void 0 ? void 0 : _params$input3.missingScheduleIds) || []),
419
+ windows: windows
420
+ };
237
421
  }
238
- function subtractBusyIntervals(workingSegments, events) {
239
- var available = [];
240
- workingSegments.forEach(function (working) {
241
- var busySegments = mergeBusySegments(events.filter(function (event) {
242
- return overlaps(working.start, working.end, dayjs(event.start_at), dayjs(event.end_at));
243
- }).map(function (event) {
244
- return {
245
- start: maxDayjs(dayjs(event.start_at), working.start),
246
- end: minDayjs(dayjs(event.end_at), working.end)
247
- };
248
- }));
249
- var cursor = working.start;
250
- busySegments.forEach(function (busy) {
251
- if (busy.start.isAfter(cursor)) {
252
- available.push({
253
- start: cursor,
254
- end: busy.start
422
+ function matchesRequirementGroup(requirementGroup, resource, product) {
423
+ var _requirementGroup$res;
424
+ var partySize = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
425
+ if (requirementGroup.formId !== undefined && resource.formId !== undefined && !sameId(requirementGroup.formId, resource.formId)) {
426
+ return false;
427
+ }
428
+ if ((_requirementGroup$res = requirementGroup.resourceIds) !== null && _requirementGroup$res !== void 0 && _requirementGroup$res.length && !requirementGroup.resourceIds.some(function (resourceId) {
429
+ return sameId(resourceId, resource.id);
430
+ })) {
431
+ return false;
432
+ }
433
+ return resource.capacity >= resolveRequiredCapacity(requirementGroup, product, partySize);
434
+ }
435
+ function resolveRequiredCapacity(requirementGroup, product, partySize) {
436
+ var _ref, _ref2;
437
+ return Math.max(1, Number((_ref = (_ref2 = partySize !== null && partySize !== void 0 ? partySize : requirementGroup.capacityRequired) !== null && _ref2 !== void 0 ? _ref2 : product.capacityRequired) !== null && _ref !== void 0 ? _ref : 1) || 1);
438
+ }
439
+ function buildCellKey(params) {
440
+ return [String(params.productId), params.groupId, String(params.resourceId), params.start, params.end].join(':');
441
+ }
442
+ function dedupeOccupancyRefKey(occupancy) {
443
+ if (occupancy.scheduleEventId !== undefined && occupancy.scheduleEventId !== null) {
444
+ return "scheduleEvent:".concat(String(occupancy.resourceId), ":").concat(String(occupancy.scheduleEventId));
445
+ }
446
+ if (occupancy.bookingUid !== undefined && occupancy.bookingUid !== null) {
447
+ return "bookingUid:".concat(String(occupancy.resourceId), ":").concat(String(occupancy.bookingUid));
448
+ }
449
+ if (occupancy.productUid !== undefined && occupancy.productUid !== null) {
450
+ return "productUid:".concat(String(occupancy.resourceId), ":").concat(String(occupancy.productUid));
451
+ }
452
+ return "range:".concat(String(occupancy.resourceId), ":").concat(occupancy.startAt, ":").concat(occupancy.endAt, ":").concat(String(occupancy.pax || 1));
453
+ }
454
+ function buildCells(projection) {
455
+ var _extractFixedOffsetMi;
456
+ var coverage = {
457
+ start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
458
+ end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
459
+ };
460
+ var cells = [];
461
+ var timezoneName = projection.meta.timezone;
462
+ var fixedOffsetMinutes = (_extractFixedOffsetMi = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi !== void 0 ? _extractFixedOffsetMi : captureRuntimeOffsetMinutes();
463
+ var projectionNow = toDateTime(projection.meta.now);
464
+ projection.products.forEach(function (product) {
465
+ (product.requirementGroups || []).forEach(function (requirementGroup) {
466
+ projection.resources.filter(function (resource) {
467
+ return matchesRequirementGroup(requirementGroup, resource, product);
468
+ }).forEach(function (resource) {
469
+ var windows = normalizeResourceWindow(resource, coverage, timezoneName);
470
+ var occupancies = projection.resourceOccupancies.filter(function (occupancy) {
471
+ return sameId(occupancy.resourceId, resource.id);
472
+ }).map(function (occupancy) {
473
+ return {
474
+ occupancy: occupancy,
475
+ range: normalizeAbsoluteRange(occupancy, timezoneName)
476
+ };
477
+ }).filter(function (item) {
478
+ return Boolean(item.range);
479
+ }).map(function (item) {
480
+ return _objectSpread(_objectSpread({}, item), {}, {
481
+ range: clipRange(item.range, coverage)
482
+ });
483
+ }).filter(function (item) {
484
+ return Boolean(item.range);
255
485
  });
256
- }
257
- if (busy.end.isAfter(cursor)) cursor = busy.end;
258
- });
259
- if (working.end.isAfter(cursor)) {
260
- available.push({
261
- start: cursor,
262
- end: working.end
263
- });
264
- }
265
- });
266
- return available;
267
- }
268
- function buildCapacityRanges(params) {
269
- var workingSegments = params.workingSegments,
270
- events = params.events,
271
- maxCapacity = params.maxCapacity,
272
- requiredCapacity = params.requiredCapacity;
273
- var ranges = [];
274
- workingSegments.forEach(function (working) {
275
- var boundaries = new Map([[working.start.valueOf(), working.start], [working.end.valueOf(), working.end]]);
276
- events.forEach(function (event) {
277
- var eventStart = dayjs(event.start_at);
278
- var eventEnd = dayjs(event.end_at);
279
- if (!overlaps(working.start, working.end, eventStart, eventEnd)) return;
280
- var clippedStart = maxDayjs(eventStart, working.start);
281
- var clippedEnd = minDayjs(eventEnd, working.end);
282
- boundaries.set(clippedStart.valueOf(), clippedStart);
283
- boundaries.set(clippedEnd.valueOf(), clippedEnd);
284
- });
285
- var points = Array.from(boundaries.values()).sort(function (left, right) {
286
- return left.valueOf() - right.valueOf();
287
- });
288
- var _loop = function _loop() {
289
- var start = points[index];
290
- var end = points[index + 1];
291
- if (!end.isAfter(start)) return 0; // continue
292
- var usedCapacity = events.filter(function (event) {
293
- return overlaps(start, end, dayjs(event.start_at), dayjs(event.end_at));
294
- }).reduce(function (sum, event) {
295
- return sum + Math.max(1, Number(event.pax || 1));
296
- }, 0);
297
- var remainingCapacity = Math.max(0, maxCapacity - usedCapacity);
298
- if (remainingCapacity < requiredCapacity) return 0; // continue
299
- var status = remainingCapacity < maxCapacity ? 'limited' : 'available';
300
- var previous = ranges[ranges.length - 1];
301
- if (previous && previous.end.isSame(start) && previous.remainingCapacity === remainingCapacity && previous.maxCapacity === maxCapacity && previous.status === status) {
302
- previous.end = end;
303
- } else {
304
- ranges.push({
486
+ var boundaries = new Set([coverage.start, coverage.end]);
487
+ windows.forEach(function (window) {
488
+ boundaries.add(window.start);
489
+ boundaries.add(window.end);
490
+ });
491
+ occupancies.forEach(function (_ref3) {
492
+ var range = _ref3.range;
493
+ boundaries.add(range.start);
494
+ boundaries.add(range.end);
495
+ });
496
+ var sortedBoundaries = Array.from(boundaries.values()).sort(function (left, right) {
497
+ return left - right;
498
+ });
499
+ var _loop = function _loop() {
500
+ var start = sortedBoundaries[index];
501
+ var end = sortedBoundaries[index + 1];
502
+ if (end <= start) return 1; // continue
503
+ var segment = {
305
504
  start: start,
306
- end: end,
505
+ end: end
506
+ };
507
+ var isInsideWorkingWindow = windows.some(function (window) {
508
+ return contains(window, segment);
509
+ });
510
+ var segmentOccupancies = isInsideWorkingWindow ? occupancies.filter(function (_ref4) {
511
+ var range = _ref4.range;
512
+ return overlaps(range, segment);
513
+ }).map(function (_ref5) {
514
+ var occupancy = _ref5.occupancy;
515
+ return occupancy;
516
+ }) : [];
517
+ var usedCapacity = segmentOccupancies.reduce(function (sum, occupancy) {
518
+ return sum + Math.max(1, Number(occupancy.pax || 1) || 1);
519
+ }, 0);
520
+ var remainingCapacity = Math.max(0, resource.capacity - usedCapacity);
521
+ var requiredCapacity = resolveRequiredCapacity(requirementGroup, product);
522
+ var reasonCodes = Array.from(new Set(segmentOccupancies.flatMap(function (occupancy) {
523
+ var source = occupancy.source || 'remote';
524
+ if (source === 'selection') return ['selection_occupied'];
525
+ if (source === 'cart') return ['cart_occupied'];
526
+ return ['resource_occupied'];
527
+ })));
528
+ var status = 'available';
529
+ if (end <= projectionNow) {
530
+ status = 'past';
531
+ reasonCodes.push('past');
532
+ } else if (!isInsideWorkingWindow) {
533
+ status = 'unavailable';
534
+ reasonCodes.push('outside_resource_window');
535
+ } else if (remainingCapacity < requiredCapacity) {
536
+ status = 'full';
537
+ reasonCodes.push('capacity_full');
538
+ } else if (remainingCapacity < resource.capacity) {
539
+ status = 'limited';
540
+ }
541
+ cells.push({
542
+ key: buildCellKey({
543
+ productId: product.id,
544
+ groupId: requirementGroup.id,
545
+ resourceId: resource.id,
546
+ start: start,
547
+ end: end
548
+ }),
549
+ productId: product.id,
550
+ requirementGroupId: requirementGroup.id,
551
+ requirementFormId: requirementGroup.formId,
552
+ resourceId: resource.id,
553
+ resourceName: resource.name,
554
+ resourceType: resource.resourceType,
555
+ startAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
556
+ endAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
557
+ date: formatDate(start, timezoneName),
558
+ startTime: formatTime(start, timezoneName),
559
+ endTime: formatTime(end, timezoneName),
560
+ timezone: projection.meta.timezone,
561
+ status: status,
562
+ // 资源组当前统一单选;具体资源仍用 capacity 表达可承载的预约数量。
307
563
  remainingCapacity: remainingCapacity,
308
- maxCapacity: maxCapacity,
309
- status: status
564
+ maxCapacity: resource.capacity,
565
+ reasonCodes: Array.from(new Set(reasonCodes)),
566
+ occupancyRefs: segmentOccupancies.map(function (occupancy) {
567
+ return {
568
+ id: occupancy.id,
569
+ resourceId: occupancy.resourceId,
570
+ source: occupancy.source,
571
+ serverEventId: occupancy.serverEventId,
572
+ bookingUid: occupancy.bookingUid,
573
+ productUid: occupancy.productUid,
574
+ scheduleEventId: occupancy.scheduleEventId
575
+ };
576
+ })
310
577
  });
578
+ };
579
+ for (var index = 0; index < sortedBoundaries.length - 1; index += 1) {
580
+ if (_loop()) continue;
311
581
  }
312
- },
313
- _ret;
314
- for (var index = 0; index < points.length - 1; index += 1) {
315
- _ret = _loop();
316
- if (_ret === 0) continue;
317
- }
582
+ });
583
+ });
584
+ });
585
+ return compressCells(cells);
586
+ }
587
+ function compressCells(cells) {
588
+ var sorted = _toConsumableArray(cells).sort(function (left, right) {
589
+ var keyLeft = [String(left.productId), String(left.requirementGroupId), String(left.resourceId), left.startAt].join(':');
590
+ var keyRight = [String(right.productId), String(right.requirementGroupId), String(right.resourceId), right.startAt].join(':');
591
+ return keyLeft.localeCompare(keyRight);
318
592
  });
319
- return ranges;
320
- }
321
- function rangeFitsDuration(segment, durationMinutes) {
322
- return segment.end.diff(segment.start, 'minute') >= durationMinutes;
323
- }
324
- function makeAvailabilityRangeCell(params) {
325
- var _segment$remainingCap;
326
- var interval = params.interval,
327
- resource = params.resource,
328
- segment = params.segment;
329
- var maxCapacity = Math.max(1, Number(segment.maxCapacity || resource.capacity || 1));
330
- var remainingCapacity = resource.resourceType === 'single' ? 1 : Math.max(0, Number((_segment$remainingCap = segment.remainingCapacity) !== null && _segment$remainingCap !== void 0 ? _segment$remainingCap : maxCapacity));
331
- var status = resource.resourceType === 'single' ? 'available' : segment.status || (remainingCapacity < maxCapacity ? 'limited' : 'available');
593
+ return sorted.reduce(function (result, cell) {
594
+ var last = result[result.length - 1];
595
+ if (last && sameId(last.productId, cell.productId) && sameId(last.requirementGroupId, cell.requirementGroupId) && sameId(last.resourceId, cell.resourceId) && last.status === cell.status && last.remainingCapacity === cell.remainingCapacity && last.maxCapacity === cell.maxCapacity && JSON.stringify(last.reasonCodes) === JSON.stringify(cell.reasonCodes) && JSON.stringify(last.occupancyRefs) === JSON.stringify(cell.occupancyRefs) && last.endAt === cell.startAt) {
596
+ last.endAt = cell.endAt;
597
+ last.endTime = cell.endTime;
598
+ return result;
599
+ }
600
+ result.push(_objectSpread({}, cell));
601
+ return result;
602
+ }, []);
603
+ }
604
+ function makeCoverageAbsolute(dateRange, timezoneName, fixedOffsetMinutes) {
605
+ var guardEndDate = nextDate(dateRange.endDate, timezoneName);
332
606
  return {
333
- key: "".concat(interval.productId, ":").concat(resource.id, ":").concat(segment.start.toISOString(), ":").concat(segment.end.toISOString()),
334
- productId: interval.productId,
335
- resourceId: resource.id,
336
- date: interval.date,
337
- start_at: segment.start.toISOString(),
338
- end_at: segment.end.toISOString(),
339
- startTime: segment.start.format('HH:mm'),
340
- endTime: segment.end.format('HH:mm'),
341
- durationMinutes: interval.durationMinutes,
342
- cellType: 'availability_range',
343
- status: status,
344
- remainingCapacity: remainingCapacity,
345
- maxCapacity: maxCapacity,
346
- reasonCodes: [],
347
- conflicts: []
607
+ startAt: formatDateTime(toDateStart(dateRange.startDate, timezoneName), timezoneName, fixedOffsetMinutes),
608
+ endAt: formatDateTime(toDateEnd(guardEndDate, timezoneName), timezoneName, fixedOffsetMinutes)
348
609
  };
349
610
  }
350
- function buildAvailabilityCellsForResource(params) {
351
- var resource = params.resource,
352
- interval = params.interval,
353
- events = params.events;
354
- var requiredCapacity = Math.max(1, Number(interval.capacityRequired || 1));
355
- var durationMinutes = Math.max(1, Number(interval.durationMinutes || 1));
356
- var maxCapacity = Math.max(1, Number(resource.capacity || 1));
357
- var workingSegments = getWorkingSegmentsForInterval(resource, interval);
358
- var now = dayjs();
359
- workingSegments = workingSegments.map(function (segment) {
360
- return {
361
- start: segment.end.isAfter(now) ? maxDayjs(segment.start, now) : segment.start,
362
- end: segment.end
363
- };
364
- }).filter(function (segment) {
365
- return segment.end.isAfter(segment.start);
366
- });
367
- var availableSegments = resource.resourceType === 'single' ? subtractBusyIntervals(workingSegments, events).map(function (segment) {
368
- return _objectSpread(_objectSpread({}, segment), {}, {
369
- remainingCapacity: 1,
370
- maxCapacity: 1,
371
- status: 'available'
611
+ function normalizeProjectionInstant(value, fallbackScalar, timezoneName, fixedOffsetMinutes) {
612
+ var scalar = value ? toDateTime(value) : fallbackScalar;
613
+ return formatDateTime(Number.isFinite(scalar) ? scalar : fallbackScalar, timezoneName, fixedOffsetMinutes);
614
+ }
615
+ function cloneSnapshotMetadataValue(value) {
616
+ if (Array.isArray(value)) {
617
+ return value.map(function (item) {
618
+ return cloneSnapshotMetadataValue(item);
372
619
  });
373
- }) : buildCapacityRanges({
374
- workingSegments: workingSegments,
375
- events: events,
376
- maxCapacity: maxCapacity,
377
- requiredCapacity: requiredCapacity
620
+ }
621
+ if (!value || _typeof(value) !== 'object') return value;
622
+ return Object.keys(value).reduce(function (result, key) {
623
+ result[key] = cloneSnapshotMetadataValue(value[key]);
624
+ return result;
625
+ }, {});
626
+ }
627
+ function deepFreeze(value) {
628
+ if (!value || _typeof(value) !== 'object' || Object.isFrozen(value)) return value;
629
+ Object.freeze(value);
630
+ Object.getOwnPropertyNames(value).forEach(function (key) {
631
+ // `raw` is an explicit escape hatch to the backend/catalog record. It is not owned by the
632
+ // projection and must not freeze mutable ProductList / Order data shared elsewhere.
633
+ if (key === 'raw') return;
634
+ var child = value[key];
635
+ if (child && (_typeof(child) === 'object' || typeof child === 'function')) {
636
+ deepFreeze(child);
637
+ }
378
638
  });
379
- return availableSegments.filter(function (segment) {
380
- return rangeFitsDuration(segment, durationMinutes);
381
- }).map(function (segment) {
382
- return makeAvailabilityRangeCell({
383
- interval: interval,
384
- resource: resource,
385
- segment: segment
386
- });
639
+ return value;
640
+ }
641
+ export function createAvailabilityProjection(input) {
642
+ var _input$businessHours, _input$businessHours2;
643
+ var coverage = normalizeCoverage(input);
644
+ var fixedOffsetMinutes = captureRuntimeOffsetMinutes();
645
+ var runtimeNow = localDateToScalar(new Date());
646
+ var products = normalizeProducts(input.products || []);
647
+ var resources = normalizeResources(input.resources || []);
648
+ var resourceOccupancies = uniqByOccupancy((input.resourceOccupancies || []).map(function (occupancy) {
649
+ return normalizeOccupancy(occupancy, coverage.absolute, input.timezone, fixedOffsetMinutes);
650
+ }).filter(function (occupancy) {
651
+ return Boolean(occupancy);
652
+ }));
653
+ var preparedAt = normalizeProjectionInstant(input.preparedAt, runtimeNow, input.timezone, fixedOffsetMinutes);
654
+ var now = normalizeProjectionInstant(input.now, runtimeNow, input.timezone, fixedOffsetMinutes);
655
+ var durationCandidateAnchorAt = normalizeProjectionInstant(input.durationCandidateAnchorAt || now, runtimeNow, input.timezone, fixedOffsetMinutes);
656
+ var businessHours = {
657
+ startTime: ((_input$businessHours = input.businessHours) === null || _input$businessHours === void 0 ? void 0 : _input$businessHours.startTime) || DEFAULT_BUSINESS_HOURS.startTime,
658
+ endTime: ((_input$businessHours2 = input.businessHours) === null || _input$businessHours2 === void 0 ? void 0 : _input$businessHours2.endTime) || DEFAULT_BUSINESS_HOURS.endTime
659
+ };
660
+ var operatingHours = normalizeOperatingHours({
661
+ input: input.operatingHours,
662
+ businessHours: businessHours,
663
+ coverage: coverage.absolute,
664
+ dateRange: coverage.dateRange,
665
+ timezoneName: input.timezone,
666
+ fixedOffsetMinutes: fixedOffsetMinutes,
667
+ fallbackSource: input.businessHours ? 'prepare_business_hours' : 'default_business_hours'
387
668
  });
669
+ var meta = {
670
+ snapshotId: input.snapshotId || "availability-".concat(Date.now(), "-").concat(Math.random().toString(36).slice(2, 8)),
671
+ preparedAt: preparedAt,
672
+ timezone: input.timezone,
673
+ orderRevision: input.orderRevision,
674
+ mode: input.mode || 'appointment',
675
+ coverage: _objectSpread(_objectSpread({}, coverage.dateRange), makeCoverageAbsolute(coverage.dateRange, input.timezone, fixedOffsetMinutes)),
676
+ defaults: {
677
+ slotStepMinutes: Math.max(1, Number(input.slotStepMinutes || DEFAULT_SLOT_STEP_MINUTES) || DEFAULT_SLOT_STEP_MINUTES),
678
+ businessHours: _objectSpread({}, businessHours),
679
+ operatingHours: operatingHours
680
+ },
681
+ now: now,
682
+ durationCandidateAnchorAt: durationCandidateAnchorAt,
683
+ anchors: input.anchors ? cloneSnapshotMetadataValue(input.anchors) : undefined,
684
+ prepareParams: input.prepareParams ? cloneSnapshotMetadataValue(input.prepareParams) : undefined
685
+ };
686
+ var projectionBase = {
687
+ meta: meta,
688
+ products: products,
689
+ resources: resources,
690
+ resourceOccupancies: resourceOccupancies
691
+ };
692
+ return deepFreeze(_objectSpread(_objectSpread({}, projectionBase), {}, {
693
+ cells: buildCells(projectionBase)
694
+ }));
388
695
  }
389
- export function evaluateResourceForInterval(params) {
390
- var resource = params.resource,
391
- interval = params.interval,
392
- context = params.context;
393
- var start = dayjs(interval.start_at);
394
- var end = dayjs(interval.end_at);
395
- var conflicts = [];
396
- var reasonCodes = [];
397
- var maxCapacity = Math.max(1, Number(resource.capacity || 1));
398
- var requiredCapacity = Math.max(1, Number(interval.capacityRequired || 1));
399
- if (start.isBefore(dayjs())) {
400
- reasonCodes.push('past');
401
- conflicts.push({
402
- type: 'past',
403
- message: '时间已过期',
404
- resourceId: resource.id,
405
- productId: interval.productId
696
+ function uniqByOccupancy(occupancies) {
697
+ var map = new Map();
698
+ occupancies.forEach(function (occupancy) {
699
+ map.set(dedupeOccupancyRefKey(occupancy), occupancy);
700
+ });
701
+ return Array.from(map.values());
702
+ }
703
+ function resolveProjectionDateRange(projection, requestDateRange) {
704
+ var startDate = (requestDateRange === null || requestDateRange === void 0 ? void 0 : requestDateRange.startDate) || projection.meta.coverage.startDate;
705
+ var endDate = (requestDateRange === null || requestDateRange === void 0 ? void 0 : requestDateRange.endDate) || projection.meta.coverage.endDate;
706
+ var isCanonicalDate = function isCanonicalDate(value) {
707
+ try {
708
+ return formatLocalDate(parseLocalDate(value)) === value;
709
+ } catch (_error) {
710
+ return false;
711
+ }
712
+ };
713
+ if (!isCanonicalDate(startDate) || !isCanonicalDate(endDate) || endDate < startDate) {
714
+ throw availabilityError('INVALID_DATE_RANGE', '查询日期范围必须是有效的 YYYY-MM-DD,且结束日期不能早于开始日期', {
715
+ requested: requestDateRange
406
716
  });
407
717
  }
408
- var insideResourceTime = resource.times.some(function (time) {
409
- return containsRange(dayjs(time.start_at), dayjs(time.end_at), start, end);
410
- });
411
- if (!insideResourceTime) {
412
- reasonCodes.push('outside_resource_time');
413
- conflicts.push({
414
- type: 'outside_resource_time',
415
- message: '所选时间不在资源可用时间内',
416
- resourceId: resource.id,
417
- productId: interval.productId
718
+ if (startDate < projection.meta.coverage.startDate || endDate > projection.meta.coverage.endDate) {
719
+ throw availabilityError('OUT_OF_PROJECTION_COVERAGE', '查询范围超出 projection.coverage', {
720
+ requested: requestDateRange,
721
+ coverage: projection.meta.coverage
722
+ });
723
+ }
724
+ return {
725
+ startDate: startDate,
726
+ endDate: endDate
727
+ };
728
+ }
729
+ function buildOperatingRanges(projection, dateRange) {
730
+ var coverage = {
731
+ start: toDateStart(dateRange.startDate, projection.meta.timezone),
732
+ end: toDateEnd(nextDate(dateRange.endDate), projection.meta.timezone)
733
+ };
734
+ return mergeRanges(projection.meta.defaults.operatingHours.windows.map(function (window) {
735
+ return normalizeAbsoluteRange(window, projection.meta.timezone);
736
+ }).filter(function (window) {
737
+ return Boolean(window);
738
+ }).map(function (window) {
739
+ return clipRange(window, coverage);
740
+ }).filter(function (window) {
741
+ return Boolean(window);
742
+ }));
743
+ }
744
+ function buildResourceWindowIndex(projection) {
745
+ var coverage = {
746
+ start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
747
+ end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
748
+ };
749
+ return new Map(projection.resources.map(function (resource) {
750
+ return [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone)];
751
+ }));
752
+ }
753
+ function buildGroupWorkingRanges(params) {
754
+ var _requirementGroup$min;
755
+ var projection = params.projection,
756
+ requirementGroup = params.requirementGroup,
757
+ dateRange = params.dateRange,
758
+ resourceIds = params.resourceIds,
759
+ partySize = params.partySize;
760
+ var guardEndDate = nextDate(dateRange.endDate, projection.meta.timezone);
761
+ var coverage = {
762
+ start: toDateStart(dateRange.startDate, projection.meta.timezone),
763
+ end: toDateEnd(guardEndDate, projection.meta.timezone)
764
+ };
765
+ var filteredResourceIdSet = resourceIds !== null && resourceIds !== void 0 && resourceIds.length ? new Set(resourceIds.map(String)) : null;
766
+ var resourceWindows = projection.resources.filter(function (resource) {
767
+ return matchesRequirementGroup(requirementGroup, resource, params.product, partySize);
768
+ }).filter(function (resource) {
769
+ return !filteredResourceIdSet || filteredResourceIdSet.has(String(resource.id));
770
+ }).map(function (resource) {
771
+ var _params$resourceWindo;
772
+ var windows = ((_params$resourceWindo = params.resourceWindowIndex) === null || _params$resourceWindo === void 0 ? void 0 : _params$resourceWindo.get(String(resource.id))) || normalizeResourceWindow(resource, coverage, projection.meta.timezone);
773
+ return windows.map(function (window) {
774
+ return clipRange(window, coverage);
775
+ }).filter(function (window) {
776
+ return Boolean(window);
418
777
  });
778
+ });
779
+ var requiredConcurrentResources = Math.max(requirementGroup.required === false ? 0 : 1, Number((_requirementGroup$min = requirementGroup.min) !== null && _requirementGroup$min !== void 0 ? _requirementGroup$min : requirementGroup.required === false ? 0 : 1) || 0);
780
+ if (requiredConcurrentResources <= 1) {
781
+ return mergeRanges(resourceWindows.flatMap(function (windows) {
782
+ return windows;
783
+ }));
419
784
  }
420
- var events = getEventsForResource(resource, context.externalEvents);
421
- var overlapping = events.filter(function (event) {
422
- return overlaps(start, end, dayjs(event.start_at), dayjs(event.end_at));
785
+ var boundaries = new Set();
786
+ resourceWindows.forEach(function (windows) {
787
+ windows.forEach(function (window) {
788
+ boundaries.add(window.start);
789
+ boundaries.add(window.end);
790
+ });
423
791
  });
424
- var usedCapacity = resource.resourceType === 'single' ? overlapping.length : overlapping.reduce(function (sum, event) {
425
- return sum + Math.max(1, Number(event.pax || 1));
426
- }, 0);
427
- var remainingCapacity = resource.resourceType === 'single' ? overlapping.length > 0 ? 0 : 1 : Math.max(0, maxCapacity - usedCapacity);
428
- if (overlapping.length > 0 && resource.resourceType === 'single') {
429
- reasonCodes.push('remote_event_overlap');
430
- conflicts.push.apply(conflicts, _toConsumableArray(overlapping.map(function (event) {
431
- return {
432
- type: 'remote_event_overlap',
433
- message: '资源时间被已有预约占用',
434
- event: event,
435
- resourceId: resource.id,
436
- productId: interval.productId
437
- };
438
- })));
792
+ var sortedBoundaries = Array.from(boundaries.values()).sort(function (left, right) {
793
+ return left - right;
794
+ });
795
+ var satisfiedRanges = [];
796
+ var _loop2 = function _loop2() {
797
+ var start = sortedBoundaries[index];
798
+ var end = sortedBoundaries[index + 1];
799
+ if (end <= start) return 1; // continue
800
+ var segment = {
801
+ start: start,
802
+ end: end
803
+ };
804
+ var activeResources = resourceWindows.reduce(function (count, windows) {
805
+ return windows.some(function (window) {
806
+ return contains(window, segment);
807
+ }) ? count + 1 : count;
808
+ }, 0);
809
+ if (activeResources >= requiredConcurrentResources) {
810
+ satisfiedRanges.push(segment);
811
+ }
812
+ };
813
+ for (var index = 0; index < sortedBoundaries.length - 1; index += 1) {
814
+ if (_loop2()) continue;
439
815
  }
440
- if (resource.resourceType !== 'single' && remainingCapacity < requiredCapacity) {
441
- reasonCodes.push('capacity_full');
442
- conflicts.push({
443
- type: 'capacity_full',
444
- message: '资源剩余容量不足',
445
- resourceId: resource.id,
446
- productId: interval.productId
816
+ return mergeRanges(satisfiedRanges);
817
+ }
818
+ function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes) {
819
+ var dates = enumerateDates(dateRange.startDate, dateRange.endDate);
820
+ var seen = new Map();
821
+ rules.forEach(function (rule) {
822
+ dates.forEach(function (date) {
823
+ if (!matchesScheduleRuleDate(rule, date, timezoneName)) return;
824
+ var start = toDateTimeInDate(date, rule.startTime, timezoneName);
825
+ var end = toDateTimeInDate(date, rule.endTime, timezoneName);
826
+ if (end <= start) end = toDateTimeInDate(nextDate(date, timezoneName), rule.endTime, timezoneName);
827
+ var key = "".concat(date, ":").concat(start, ":").concat(end);
828
+ if (!seen.has(key)) {
829
+ seen.set(key, {
830
+ key: key,
831
+ productId: '',
832
+ source: 'session_schedule',
833
+ startAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
834
+ endAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
835
+ bookingStartAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
836
+ bookingEndAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
837
+ date: formatDate(start, timezoneName),
838
+ startTime: formatTime(start, timezoneName),
839
+ endTime: formatTime(end, timezoneName),
840
+ timezone: timezoneName,
841
+ scheduleRefs: []
842
+ });
843
+ }
844
+ var current = seen.get(key);
845
+ var scheduleRef = _objectSpread(_objectSpread({}, rule.scheduleRef || {}), {}, {
846
+ ruleId: rule.id
847
+ });
848
+ current.scheduleRefs = mergeScheduleRefs(current.scheduleRefs, [scheduleRef]);
849
+ if (!current.primaryScheduleRef) current.primaryScheduleRef = scheduleRef;
447
850
  });
851
+ });
852
+ return Array.from(seen.values());
853
+ }
854
+ function matchesScheduleRuleDate(rule, date, _timezoneName) {
855
+ var _rule$includeDates, _rule$excludeDates;
856
+ if ((_rule$includeDates = rule.includeDates) !== null && _rule$includeDates !== void 0 && _rule$includeDates.includes(date)) return true;
857
+ if ((_rule$excludeDates = rule.excludeDates) !== null && _rule$excludeDates !== void 0 && _rule$excludeDates.includes(date)) return false;
858
+ if (rule.startDate && date < rule.startDate) return false;
859
+ if (rule.endDate && date > rule.endDate) return false;
860
+ if (rule.mode === 'designation') {
861
+ var _rule$dates;
862
+ if ((_rule$dates = rule.dates) !== null && _rule$dates !== void 0 && _rule$dates.includes(date)) return true;
863
+ return false;
448
864
  }
449
- var status = 'available';
450
- if (reasonCodes.includes('past')) status = 'past';else if (reasonCodes.includes('outside_resource_time')) status = 'unavailable';else if (reasonCodes.includes('remote_event_overlap') || reasonCodes.includes('capacity_full')) {
451
- status = 'full';
452
- } else if (remainingCapacity < maxCapacity) {
453
- status = 'limited';
865
+ if (rule.mode === 'weekly') {
866
+ var _rule$weekdays;
867
+ if (!((_rule$weekdays = rule.weekdays) !== null && _rule$weekdays !== void 0 && _rule$weekdays.length)) return true;
868
+ var freq = Math.max(1, Number(rule.frequency || 1) || 1);
869
+ if (!rule.weekdays.includes(weekdayFromLocalDate(date))) return false;
870
+ if (!rule.startDate) return true;
871
+ var targetWeekStart = dateOrdinalFromLocalDate(date) - weekdayFromLocalDate(date);
872
+ var ruleWeekStart = dateOrdinalFromLocalDate(rule.startDate) - weekdayFromLocalDate(rule.startDate);
873
+ return Math.floor((targetWeekStart - ruleWeekStart) / 7) % freq === 0;
454
874
  }
455
- return {
456
- key: "".concat(interval.productId, ":").concat(resource.id, ":").concat(interval.start_at, ":").concat(interval.end_at),
457
- productId: interval.productId,
458
- resourceId: resource.id,
459
- date: interval.date,
460
- start_at: interval.start_at,
461
- end_at: interval.end_at,
462
- startTime: timeOf(interval.start_at),
463
- endTime: timeOf(interval.end_at),
464
- durationMinutes: interval.durationMinutes,
465
- cellType: status === 'available' || status === 'limited' ? 'fixed_interval' : 'unavailable',
466
- status: status,
467
- remainingCapacity: remainingCapacity,
468
- maxCapacity: maxCapacity,
469
- reasonCodes: reasonCodes,
470
- conflicts: conflicts
471
- };
875
+ if (rule.mode === 'daily' && rule.startDate) {
876
+ var _freq = Math.max(1, Number(rule.frequency || 1) || 1);
877
+ return (dateOrdinalFromLocalDate(date) - dateOrdinalFromLocalDate(rule.startDate)) % _freq === 0;
878
+ }
879
+ return true;
472
880
  }
473
- function aggregateStatus(cells) {
474
- if (!cells.length) return 'unavailable';
475
- if (cells.some(function (cell) {
476
- return cell.status === 'available';
477
- })) return 'available';
478
- if (cells.some(function (cell) {
479
- return cell.status === 'limited';
480
- })) return 'limited';
481
- if (cells.some(function (cell) {
482
- return cell.status === 'past';
483
- })) return 'past';
484
- if (cells.some(function (cell) {
485
- return cell.status === 'full';
486
- })) return 'full';
487
- if (cells.some(function (cell) {
488
- return cell.status === 'conflict';
489
- })) return 'conflict';
490
- return 'unavailable';
881
+ function mergeScheduleRefs(base, next) {
882
+ var map = new Map();
883
+ [].concat(_toConsumableArray(base), _toConsumableArray(next)).forEach(function (ref) {
884
+ var _ref$scheduleId, _ref$timeSlotId, _ref$ruleId, _ref$source, _ref$label;
885
+ var key = [(_ref$scheduleId = ref.scheduleId) !== null && _ref$scheduleId !== void 0 ? _ref$scheduleId : '', (_ref$timeSlotId = ref.timeSlotId) !== null && _ref$timeSlotId !== void 0 ? _ref$timeSlotId : '', (_ref$ruleId = ref.ruleId) !== null && _ref$ruleId !== void 0 ? _ref$ruleId : '', (_ref$source = ref.source) !== null && _ref$source !== void 0 ? _ref$source : '', (_ref$label = ref.label) !== null && _ref$label !== void 0 ? _ref$label : ''].join(':');
886
+ map.set(key, ref);
887
+ });
888
+ return Array.from(map.values());
491
889
  }
492
- function uniqueReasonCodes(cells) {
493
- return Array.from(new Set(cells.flatMap(function (cell) {
494
- return cell.reasonCodes || [];
495
- })));
890
+ function buildProductIndex(projection) {
891
+ return new Map(projection.products.map(function (product) {
892
+ return [String(product.id), product];
893
+ }));
496
894
  }
497
- function buildProductOptions(products, cells, query) {
498
- var queryProducts = getQueryProducts(products, query);
499
- return queryProducts.map(function (product) {
500
- var productCells = cells.filter(function (cell) {
501
- return sameId(cell.productId, product.id);
895
+ function buildGroupResourceIndex(params) {
896
+ var _params$resourceIds;
897
+ var resourceIdSet = (_params$resourceIds = params.resourceIds) !== null && _params$resourceIds !== void 0 && _params$resourceIds.length ? new Set(params.resourceIds.map(String)) : null;
898
+ var index = new Map();
899
+ params.projection.products.forEach(function (product) {
900
+ (product.requirementGroups || []).forEach(function (requirementGroup) {
901
+ var key = [String(product.id), requirementGroup.id].join(':');
902
+ index.set(key, params.projection.resources.filter(function (resource) {
903
+ return matchesRequirementGroup(requirementGroup, resource, product, params.partySize);
904
+ }).filter(function (resource) {
905
+ return !resourceIdSet || resourceIdSet.has(String(resource.id));
906
+ }));
502
907
  });
503
- var status = product.kind === 'normal' ? 'available' : aggregateStatus(productCells);
908
+ });
909
+ return index;
910
+ }
911
+ function getSessionRangesForProduct(projection, product, dateRange) {
912
+ var _extractFixedOffsetMi2;
913
+ var fixedOffsetMinutes = (_extractFixedOffsetMi2 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi2 !== void 0 ? _extractFixedOffsetMi2 : captureRuntimeOffsetMinutes();
914
+ var ranges = (product.sessionRanges || []).map(function (range) {
915
+ var _range$scheduleRefs2;
916
+ var normalized = normalizeAbsoluteRange(range, projection.meta.timezone);
917
+ if (!normalized) return null;
918
+ var startDate = formatDate(normalized.start, projection.meta.timezone);
919
+ if (startDate < dateRange.startDate || startDate > dateRange.endDate) {
920
+ return null;
921
+ }
504
922
  return {
923
+ key: "".concat(String(product.id), ":").concat(normalized.start, ":").concat(normalized.end),
505
924
  productId: product.id,
506
- title: resolveProductTitle(product),
507
- status: status,
508
- reasonCodes: uniqueReasonCodes(productCells)
925
+ source: 'session_schedule',
926
+ startAt: formatDateTime(normalized.start, projection.meta.timezone, fixedOffsetMinutes),
927
+ endAt: formatDateTime(normalized.end, projection.meta.timezone, fixedOffsetMinutes),
928
+ bookingStartAt: formatDateTime(normalized.start, projection.meta.timezone, fixedOffsetMinutes),
929
+ bookingEndAt: formatDateTime(normalized.end, projection.meta.timezone, fixedOffsetMinutes),
930
+ date: formatDate(normalized.start, projection.meta.timezone),
931
+ startTime: formatTime(normalized.start, projection.meta.timezone),
932
+ endTime: formatTime(normalized.end, projection.meta.timezone),
933
+ timezone: projection.meta.timezone,
934
+ scheduleRefs: range.scheduleRefs || [],
935
+ primaryScheduleRef: (_range$scheduleRefs2 = range.scheduleRefs) === null || _range$scheduleRefs2 === void 0 ? void 0 : _range$scheduleRefs2[0]
509
936
  };
937
+ }).filter(Boolean);
938
+ var ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes);
939
+ return dedupeProductRanges([].concat(_toConsumableArray(ranges), _toConsumableArray(ruleRanges.map(function (range) {
940
+ return _objectSpread(_objectSpread({}, range), {}, {
941
+ key: "".concat(String(product.id), ":").concat(range.startAt, ":").concat(range.endAt),
942
+ productId: product.id,
943
+ timezone: projection.meta.timezone
944
+ });
945
+ }))));
946
+ }
947
+ function dedupeProductRanges(ranges) {
948
+ var map = new Map();
949
+ ranges.forEach(function (range) {
950
+ var key = getProductTimeRangeIdentity(range);
951
+ if (!map.has(key)) {
952
+ map.set(key, _objectSpread(_objectSpread({}, range), {}, {
953
+ key: key
954
+ }));
955
+ return;
956
+ }
957
+ var existing = map.get(key);
958
+ existing.scheduleRefs = mergeScheduleRefs(existing.scheduleRefs, range.scheduleRefs || []);
959
+ if (!existing.primaryScheduleRef) existing.primaryScheduleRef = range.primaryScheduleRef;
960
+ });
961
+ return Array.from(map.values()).sort(function (left, right) {
962
+ return left.startAt.localeCompare(right.startAt);
510
963
  });
511
964
  }
512
- function buildResourceOptions(resources, cells, query) {
513
- var queryResources = getQueryResources(resources, query);
514
- return queryResources.map(function (resource) {
515
- var resourceCells = cells.filter(function (cell) {
516
- return cell.resourceId !== undefined && sameId(cell.resourceId, resource.id);
965
+ export function getProductTimeRanges(projection) {
966
+ var _extractFixedOffsetMi3, _request$productIds;
967
+ var request = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
968
+ var normalizedPartySize = normalizePartySize(request.partySize);
969
+ var dateRange = resolveProjectionDateRange(projection, request.dateRange);
970
+ var fixedOffsetMinutes = (_extractFixedOffsetMi3 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi3 !== void 0 ? _extractFixedOffsetMi3 : captureRuntimeOffsetMinutes();
971
+ var resourceWindowIndex = buildResourceWindowIndex(projection);
972
+ var filteredProductIdSet = (_request$productIds = request.productIds) !== null && _request$productIds !== void 0 && _request$productIds.length ? new Set(request.productIds.map(String)) : null;
973
+ return projection.products.filter(function (product) {
974
+ return !filteredProductIdSet || filteredProductIdSet.has(String(product.id));
975
+ }).map(function (product) {
976
+ if (product.kind === 'normal') {
977
+ return {
978
+ productId: product.id,
979
+ title: product.title,
980
+ kind: product.kind,
981
+ ranges: []
982
+ };
983
+ }
984
+ if (product.kind === 'session' || product.kind === 'venue_slot') {
985
+ return {
986
+ productId: product.id,
987
+ title: product.title,
988
+ kind: product.kind,
989
+ ranges: getSessionRangesForProduct(projection, product, dateRange)
990
+ };
991
+ }
992
+ var requiredGroups = (product.requirementGroups || []).filter(function (group) {
993
+ return group.required !== false;
994
+ });
995
+ var operatingRanges = buildOperatingRanges(projection, dateRange);
996
+ var resourceRanges = requiredGroups.length ? requiredGroups.map(function (group) {
997
+ return buildGroupWorkingRanges({
998
+ projection: projection,
999
+ product: product,
1000
+ requirementGroup: group,
1001
+ dateRange: dateRange,
1002
+ resourceIds: request.resourceIds,
1003
+ partySize: normalizedPartySize,
1004
+ resourceWindowIndex: resourceWindowIndex
1005
+ });
1006
+ }).reduce(function (current, ranges) {
1007
+ return current === null ? ranges : intersectRangeLists(current, ranges);
1008
+ }, null) || [] : null;
1009
+ var baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
1010
+ var durationMinutes = Math.max(1, Number(product.durationMinutes || 30) || 30);
1011
+ var timeSliceMinutes = projection.meta.defaults.slotStepMinutes;
1012
+ var durationAnchor = ceilToTenMinutes(toDateTime(projection.meta.durationCandidateAnchorAt, projection.meta.timezone), projection.meta.timezone);
1013
+ var ranges = [];
1014
+ baseRanges.forEach(function (range) {
1015
+ if (range.end <= durationAnchor) return;
1016
+ var cursor = range.start;
1017
+ if (cursor < durationAnchor) {
1018
+ cursor = durationAnchor;
1019
+ }
1020
+ while (true) {
1021
+ var bookingEnd = addMinutesZoned(cursor, durationMinutes, projection.meta.timezone);
1022
+ if (bookingEnd > range.end) break;
1023
+ var bookingDate = formatDate(cursor);
1024
+ if (bookingDate >= dateRange.startDate && bookingDate <= dateRange.endDate) {
1025
+ ranges.push({
1026
+ key: "".concat(String(product.id), ":").concat(cursor, ":").concat(bookingEnd),
1027
+ productId: product.id,
1028
+ source: 'duration_window',
1029
+ startAt: formatDateTime(cursor, projection.meta.timezone, fixedOffsetMinutes),
1030
+ endAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
1031
+ bookingStartAt: formatDateTime(cursor, projection.meta.timezone, fixedOffsetMinutes),
1032
+ bookingEndAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
1033
+ date: formatDate(cursor, projection.meta.timezone),
1034
+ startTime: formatTime(cursor, projection.meta.timezone),
1035
+ endTime: formatTime(bookingEnd, projection.meta.timezone),
1036
+ timezone: projection.meta.timezone,
1037
+ scheduleRefs: []
1038
+ });
1039
+ }
1040
+ cursor = addMinutesZoned(cursor, timeSliceMinutes, projection.meta.timezone);
1041
+ }
517
1042
  });
518
- var bestCell = resourceCells.find(function (cell) {
519
- return cell.status === 'available';
520
- }) || resourceCells.find(function (cell) {
521
- return cell.status === 'limited';
522
- }) || resourceCells[0];
523
1043
  return {
524
- resourceId: resource.id,
525
- resourceName: resource.name,
526
- resourceFormId: resource.formId,
527
- resourceType: resource.resourceType,
528
- status: resourceCells.length ? aggregateStatus(resourceCells) : 'unavailable',
529
- remainingCapacity: bestCell === null || bestCell === void 0 ? void 0 : bestCell.remainingCapacity,
530
- maxCapacity: (bestCell === null || bestCell === void 0 ? void 0 : bestCell.maxCapacity) || resource.capacity,
531
- reasonCodes: uniqueReasonCodes(resourceCells)
1044
+ productId: product.id,
1045
+ title: product.title,
1046
+ kind: product.kind,
1047
+ ranges: dedupeProductRanges(ranges)
532
1048
  };
533
1049
  });
534
1050
  }
535
- function buildDateSummary(cells, query) {
536
- var dates = query.date ? [query.date] : query.dateRange ? addDays(query.dateRange.start, query.dateRange.end) : Array.from(new Set(cells.map(function (cell) {
537
- return cell.date;
538
- }).filter(Boolean)));
539
- return dates.map(function (date) {
540
- var dateCells = cells.filter(function (cell) {
541
- return cell.date === date;
1051
+ function aggregateMatchedCells(productId, requirementGroupId, resourceId, candidateRange, indexedCells) {
1052
+ var _extractFixedOffsetMi4, _extractFixedOffsetMi5;
1053
+ var candidateStartMs = candidateRange.start;
1054
+ var candidateEndMs = candidateRange.end;
1055
+ var left = 0;
1056
+ var right = indexedCells.length;
1057
+ while (left < right) {
1058
+ var middle = Math.floor((left + right) / 2);
1059
+ if (indexedCells[middle].range.endMs <= candidateStartMs) {
1060
+ left = middle + 1;
1061
+ } else {
1062
+ right = middle;
1063
+ }
1064
+ }
1065
+ var overlapped = [];
1066
+ for (var index = Math.max(0, left - 1); index < indexedCells.length; index += 1) {
1067
+ var _indexedCells$index = indexedCells[index],
1068
+ cell = _indexedCells$index.cell,
1069
+ range = _indexedCells$index.range;
1070
+ if (range.startMs >= candidateEndMs) break;
1071
+ if (range.endMs <= candidateStartMs) continue;
1072
+ var clipped = clipRange(range, candidateRange);
1073
+ if (clipped) overlapped.push({
1074
+ cell: cell,
1075
+ range: clipped
542
1076
  });
543
- var availableCount = dateCells.filter(function (cell) {
544
- return cell.status === 'available' || cell.status === 'limited';
545
- }).length;
546
- return {
547
- date: date,
548
- status: dateCells.length ? aggregateStatus(dateCells) : 'unavailable',
549
- availableCount: availableCount,
550
- totalCount: dateCells.length
551
- };
1077
+ }
1078
+ if (!overlapped.length) return undefined;
1079
+ var cursor = candidateRange.start;
1080
+ for (var _i = 0, _overlapped = overlapped; _i < _overlapped.length; _i++) {
1081
+ var item = _overlapped[_i];
1082
+ if (item.range.start > cursor) return undefined;
1083
+ if (item.range.end > cursor) {
1084
+ cursor = item.range.end;
1085
+ }
1086
+ if (cursor >= candidateRange.end) break;
1087
+ }
1088
+ if (cursor < candidateRange.end) return undefined;
1089
+ var cells = overlapped.map(function (item) {
1090
+ return item.cell;
1091
+ });
1092
+ var statuses = cells.map(function (cell) {
1093
+ return cell.status;
1094
+ });
1095
+ var nonPastStatuses = statuses.filter(function (status) {
1096
+ return status !== 'past';
1097
+ });
1098
+ var aggregatedStatus = function () {
1099
+ if (nonPastStatuses.some(function (status) {
1100
+ return status === 'unavailable';
1101
+ })) return 'unavailable';
1102
+ if (nonPastStatuses.some(function (status) {
1103
+ return status === 'full';
1104
+ })) return 'full';
1105
+ if (nonPastStatuses.some(function (status) {
1106
+ return status === 'limited';
1107
+ })) return 'limited';
1108
+ if (nonPastStatuses.every(function (status) {
1109
+ return status === 'available';
1110
+ })) return 'available';
1111
+ if (statuses.every(function (status) {
1112
+ return status === 'past';
1113
+ })) return 'past';
1114
+ return 'available';
1115
+ }();
1116
+ var capacityCells = cells.filter(function (cell) {
1117
+ return cell.remainingCapacity !== undefined || cell.maxCapacity !== undefined;
1118
+ });
1119
+ var reasonCodes = Array.from(new Set(cells.flatMap(function (cell) {
1120
+ return cell.reasonCodes;
1121
+ })));
1122
+ var occupancyRefs = Array.from(new Map(cells.flatMap(function (cell) {
1123
+ return cell.occupancyRefs || [];
1124
+ }).map(function (ref) {
1125
+ var _ref$id, _ref$resourceId, _ref$serverEventId, _ref$bookingUid, _ref$productUid, _ref$scheduleEventId, _ref$source2;
1126
+ return [[(_ref$id = ref.id) !== null && _ref$id !== void 0 ? _ref$id : '', (_ref$resourceId = ref.resourceId) !== null && _ref$resourceId !== void 0 ? _ref$resourceId : '', (_ref$serverEventId = ref.serverEventId) !== null && _ref$serverEventId !== void 0 ? _ref$serverEventId : '', (_ref$bookingUid = ref.bookingUid) !== null && _ref$bookingUid !== void 0 ? _ref$bookingUid : '', (_ref$productUid = ref.productUid) !== null && _ref$productUid !== void 0 ? _ref$productUid : '', (_ref$scheduleEventId = ref.scheduleEventId) !== null && _ref$scheduleEventId !== void 0 ? _ref$scheduleEventId : '', (_ref$source2 = ref.source) !== null && _ref$source2 !== void 0 ? _ref$source2 : ''].join(':'), ref];
1127
+ })).values());
1128
+ var firstCell = cells[0];
1129
+ return _objectSpread(_objectSpread({}, firstCell), {}, {
1130
+ key: buildCellKey({
1131
+ productId: productId,
1132
+ groupId: requirementGroupId,
1133
+ resourceId: resourceId,
1134
+ start: candidateRange.start,
1135
+ end: candidateRange.end
1136
+ }),
1137
+ startAt: formatDateTime(candidateRange.start, firstCell.timezone, (_extractFixedOffsetMi4 = extractFixedOffsetMinutes(firstCell.startAt)) !== null && _extractFixedOffsetMi4 !== void 0 ? _extractFixedOffsetMi4 : captureRuntimeOffsetMinutes()),
1138
+ endAt: formatDateTime(candidateRange.end, firstCell.timezone, (_extractFixedOffsetMi5 = extractFixedOffsetMinutes(firstCell.startAt)) !== null && _extractFixedOffsetMi5 !== void 0 ? _extractFixedOffsetMi5 : captureRuntimeOffsetMinutes()),
1139
+ date: formatDate(candidateRange.start, firstCell.timezone),
1140
+ startTime: formatTime(candidateRange.start, firstCell.timezone),
1141
+ endTime: formatTime(candidateRange.end, firstCell.timezone),
1142
+ status: aggregatedStatus,
1143
+ remainingCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1144
+ var _cell$remainingCapaci;
1145
+ return (_cell$remainingCapaci = cell.remainingCapacity) !== null && _cell$remainingCapaci !== void 0 ? _cell$remainingCapaci : Number.MAX_SAFE_INTEGER;
1146
+ }))) : undefined,
1147
+ maxCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1148
+ var _cell$maxCapacity;
1149
+ return (_cell$maxCapacity = cell.maxCapacity) !== null && _cell$maxCapacity !== void 0 ? _cell$maxCapacity : Number.MAX_SAFE_INTEGER;
1150
+ }))) : undefined,
1151
+ reasonCodes: reasonCodes,
1152
+ occupancyRefs: occupancyRefs
552
1153
  });
553
1154
  }
554
- function buildTimeLabels(date, context, query) {
555
- if (!date) return [];
556
- var slotMinutes = query.slotDurationMinutes || context.slotDurationMinutes;
557
- var labels = [];
558
- var cursor = toDateTime(date, context.businessStartTime);
559
- var end = toDateTime(date, context.businessEndTime);
560
- if (!end.isAfter(cursor)) end = end.add(1, 'day');
561
- while (cursor.isBefore(end)) {
562
- labels.push(cursor.format('HH:mm'));
563
- cursor = cursor.add(slotMinutes, 'minute');
1155
+ function aggregateMatchedCellSummary(candidateRange, indexedCells) {
1156
+ var candidateStartMs = candidateRange.startMs;
1157
+ var candidateEndMs = candidateRange.endMs;
1158
+ var left = 0;
1159
+ var right = indexedCells.length;
1160
+ while (left < right) {
1161
+ var middle = Math.floor((left + right) / 2);
1162
+ if (indexedCells[middle].range.endMs <= candidateStartMs) {
1163
+ left = middle + 1;
1164
+ } else {
1165
+ right = middle;
1166
+ }
564
1167
  }
565
- return labels;
566
- }
567
- function buildGridSlotInterval(product, date, label, context, query) {
568
- var slotMinutes = product.kind === 'duration' ? Math.max(1, Number(product.durationMinutes || query.slotDurationMinutes || context.slotDurationMinutes)) : query.slotDurationMinutes || context.slotDurationMinutes;
569
- var start = toDateTime(date, label);
570
- var end = start.add(slotMinutes, 'minute');
1168
+ var cells = [];
1169
+ var cursorMs = candidateStartMs;
1170
+ for (var index = Math.max(0, left - 1); index < indexedCells.length; index += 1) {
1171
+ var _indexedCells$index2 = indexedCells[index],
1172
+ cell = _indexedCells$index2.cell,
1173
+ range = _indexedCells$index2.range;
1174
+ if (range.startMs >= candidateEndMs) break;
1175
+ if (range.endMs <= candidateStartMs) continue;
1176
+ var clippedStartMs = Math.max(range.startMs, candidateStartMs);
1177
+ var clippedEndMs = Math.min(range.endMs, candidateEndMs);
1178
+ if (clippedEndMs <= clippedStartMs) continue;
1179
+ if (clippedStartMs > cursorMs) return undefined;
1180
+ cells.push(cell);
1181
+ if (clippedEndMs > cursorMs) cursorMs = clippedEndMs;
1182
+ if (cursorMs >= candidateEndMs) break;
1183
+ }
1184
+ if (!cells.length || cursorMs < candidateEndMs) return undefined;
1185
+ var statuses = cells.map(function (cell) {
1186
+ return cell.status;
1187
+ });
1188
+ var nonPastStatuses = statuses.filter(function (status) {
1189
+ return status !== 'past';
1190
+ });
1191
+ var status = function () {
1192
+ if (nonPastStatuses.some(function (cellStatus) {
1193
+ return cellStatus === 'unavailable';
1194
+ })) return 'unavailable';
1195
+ if (nonPastStatuses.some(function (cellStatus) {
1196
+ return cellStatus === 'full';
1197
+ })) return 'full';
1198
+ if (nonPastStatuses.some(function (cellStatus) {
1199
+ return cellStatus === 'limited';
1200
+ })) return 'limited';
1201
+ if (nonPastStatuses.every(function (cellStatus) {
1202
+ return cellStatus === 'available';
1203
+ })) return 'available';
1204
+ if (statuses.every(function (cellStatus) {
1205
+ return cellStatus === 'past';
1206
+ })) return 'past';
1207
+ return 'available';
1208
+ }();
1209
+ var capacityCells = cells.filter(function (cell) {
1210
+ return cell.remainingCapacity !== undefined || cell.maxCapacity !== undefined;
1211
+ });
571
1212
  return {
572
- id: "".concat(product.id, ":").concat(date, ":").concat(label),
573
- productId: product.id,
574
- source: product.kind === 'session' ? 'session' : product.kind === 'venue_slot' ? 'venue_slot' : 'duration',
575
- date: date,
576
- start_at: start.toISOString(),
577
- end_at: end.toISOString(),
578
- durationMinutes: slotMinutes,
579
- quantity: Math.max(1, Number(product.quantity || 1)),
580
- capacityRequired: Math.max(1, Number(query.pax || product.capacityRequired || 1)),
581
- resourceRequirements: product.resourceRequirements || []
1213
+ status: status,
1214
+ remainingCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1215
+ var _cell$remainingCapaci2;
1216
+ return (_cell$remainingCapaci2 = cell.remainingCapacity) !== null && _cell$remainingCapaci2 !== void 0 ? _cell$remainingCapaci2 : Number.MAX_SAFE_INTEGER;
1217
+ }))) : undefined,
1218
+ maxCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1219
+ var _cell$maxCapacity2;
1220
+ return (_cell$maxCapacity2 = cell.maxCapacity) !== null && _cell$maxCapacity2 !== void 0 ? _cell$maxCapacity2 : Number.MAX_SAFE_INTEGER;
1221
+ }))) : undefined,
1222
+ reasonCodes: Array.from(new Set(cells.flatMap(function (cell) {
1223
+ return cell.reasonCodes;
1224
+ })))
582
1225
  };
583
1226
  }
584
- function buildTimeGrid(context, query, cells) {
585
- var _query$dateRange;
586
- var date = query.date || ((_query$dateRange = query.dateRange) === null || _query$dateRange === void 0 ? void 0 : _query$dateRange.start);
587
- var products = getQueryProducts(context.products, query).filter(function (product) {
588
- return product.kind !== 'normal';
1227
+ function buildAvailabilityCellIndex(projection) {
1228
+ var index = new Map();
1229
+ projection.cells.forEach(function (cell) {
1230
+ if (cell.requirementGroupId == null || cell.resourceId == null) return;
1231
+ var key = [String(cell.productId), cell.requirementGroupId, String(cell.resourceId)].join(':');
1232
+ var entries = index.get(key) || [];
1233
+ entries.push({
1234
+ cell: cell,
1235
+ range: toIndexedRange({
1236
+ start: toDateTime(cell.startAt, projection.meta.timezone),
1237
+ end: toDateTime(cell.endAt, projection.meta.timezone)
1238
+ })
1239
+ });
1240
+ index.set(key, entries);
589
1241
  });
590
- var resources = getQueryResources(context.resources, query);
591
- var timeLabels = buildTimeLabels(date, context, query);
592
- if (!date || !products.length || !resources.length) {
1242
+ index.forEach(function (entries, key) {
1243
+ index.set(key, entries.sort(function (left, right) {
1244
+ return left.range.startMs - right.range.startMs;
1245
+ }));
1246
+ });
1247
+ return index;
1248
+ }
1249
+ function buildEvaluationContext(params) {
1250
+ var _params$resourceIds2;
1251
+ var evaluatedAt = toDateTime(params.evaluatedAt || params.projection.meta.now, params.projection.meta.timezone);
1252
+ var selectionsByGroupKey = new Map();
1253
+ (params.requirementSelections || []).forEach(function (selection) {
1254
+ var _selection$formId;
1255
+ var key = selection.requirementGroupId || String((_selection$formId = selection.formId) !== null && _selection$formId !== void 0 ? _selection$formId : '');
1256
+ selectionsByGroupKey.set(key, selection);
1257
+ });
1258
+ return {
1259
+ evaluatedAt: evaluatedAt,
1260
+ evaluatedAtMs: evaluatedAt,
1261
+ resourceIdSet: (_params$resourceIds2 = params.resourceIds) !== null && _params$resourceIds2 !== void 0 && _params$resourceIds2.length ? new Set(params.resourceIds.map(String)) : null,
1262
+ selectionsByGroupKey: selectionsByGroupKey
1263
+ };
1264
+ }
1265
+ function summarizeStatuses(statuses) {
1266
+ if (!statuses.length) return 'unavailable';
1267
+ if (statuses.every(function (status) {
1268
+ return status === 'past';
1269
+ })) return 'past';
1270
+ if (statuses.some(function (status) {
1271
+ return status === 'unavailable';
1272
+ })) return 'unavailable';
1273
+ if (statuses.some(function (status) {
1274
+ return status === 'full';
1275
+ })) return 'full';
1276
+ if (statuses.some(function (status) {
1277
+ return status === 'limited';
1278
+ })) return 'limited';
1279
+ if (statuses.every(function (status) {
1280
+ return status === 'available';
1281
+ })) return 'available';
1282
+ if (statuses.some(function (status) {
1283
+ return status === 'past';
1284
+ })) return 'past';
1285
+ return statuses[0];
1286
+ }
1287
+ function createStatusAggregate() {
1288
+ return {
1289
+ count: 0,
1290
+ hasAvailable: false,
1291
+ hasLimited: false,
1292
+ hasFull: false,
1293
+ allPast: true
1294
+ };
1295
+ }
1296
+ function pushStatusAggregate(aggregate, status) {
1297
+ aggregate.count += 1;
1298
+ if (status !== 'past') aggregate.allPast = false;
1299
+ if (status === 'available') aggregate.hasAvailable = true;
1300
+ if (status === 'limited') aggregate.hasLimited = true;
1301
+ if (status === 'full') aggregate.hasFull = true;
1302
+ }
1303
+ function summarizeStatusAggregate(aggregate) {
1304
+ if (aggregate.count === 0) return 'unavailable';
1305
+ if (aggregate.hasAvailable) return 'available';
1306
+ if (aggregate.hasLimited) return 'limited';
1307
+ if (aggregate.hasFull) return 'full';
1308
+ if (aggregate.allPast) return 'past';
1309
+ return 'unavailable';
1310
+ }
1311
+ function evaluateMatchedResourceSummary(params) {
1312
+ var _ref6, _params$matchedCell$m;
1313
+ var isPast = params.candidateRange.endMs <= params.evaluationContext.evaluatedAtMs;
1314
+ if (!params.matchedCell) {
593
1315
  return {
594
- date: date,
595
- timeLabels: timeLabels,
596
- resources: []
1316
+ status: isPast ? 'past' : 'unavailable',
1317
+ maxPartySize: 0,
1318
+ remainingCapacity: 0,
1319
+ maxCapacity: 0,
1320
+ reasonCodes: isPast ? ['past'] : ['outside_resource_window']
597
1321
  };
598
1322
  }
599
- var rows = resources.map(function (resource) {
600
- var slots = timeLabels.map(function (label) {
601
- var product = products[0];
602
- var interval = buildGridSlotInterval(product, date, label, context, query);
603
- var start = dayjs(interval.start_at);
604
- var end = dayjs(interval.end_at);
605
- var cell = cells.find(function (item) {
606
- return sameId(item.productId, product.id) && sameId(item.resourceId, resource.id) && item.date === date && (item.status === 'available' || item.status === 'limited') && containsRange(dayjs(item.start_at), dayjs(item.end_at), start, end);
607
- });
608
- return {
609
- startTime: label,
610
- endTime: dayjs(interval.end_at).format('HH:mm'),
611
- status: (cell === null || cell === void 0 ? void 0 : cell.status) || 'unavailable',
612
- resourceId: resource.id,
613
- productId: product.id,
614
- remainingCapacity: cell === null || cell === void 0 ? void 0 : cell.remainingCapacity,
615
- maxCapacity: (cell === null || cell === void 0 ? void 0 : cell.maxCapacity) || resource.capacity,
616
- reasonCodes: (cell === null || cell === void 0 ? void 0 : cell.reasonCodes) || ['outside_available_range']
617
- };
618
- });
619
- return {
620
- resourceId: resource.id,
621
- resourceName: resource.name,
622
- resourceFormId: resource.formId,
623
- slots: slots
624
- };
625
- });
1323
+ var remainingCapacity = Math.max(0, params.matchedCell.remainingCapacity || 0);
1324
+ var maxCapacity = Math.max(0, (_ref6 = (_params$matchedCell$m = params.matchedCell.maxCapacity) !== null && _params$matchedCell$m !== void 0 ? _params$matchedCell$m : params.resourceCapacity) !== null && _ref6 !== void 0 ? _ref6 : 0);
1325
+ var maxPartySize = isPast ? 0 : remainingCapacity;
1326
+ var status = function () {
1327
+ if (isPast) return 'past';
1328
+ if (params.matchedCell.status === 'unavailable') return 'unavailable';
1329
+ if (remainingCapacity < params.requiredCapacity) return 'full';
1330
+ if (remainingCapacity < maxCapacity) return 'limited';
1331
+ return 'available';
1332
+ }();
626
1333
  return {
627
- date: date,
628
- timeLabels: timeLabels,
629
- resources: rows
1334
+ status: status,
1335
+ remainingCapacity: remainingCapacity,
1336
+ maxCapacity: maxCapacity,
1337
+ maxPartySize: maxPartySize,
1338
+ reasonCodes: isPast ? Array.from(new Set([].concat(_toConsumableArray(params.matchedCell.reasonCodes || []), ['past']))) : params.matchedCell.reasonCodes
630
1339
  };
631
1340
  }
632
- export function queryPlannerAvailability(contextInput) {
633
- var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
634
- var context = normalizePlannerContext(contextInput);
635
- var intervals = buildDemandIntervals(context.products, query, context);
636
- var resources = getQueryResources(context.resources, query);
637
- var eventsByResource = collectEventsByResource(resources, context.externalEvents);
638
- var cells = [];
639
- var _iterator4 = _createForOfIteratorHelper(intervals),
640
- _step4;
641
- try {
642
- var _loop2 = function _loop2() {
643
- var interval = _step4.value;
644
- var candidateResources = resources.filter(function (resource) {
645
- var requirements = interval.resourceRequirements || [];
646
- if (!requirements.length) return true;
647
- return requirements.some(function (requirement) {
648
- return matchesResourceRequirement(resource, requirement);
649
- });
650
- });
651
- if (!candidateResources.length) {
652
- cells.push({
653
- key: "".concat(interval.productId, ":missing-resource:").concat(interval.start_at),
654
- productId: interval.productId,
655
- date: interval.date,
656
- start_at: interval.start_at,
657
- end_at: interval.end_at,
658
- startTime: timeOf(interval.start_at),
659
- endTime: timeOf(interval.end_at),
660
- durationMinutes: interval.durationMinutes,
661
- cellType: 'unavailable',
662
- status: 'unavailable',
663
- reasonCodes: ['missing_resource'],
664
- conflicts: [{
665
- type: 'missing_resource',
666
- message: '没有匹配商品需求的资源',
667
- productId: interval.productId
668
- }]
669
- });
670
- return 1; // continue
671
- }
672
- candidateResources.forEach(function (resource) {
673
- if (interval.source === 'duration' && !query.startTime) {
674
- cells.push.apply(cells, _toConsumableArray(buildAvailabilityCellsForResource({
675
- resource: resource,
676
- interval: interval,
677
- events: eventsByResource.get(String(resource.id)) || []
678
- })));
679
- return;
680
- }
681
- cells.push(evaluateResourceForInterval({
682
- resource: resource,
683
- interval: interval,
684
- context: context
685
- }));
686
- });
687
- };
688
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
689
- if (_loop2()) continue;
1341
+ function evaluateRequirementGroupSummary(params) {
1342
+ var _requirementGroup$min2, _requirementGroup$max, _params$groupResource;
1343
+ var projection = params.projection,
1344
+ product = params.product,
1345
+ requirementGroup = params.requirementGroup,
1346
+ candidateRange = params.candidateRange,
1347
+ requestedPartySize = params.requestedPartySize,
1348
+ evaluationContext = params.evaluationContext;
1349
+ var required = requirementGroup.required !== false;
1350
+ var minimum = Math.max(required ? 1 : 0, Number((_requirementGroup$min2 = requirementGroup.min) !== null && _requirementGroup$min2 !== void 0 ? _requirementGroup$min2 : required ? 1 : 0) || 0);
1351
+ var maximum = Math.max(minimum, Number((_requirementGroup$max = requirementGroup.max) !== null && _requirementGroup$max !== void 0 ? _requirementGroup$max : Math.max(minimum, 1)) || Math.max(minimum, 1));
1352
+ var matchedResources = ((_params$groupResource = params.groupResourceIndex) === null || _params$groupResource === void 0 ? void 0 : _params$groupResource.get([String(product.id), requirementGroup.id].join(':'))) || projection.resources.filter(function (resource) {
1353
+ return matchesRequirementGroup(requirementGroup, resource, product, requestedPartySize !== null && requestedPartySize !== void 0 ? requestedPartySize : 1);
1354
+ }).filter(function (resource) {
1355
+ return !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id));
1356
+ });
1357
+ var requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
1358
+ var optionStatusAggregate = createStatusAggregate();
1359
+ var reasonCodeSet = new Set();
1360
+ var totalResourceCount = 0;
1361
+ var availableResourceCount = 0;
1362
+ var groupMaxPartySize = 0;
1363
+ var groupRemainingCapacity = 0;
1364
+ var groupMaxCapacity = 0;
1365
+ matchedResources.forEach(function (resource) {
1366
+ var _params$cellIndex;
1367
+ totalResourceCount += 1;
1368
+ var matchedCell = aggregateMatchedCellSummary(candidateRange, ((_params$cellIndex = params.cellIndex) === null || _params$cellIndex === void 0 ? void 0 : _params$cellIndex.get([String(product.id), requirementGroup.id, String(resource.id)].join(':'))) || []);
1369
+ var option = evaluateMatchedResourceSummary({
1370
+ matchedCell: matchedCell,
1371
+ resourceCapacity: resource.capacity,
1372
+ requiredCapacity: requiredCapacity,
1373
+ candidateRange: candidateRange,
1374
+ evaluationContext: evaluationContext
1375
+ });
1376
+ pushStatusAggregate(optionStatusAggregate, option.status);
1377
+ option.reasonCodes.forEach(function (reasonCode) {
1378
+ return reasonCodeSet.add(reasonCode);
1379
+ });
1380
+ groupRemainingCapacity = Math.max(groupRemainingCapacity, option.remainingCapacity);
1381
+ groupMaxCapacity = Math.max(groupMaxCapacity, option.maxCapacity);
1382
+ if (option.status === 'available' || option.status === 'limited') {
1383
+ availableResourceCount += 1;
1384
+ groupMaxPartySize = Math.max(groupMaxPartySize, option.maxPartySize);
690
1385
  }
691
- } catch (err) {
692
- _iterator4.e(err);
693
- } finally {
694
- _iterator4.f();
1386
+ });
1387
+ var status = 'available';
1388
+ if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
1389
+ status = 'past';
1390
+ } else if (availableResourceCount < minimum) {
1391
+ status = optionStatusAggregate.hasAvailable || optionStatusAggregate.hasLimited || optionStatusAggregate.hasFull ? 'full' : 'unavailable';
1392
+ } else if (availableResourceCount === totalResourceCount && !optionStatusAggregate.hasLimited && !optionStatusAggregate.hasFull && !optionStatusAggregate.allPast) {
1393
+ status = 'available';
1394
+ } else {
1395
+ status = 'limited';
695
1396
  }
696
1397
  return {
697
- query: query,
698
- demandIntervals: intervals,
699
- productOptions: buildProductOptions(context.products, cells, query),
700
- resourceOptions: buildResourceOptions(context.resources, cells, query),
701
- dateSummary: buildDateSummary(cells, query),
702
- timeGrid: buildTimeGrid(context, query, cells),
703
- cells: cells
1398
+ required: required,
1399
+ min: minimum,
1400
+ max: maximum,
1401
+ status: status,
1402
+ maxPartySize: groupMaxPartySize,
1403
+ remainingCapacity: groupRemainingCapacity,
1404
+ maxCapacity: groupMaxCapacity,
1405
+ reasonCodes: Array.from(reasonCodeSet)
704
1406
  };
705
1407
  }
706
- function getIntervalRequirements(interval) {
707
- return interval.resourceRequirements.length ? interval.resourceRequirements : [{
708
- capacityRequired: interval.capacityRequired,
709
- required: true
710
- }];
711
- }
712
- function buildAssignableCandidateGroups(params) {
713
- var interval = params.interval,
714
- cells = params.cells,
715
- resources = params.resources;
716
- var conflicts = [];
717
- var groups = getIntervalRequirements(interval).map(function (requirement) {
718
- var candidateCells = cells.filter(function (cell) {
719
- if (!sameId(cell.productId, interval.productId)) return false;
720
- if (cell.date !== interval.date) return false;
721
- if (cell.resourceId === undefined) return false;
722
- if (cell.status !== 'available' && cell.status !== 'limited') return false;
723
- var cellStart = dayjs(cell.start_at);
724
- var cellEnd = dayjs(cell.end_at);
725
- var intervalStart = dayjs(interval.start_at);
726
- var intervalEnd = dayjs(interval.end_at);
727
- return containsRange(cellStart, cellEnd, intervalStart, intervalEnd) || interval.source === 'duration';
1408
+ function evaluateCandidateSummary(projection, candidate, request) {
1409
+ var _request$productIndex, _request$candidateRan;
1410
+ var product = ((_request$productIndex = request.productIndex) === null || _request$productIndex === void 0 ? void 0 : _request$productIndex.get(String(candidate.productId))) || projection.products.find(function (item) {
1411
+ return sameId(item.id, candidate.productId);
1412
+ });
1413
+ if (!product) {
1414
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate.productId 不存在于 projection.products', {
1415
+ candidate: candidate
728
1416
  });
729
- return candidateCells.flatMap(function (cell) {
730
- var resource = resources.find(function (item) {
731
- return sameId(item.id, cell.resourceId);
732
- });
733
- if (!resource || !matchesResourceRequirement(resource, requirement)) return [];
734
- return [{
735
- requirement: requirement,
736
- resource: resource,
737
- cell: cell
738
- }];
1417
+ }
1418
+ var candidateRangeCacheKey = getCandidateRangeCacheKey(candidate);
1419
+ var candidateRange = ((_request$candidateRan = request.candidateRangeCache) === null || _request$candidateRan === void 0 ? void 0 : _request$candidateRan.get(candidateRangeCacheKey)) || function () {
1420
+ var normalized = normalizeAbsoluteRange(getCandidateBookingRange(candidate), projection.meta.timezone);
1421
+ return normalized ? toIndexedRange(normalized) : null;
1422
+ }();
1423
+ if (!candidateRange) {
1424
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate 时间范围非法', {
1425
+ candidate: candidate
739
1426
  });
1427
+ }
1428
+ if (request.candidateRangeCache && !request.candidateRangeCache.has(candidateRangeCacheKey)) {
1429
+ request.candidateRangeCache.set(candidateRangeCacheKey, candidateRange);
1430
+ }
1431
+ var evaluationContext = request.evaluationContext || buildEvaluationContext({
1432
+ projection: projection,
1433
+ evaluatedAt: request.evaluatedAt,
1434
+ resourceIds: request.resourceIds
740
1435
  });
741
- groups.forEach(function (group, index) {
742
- var _interval$resourceReq;
743
- if (group.length) return;
744
- conflicts.push({
745
- type: 'missing_resource',
746
- message: '没有匹配商品需求的可用资源',
747
- productId: interval.productId,
748
- resourceId: (_interval$resourceReq = interval.resourceRequirements[index]) === null || _interval$resourceReq === void 0 || (_interval$resourceReq = _interval$resourceReq.resourceIds) === null || _interval$resourceReq === void 0 ? void 0 : _interval$resourceReq[0]
1436
+ var requestedPartySize = normalizePartySize(request.partySize);
1437
+ var requiredGroups = (product.requirementGroups || []).filter(function (group) {
1438
+ return group.required !== false;
1439
+ }).map(function (requirementGroup) {
1440
+ return evaluateRequirementGroupSummary({
1441
+ projection: projection,
1442
+ product: product,
1443
+ requirementGroup: requirementGroup,
1444
+ candidateRange: candidateRange,
1445
+ requestedPartySize: requestedPartySize,
1446
+ evaluationContext: evaluationContext,
1447
+ cellIndex: request.cellIndex,
1448
+ groupResourceIndex: request.groupResourceIndex
749
1449
  });
750
1450
  });
1451
+ var gate = evaluateCandidateTimeGate({
1452
+ product: product,
1453
+ candidateRange: candidateRange,
1454
+ evaluationContext: evaluationContext,
1455
+ timezoneName: projection.meta.timezone
1456
+ });
1457
+ var candidateStatus = requiredGroups.length ? summarizeStatuses(requiredGroups.map(function (group) {
1458
+ return group.status;
1459
+ })) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ? 'past' : 'available';
1460
+ var maxPartySize = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1461
+ return Math.min(min, group.maxPartySize);
1462
+ }, Number.MAX_SAFE_INTEGER) : 0;
1463
+ var remainingCapacity = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1464
+ return Math.min(min, group.remainingCapacity);
1465
+ }, Number.MAX_SAFE_INTEGER) : 0;
1466
+ var maxCapacity = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1467
+ return Math.min(min, group.maxCapacity);
1468
+ }, Number.MAX_SAFE_INTEGER) : 0;
1469
+ var status = (gate === null || gate === void 0 ? void 0 : gate.status) || candidateStatus;
751
1470
  return {
752
- groups: groups,
753
- conflicts: conflicts
1471
+ status: status,
1472
+ maxPartySize: gate ? 0 : Number.isFinite(maxPartySize) ? maxPartySize : 0,
1473
+ remainingCapacity: status === 'past' || status === 'unavailable' ? 0 : Number.isFinite(remainingCapacity) ? remainingCapacity : 0,
1474
+ maxCapacity: Number.isFinite(maxCapacity) ? maxCapacity : 0,
1475
+ reasonCodes: (gate === null || gate === void 0 ? void 0 : gate.reasonCodes) || Array.from(new Set(requiredGroups.flatMap(function (group) {
1476
+ return group.reasonCodes;
1477
+ })))
754
1478
  };
755
1479
  }
756
- function combineCandidateGroups(groups) {
757
- if (!groups.length || groups.some(function (group) {
758
- return group.length === 0;
759
- })) return [];
760
- return groups.reduce(function (combinations, group) {
761
- return combinations.flatMap(function (combination) {
762
- return group.map(function (candidate) {
763
- return [].concat(_toConsumableArray(combination), [candidate]);
1480
+ function evaluateCandidate(projection, candidate, request) {
1481
+ var _request$productIndex2, _request$candidateRan2;
1482
+ var product = ((_request$productIndex2 = request.productIndex) === null || _request$productIndex2 === void 0 ? void 0 : _request$productIndex2.get(String(candidate.productId))) || projection.products.find(function (item) {
1483
+ return sameId(item.id, candidate.productId);
1484
+ });
1485
+ if (!product) {
1486
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate.productId 不存在于 projection.products', {
1487
+ candidate: candidate
1488
+ });
1489
+ }
1490
+ var candidateRangeCacheKey = getCandidateRangeCacheKey(candidate);
1491
+ var candidateRange = ((_request$candidateRan2 = request.candidateRangeCache) === null || _request$candidateRan2 === void 0 ? void 0 : _request$candidateRan2.get(candidateRangeCacheKey)) || function () {
1492
+ var normalized = normalizeAbsoluteRange(getCandidateBookingRange(candidate), projection.meta.timezone);
1493
+ return normalized ? toIndexedRange(normalized) : null;
1494
+ }();
1495
+ if (!candidateRange) {
1496
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate 时间范围非法', {
1497
+ candidate: candidate
1498
+ });
1499
+ }
1500
+ if (request.candidateRangeCache && !request.candidateRangeCache.has(candidateRangeCacheKey)) {
1501
+ request.candidateRangeCache.set(candidateRangeCacheKey, candidateRange);
1502
+ }
1503
+ var evaluationContext = request.evaluationContext || buildEvaluationContext({
1504
+ projection: projection,
1505
+ evaluatedAt: request.evaluatedAt,
1506
+ resourceIds: request.resourceIds,
1507
+ requirementSelections: request.requirementSelections
1508
+ });
1509
+ var requestedPartySize = normalizePartySize(request.partySize);
1510
+ var partySize = requestedPartySize !== null && requestedPartySize !== void 0 ? requestedPartySize : 1;
1511
+ var selectionsByGroupKey = evaluationContext.selectionsByGroupKey;
1512
+ var requirementGroups = (product.requirementGroups || []).map(function (requirementGroup) {
1513
+ var _requirementGroup$min3, _requirementGroup$max2, _requirementGroup$for2, _request$groupResourc;
1514
+ var required = requirementGroup.required !== false;
1515
+ var minimum = Math.max(required ? 1 : 0, Number((_requirementGroup$min3 = requirementGroup.min) !== null && _requirementGroup$min3 !== void 0 ? _requirementGroup$min3 : required ? 1 : 0) || 0);
1516
+ var maximum = Math.max(minimum, Number((_requirementGroup$max2 = requirementGroup.max) !== null && _requirementGroup$max2 !== void 0 ? _requirementGroup$max2 : Math.max(minimum, 1)) || Math.max(minimum, 1));
1517
+ var selection = selectionsByGroupKey.get(requirementGroup.id) || selectionsByGroupKey.get(String((_requirementGroup$for2 = requirementGroup.formId) !== null && _requirementGroup$for2 !== void 0 ? _requirementGroup$for2 : ''));
1518
+ var matchedResources = (((_request$groupResourc = request.groupResourceIndex) === null || _request$groupResourc === void 0 ? void 0 : _request$groupResourc.get([String(product.id), requirementGroup.id].join(':'))) || projection.resources.filter(function (resource) {
1519
+ return matchesRequirementGroup(requirementGroup, resource, product, partySize);
1520
+ }).filter(function (resource) {
1521
+ return !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id));
1522
+ })).filter(function (resource) {
1523
+ return !selection || selection.resourceIds.some(function (resourceId) {
1524
+ return sameId(resourceId, resource.id);
764
1525
  });
765
1526
  });
766
- }, [[]]);
767
- }
768
- function buildSlotFromCandidates(params) {
769
- var interval = params.interval,
770
- candidates = params.candidates,
771
- start = params.start,
772
- end = params.end;
773
- if (!end.isAfter(start)) return null;
774
- var status = candidates.some(function (candidate) {
775
- return candidate.cell.status === 'limited';
776
- }) ? 'limited' : 'available';
777
- var assignments = candidates.map(function (candidate) {
1527
+ var options = matchedResources.map(function (resource) {
1528
+ var _request$cellIndex;
1529
+ var matchedCell = aggregateMatchedCells(product.id, requirementGroup.id, resource.id, candidateRange, ((_request$cellIndex = request.cellIndex) === null || _request$cellIndex === void 0 ? void 0 : _request$cellIndex.get([String(product.id), requirementGroup.id, String(resource.id)].join(':'))) || []);
1530
+ if (!matchedCell) {
1531
+ var _isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
1532
+ return {
1533
+ resourceId: resource.id,
1534
+ resourceName: resource.name,
1535
+ resourceFormId: resource.formId,
1536
+ resourceType: resource.resourceType,
1537
+ status: _isPast ? 'past' : 'unavailable',
1538
+ maxPartySize: 0,
1539
+ reasonCodes: _isPast ? ['past'] : ['outside_resource_window']
1540
+ };
1541
+ }
1542
+ var remainingCapacity = matchedCell.remainingCapacity;
1543
+ var maxCapacity = matchedCell.maxCapacity;
1544
+ var isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
1545
+ var maxPartySize = isPast ? 0 : Math.max(0, remainingCapacity || 0);
1546
+ var requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
1547
+ var status = function () {
1548
+ if (isPast) return 'past';
1549
+ if (matchedCell.status === 'unavailable') return 'unavailable';
1550
+ if ((remainingCapacity || 0) < requiredCapacity) return 'full';
1551
+ if ((remainingCapacity || 0) < (maxCapacity || resource.capacity)) return 'limited';
1552
+ return 'available';
1553
+ }();
1554
+ return {
1555
+ resourceId: resource.id,
1556
+ resourceName: resource.name,
1557
+ resourceFormId: resource.formId,
1558
+ resourceType: resource.resourceType,
1559
+ status: status,
1560
+ remainingCapacity: remainingCapacity,
1561
+ maxCapacity: maxCapacity,
1562
+ maxPartySize: maxPartySize,
1563
+ reasonCodes: isPast ? Array.from(new Set([].concat(_toConsumableArray(matchedCell.reasonCodes), ['past']))) : matchedCell.reasonCodes,
1564
+ matchedCell: matchedCell
1565
+ };
1566
+ });
1567
+ var availableOptions = options.filter(function (option) {
1568
+ return option.status === 'available' || option.status === 'limited';
1569
+ });
1570
+ var totalResourceCount = options.length;
1571
+ var availableResourceCount = availableOptions.length;
1572
+ var groupMaxPartySize = availableOptions.reduce(function (max, option) {
1573
+ return Math.max(max, option.maxPartySize);
1574
+ }, 0);
1575
+ var status = 'available';
1576
+ if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
1577
+ status = 'past';
1578
+ } else if (availableResourceCount < minimum) {
1579
+ status = options.some(function (option) {
1580
+ return option.status === 'full' || option.status === 'limited' || option.status === 'available';
1581
+ }) ? 'full' : 'unavailable';
1582
+ } else if (options.every(function (option) {
1583
+ return option.status === 'available';
1584
+ })) {
1585
+ status = 'available';
1586
+ } else {
1587
+ status = 'limited';
1588
+ }
778
1589
  return {
779
- productId: interval.productId,
780
- resourceId: candidate.resource.id,
781
- start_at: start.toISOString(),
782
- end_at: end.toISOString(),
783
- capacityRequired: Math.max(1, Number(candidate.requirement.capacityRequired || interval.capacityRequired || 1))
1590
+ groupId: requirementGroup.id,
1591
+ formId: requirementGroup.formId,
1592
+ title: requirementGroup.title,
1593
+ code: requirementGroup.code,
1594
+ resourceType: requirementGroup.resourceType,
1595
+ required: required,
1596
+ min: minimum,
1597
+ max: maximum,
1598
+ status: status,
1599
+ availableResourceCount: availableResourceCount,
1600
+ totalResourceCount: totalResourceCount,
1601
+ maxPartySize: groupMaxPartySize,
1602
+ remainingCapacity: Math.max.apply(Math, _toConsumableArray(availableOptions.map(function (option) {
1603
+ return option.remainingCapacity || 0;
1604
+ })).concat([0])),
1605
+ maxCapacity: Math.max.apply(Math, _toConsumableArray(availableOptions.map(function (option) {
1606
+ return option.maxCapacity || 0;
1607
+ })).concat([0])),
1608
+ reasonCodes: Array.from(new Set(options.flatMap(function (option) {
1609
+ return option.reasonCodes;
1610
+ }))),
1611
+ options: options
784
1612
  };
785
1613
  });
786
- return {
787
- key: [interval.productId, start.toISOString(), end.toISOString()].concat(_toConsumableArray(candidates.map(function (candidate) {
788
- return candidate.resource.id;
789
- }))).join(':'),
790
- productId: interval.productId,
791
- date: interval.date,
792
- start_at: start.toISOString(),
793
- end_at: end.toISOString(),
794
- startTime: start.format('HH:mm'),
795
- endTime: end.format('HH:mm'),
796
- durationMinutes: Math.max(1, end.diff(start, 'minute')),
797
- status: status,
798
- resources: candidates.map(function (candidate) {
1614
+ var requiredGroups = requirementGroups.filter(function (group) {
1615
+ return group.required;
1616
+ });
1617
+ var requiredStatuses = requiredGroups.map(function (group) {
1618
+ return group.status;
1619
+ });
1620
+ var gate = evaluateCandidateTimeGate({
1621
+ product: product,
1622
+ candidateRange: candidateRange,
1623
+ evaluationContext: evaluationContext,
1624
+ timezoneName: projection.meta.timezone
1625
+ });
1626
+ var candidateStatus = requiredGroups.length ? summarizeStatuses(requiredStatuses) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ? 'past' : 'available';
1627
+ var maxPartySize = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1628
+ return Math.min(min, group.maxPartySize);
1629
+ }, Number.MAX_SAFE_INTEGER) : 0;
1630
+ var selectedAssignments = requirementGroups.flatMap(function (group) {
1631
+ var _group$formId;
1632
+ var selection = selectionsByGroupKey.get(group.groupId) || selectionsByGroupKey.get(String((_group$formId = group.formId) !== null && _group$formId !== void 0 ? _group$formId : ''));
1633
+ if (!selection && !(request.allowImplicitSelection && group.required)) return [];
1634
+ var availableOptions = group.options.filter(function (option) {
1635
+ return option.status === 'available' || option.status === 'limited';
1636
+ });
1637
+ var selectedOptions = selection ? availableOptions.slice(0, group.max) : availableOptions.slice(0, Math.max(group.min, 1));
1638
+ var originalGroup = (product.requirementGroups || []).find(function (item) {
1639
+ return item.id === group.groupId;
1640
+ }) || {
1641
+ id: group.groupId,
1642
+ resourceType: group.resourceType,
1643
+ min: group.min,
1644
+ max: group.max
1645
+ };
1646
+ return selectedOptions.map(function (selectedOption) {
1647
+ var _candidate$primarySch, _candidate$primarySch2;
799
1648
  return {
800
- resourceId: candidate.resource.id,
801
- resourceName: candidate.resource.name,
802
- resourceFormId: candidate.resource.formId,
803
- resourceType: candidate.resource.resourceType,
804
- capacityRequired: Math.max(1, Number(candidate.requirement.capacityRequired || interval.capacityRequired || 1)),
805
- remainingCapacity: candidate.cell.remainingCapacity,
806
- maxCapacity: candidate.cell.maxCapacity
1649
+ productId: product.id,
1650
+ resourceId: selectedOption.resourceId,
1651
+ formId: group.formId,
1652
+ startAt: candidate.bookingStartAt || candidate.startAt,
1653
+ endAt: candidate.bookingEndAt || candidate.endAt,
1654
+ capacityRequired: resolveRequiredCapacity(originalGroup, product, requestedPartySize),
1655
+ scheduleId: (_candidate$primarySch = candidate.primaryScheduleRef) === null || _candidate$primarySch === void 0 ? void 0 : _candidate$primarySch.scheduleId,
1656
+ timeSlotId: (_candidate$primarySch2 = candidate.primaryScheduleRef) === null || _candidate$primarySch2 === void 0 ? void 0 : _candidate$primarySch2.timeSlotId
807
1657
  };
1658
+ });
1659
+ });
1660
+ return {
1661
+ item: _objectSpread(_objectSpread({}, candidate), {}, {
1662
+ status: (gate === null || gate === void 0 ? void 0 : gate.status) || candidateStatus,
1663
+ maxPartySize: gate ? 0 : Number.isFinite(maxPartySize) ? maxPartySize : 0,
1664
+ reasonCodes: (gate === null || gate === void 0 ? void 0 : gate.reasonCodes) || Array.from(new Set(requiredGroups.flatMap(function (group) {
1665
+ return group.reasonCodes;
1666
+ }))),
1667
+ requirementGroups: requirementGroups
808
1668
  }),
809
- assignments: assignments,
810
- conflicts: []
1669
+ assignments: gate ? [] : selectedAssignments,
1670
+ gate: gate || undefined
811
1671
  };
812
1672
  }
813
- function buildSlotsForInterval(params) {
814
- var interval = params.interval,
815
- candidates = params.candidates,
816
- query = params.query,
817
- context = params.context;
818
- var start = candidates.reduce(function (current, candidate) {
819
- return maxDayjs(current, dayjs(candidate.cell.start_at));
820
- }, dayjs(interval.start_at));
821
- var end = candidates.reduce(function (current, candidate) {
822
- return minDayjs(current, dayjs(candidate.cell.end_at));
823
- }, dayjs(interval.end_at));
824
- var durationMinutes = Math.max(1, Number(interval.durationMinutes || 1));
825
- if (interval.source !== 'duration' || query.startTime) {
826
- var fixedStart = dayjs(interval.start_at);
827
- var fixedEnd = dayjs(interval.end_at);
828
- if (containsRange(start, end, fixedStart, fixedEnd) || candidates.every(function (candidate) {
829
- return candidate.cell.cellType === 'fixed_interval';
830
- })) {
831
- var slot = buildSlotFromCandidates({
832
- interval: interval,
833
- candidates: candidates,
834
- start: fixedStart,
835
- end: fixedEnd
1673
+ function assertTimeCandidates(request) {
1674
+ if (request.target === 'time' && (!Array.isArray(request.candidates) || request.candidates.length === 0)) {
1675
+ throw availabilityError('MISSING_TIME_CANDIDATES', 'target=time 必须传非空 candidates', {
1676
+ request: request
1677
+ });
1678
+ }
1679
+ }
1680
+ function assertSelectedRanges(request) {
1681
+ if (request.target === 'resource' && (!Array.isArray(request.selectedRanges) || request.selectedRanges.length === 0)) {
1682
+ throw availabilityError('MISSING_SELECTED_RANGE', 'target=resource 必须传非空 selectedRanges', {
1683
+ request: request
1684
+ });
1685
+ }
1686
+ }
1687
+ function assertRangesWithinProjection(projection, ranges) {
1688
+ var coverage = {
1689
+ start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
1690
+ end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
1691
+ };
1692
+ ranges.forEach(function (range) {
1693
+ var displayRange = normalizeAbsoluteRange(range, projection.meta.timezone);
1694
+ var bookingRange = normalizeAbsoluteRange(getCandidateBookingRange(range), projection.meta.timezone);
1695
+ if (!displayRange || !bookingRange) {
1696
+ throw availabilityError('INVALID_TARGET_FILTER', '候选时间范围非法', {
1697
+ range: range
1698
+ });
1699
+ }
1700
+ if (!contains(coverage, displayRange) || !contains(coverage, bookingRange)) {
1701
+ throw availabilityError('OUT_OF_PROJECTION_COVERAGE', '候选时间范围超出 projection.coverage', {
1702
+ range: range,
1703
+ coverage: projection.meta.coverage
836
1704
  });
837
- return slot ? [slot] : [];
838
1705
  }
839
- return [];
1706
+ });
1707
+ }
1708
+ export function queryAvailabilityProjection(projection, request) {
1709
+ var _extractFixedOffsetMi6, _request$productIds2;
1710
+ var normalizedPartySize = normalizePartySize(request.partySize);
1711
+ var evaluatedAt = formatDateTime(localDateToScalar(new Date()), projection.meta.timezone, (_extractFixedOffsetMi6 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi6 !== void 0 ? _extractFixedOffsetMi6 : captureRuntimeOffsetMinutes());
1712
+ if (request.target === 'time') {
1713
+ assertTimeCandidates(request);
1714
+ assertRangesWithinProjection(projection, request.candidates);
1715
+ }
1716
+ if (request.target === 'resource') {
1717
+ assertSelectedRanges(request);
1718
+ assertRangesWithinProjection(projection, request.selectedRanges);
840
1719
  }
841
- var stepMinutes = Math.max(1, Number(query.slotDurationMinutes || context.slotDurationMinutes || 30));
842
- var slots = [];
843
- var cursor = start;
844
- while (!cursor.add(durationMinutes, 'minute').isAfter(end)) {
845
- var slotEnd = cursor.add(durationMinutes, 'minute');
846
- var _slot = buildSlotFromCandidates({
847
- interval: interval,
848
- candidates: candidates,
849
- start: cursor,
850
- end: slotEnd
1720
+ var productIdSet = (_request$productIds2 = request.productIds) !== null && _request$productIds2 !== void 0 && _request$productIds2.length ? new Set(request.productIds.map(String)) : null;
1721
+ var productIndex = buildProductIndex(projection);
1722
+ var cellIndex = buildAvailabilityCellIndex(projection);
1723
+ var groupResourceIndex = buildGroupResourceIndex({
1724
+ projection: projection,
1725
+ resourceIds: request.resourceIds,
1726
+ partySize: normalizedPartySize
1727
+ });
1728
+ var candidateRangeCache = new Map();
1729
+ var evaluationContext = buildEvaluationContext({
1730
+ projection: projection,
1731
+ evaluatedAt: evaluatedAt,
1732
+ resourceIds: request.resourceIds
1733
+ });
1734
+ if (request.target === 'time') {
1735
+ var candidates = dedupeProductRanges(request.candidates);
1736
+ var candidatesByProduct = new Map();
1737
+ candidates.forEach(function (candidate) {
1738
+ var key = String(candidate.productId);
1739
+ candidatesByProduct.set(key, [].concat(_toConsumableArray(candidatesByProduct.get(key) || []), [candidate]));
1740
+ });
1741
+ var _data = projection.products.filter(function (product) {
1742
+ return !productIdSet || productIdSet.has(String(product.id));
1743
+ }).map(function (product) {
1744
+ return {
1745
+ productId: product.id,
1746
+ title: product.title,
1747
+ times: (candidatesByProduct.get(String(product.id)) || []).map(function (candidate) {
1748
+ return evaluateCandidate(projection, candidate, {
1749
+ partySize: normalizedPartySize,
1750
+ resourceIds: request.resourceIds,
1751
+ evaluatedAt: evaluatedAt,
1752
+ cellIndex: cellIndex,
1753
+ productIndex: productIndex,
1754
+ groupResourceIndex: groupResourceIndex,
1755
+ candidateRangeCache: candidateRangeCache,
1756
+ evaluationContext: evaluationContext
1757
+ }).item;
1758
+ })
1759
+ };
1760
+ }).filter(function (group) {
1761
+ return group.times.length > 0;
851
1762
  });
852
- if (_slot) slots.push(_slot);
853
- cursor = cursor.add(stepMinutes, 'minute');
1763
+ return {
1764
+ target: 'time',
1765
+ evaluatedAt: evaluatedAt,
1766
+ snapshotId: projection.meta.snapshotId,
1767
+ data: _data
1768
+ };
854
1769
  }
855
- return slots;
856
- }
857
- export function resolveAssignableSlots(contextInput) {
858
- var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
859
- var context = normalizePlannerContext(contextInput);
860
- var projection = queryPlannerAvailability(context, query);
861
- var resources = getQueryResources(context.resources, query);
862
- var slotsByKey = new Map();
863
- var conflicts = [];
864
- projection.demandIntervals.forEach(function (interval) {
865
- var _buildAssignableCandi = buildAssignableCandidateGroups({
866
- interval: interval,
867
- cells: projection.cells,
868
- resources: resources
869
- }),
870
- groups = _buildAssignableCandi.groups,
871
- groupConflicts = _buildAssignableCandi.conflicts;
872
- conflicts.push.apply(conflicts, _toConsumableArray(groupConflicts));
873
- combineCandidateGroups(groups).forEach(function (candidates) {
874
- buildSlotsForInterval({
875
- interval: interval,
876
- candidates: candidates,
877
- query: query,
878
- context: context
879
- }).forEach(function (slot) {
880
- if (!slotsByKey.has(slot.key)) slotsByKey.set(slot.key, slot);
881
- });
1770
+ if (request.target === 'resource') {
1771
+ var selectedRanges = dedupeProductRanges(request.selectedRanges);
1772
+ var rangesByProduct = new Map();
1773
+ selectedRanges.forEach(function (candidate) {
1774
+ var key = String(candidate.productId);
1775
+ rangesByProduct.set(key, [].concat(_toConsumableArray(rangesByProduct.get(key) || []), [candidate]));
1776
+ });
1777
+ var _data2 = projection.products.filter(function (product) {
1778
+ return !productIdSet || productIdSet.has(String(product.id));
1779
+ }).map(function (product) {
1780
+ return {
1781
+ productId: product.id,
1782
+ title: product.title,
1783
+ ranges: (rangesByProduct.get(String(product.id)) || []).map(function (candidate) {
1784
+ var evaluated = evaluateCandidate(projection, candidate, {
1785
+ partySize: normalizedPartySize,
1786
+ resourceIds: request.resourceIds,
1787
+ evaluatedAt: evaluatedAt,
1788
+ cellIndex: cellIndex,
1789
+ productIndex: productIndex,
1790
+ groupResourceIndex: groupResourceIndex,
1791
+ candidateRangeCache: candidateRangeCache,
1792
+ evaluationContext: evaluationContext
1793
+ }).item;
1794
+ return {
1795
+ range: candidate,
1796
+ status: evaluated.status,
1797
+ maxPartySize: evaluated.maxPartySize,
1798
+ reasonCodes: evaluated.reasonCodes,
1799
+ requirementGroups: evaluated.requirementGroups
1800
+ };
1801
+ })
1802
+ };
1803
+ }).filter(function (group) {
1804
+ return group.ranges.length > 0;
882
1805
  });
1806
+ return {
1807
+ target: 'resource',
1808
+ evaluatedAt: evaluatedAt,
1809
+ snapshotId: projection.meta.snapshotId,
1810
+ data: _data2
1811
+ };
1812
+ }
1813
+ var dateRange = resolveProjectionDateRange(projection, request.dateRange);
1814
+ var candidateGroups = getProductTimeRanges(projection, {
1815
+ productIds: request.productIds,
1816
+ dateRange: dateRange,
1817
+ resourceIds: request.resourceIds,
1818
+ partySize: normalizedPartySize
883
1819
  });
884
- return {
885
- projection: projection,
886
- slots: Array.from(slotsByKey.values()),
887
- conflicts: conflicts
888
- };
889
- }
890
- export function autoAssignResources(contextInput) {
891
- var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
892
- var baseContext = normalizePlannerContext(contextInput);
893
- var projection = queryPlannerAvailability(baseContext, query);
894
- var assignments = [];
895
- var conflicts = [];
896
- var missingTimeProductIds = new Set();
897
- var _iterator5 = _createForOfIteratorHelper(projection.demandIntervals),
898
- _step5;
899
- try {
900
- var _loop3 = function _loop3() {
901
- var interval = _step5.value;
902
- if (interval.source === 'duration' && !query.startTime) {
903
- var productKey = String(interval.productId);
904
- if (!missingTimeProductIds.has(productKey)) {
905
- missingTimeProductIds.add(productKey);
906
- conflicts.push({
907
- type: 'missing_time',
908
- message: 'duration 商品需要先确定开始时间后才能自动分配资源',
909
- productId: interval.productId
910
- });
911
- }
912
- return 0; // continue
913
- }
914
- var contextWithAssignments = withAssignmentEvents(baseContext, assignments);
915
- var candidateResources = getQueryResources(contextWithAssignments.resources, query).filter(function (resource) {
916
- var requirements = interval.resourceRequirements || [];
917
- if (!requirements.length) return true;
918
- return requirements.some(function (requirement) {
919
- return matchesResourceRequirement(resource, requirement);
920
- });
921
- });
922
- var availableCell = candidateResources.map(function (resource) {
923
- return evaluateResourceForInterval({
924
- resource: resource,
925
- interval: interval,
926
- context: contextWithAssignments
927
- });
928
- }).find(function (cell) {
929
- return cell.status === 'available' || cell.status === 'limited';
1820
+ var candidateGroupMap = new Map(candidateGroups.map(function (group) {
1821
+ return [String(group.productId), group];
1822
+ }));
1823
+ if (request.target === 'date') {
1824
+ var _data3 = projection.products.filter(function (product) {
1825
+ return !productIdSet || productIdSet.has(String(product.id));
1826
+ }).map(function (product) {
1827
+ var group = candidateGroupMap.get(String(product.id)) || {
1828
+ productId: product.id,
1829
+ title: product.title,
1830
+ kind: product.kind,
1831
+ ranges: []
1832
+ };
1833
+ var byDate = new Map();
1834
+ group.ranges.forEach(function (candidate) {
1835
+ var evaluated = evaluateCandidateSummary(projection, candidate, {
1836
+ partySize: normalizedPartySize,
1837
+ resourceIds: request.resourceIds,
1838
+ evaluatedAt: evaluatedAt,
1839
+ cellIndex: cellIndex,
1840
+ productIndex: productIndex,
1841
+ groupResourceIndex: groupResourceIndex,
1842
+ candidateRangeCache: candidateRangeCache,
1843
+ evaluationContext: evaluationContext
930
1844
  });
931
- if (!(availableCell !== null && availableCell !== void 0 && availableCell.resourceId)) {
932
- conflicts.push({
933
- type: 'missing_resource',
934
- message: '无法为商品自动分配可用资源',
935
- productId: interval.productId
936
- });
937
- return 0; // continue
1845
+ var aggregate = byDate.get(candidate.date) || {
1846
+ availableCount: 0,
1847
+ totalCount: 0,
1848
+ maxPartySize: 0,
1849
+ remainingCapacity: 0,
1850
+ maxCapacity: 0,
1851
+ reasonCodeSet: new Set(),
1852
+ statusAggregate: createStatusAggregate()
1853
+ };
1854
+ aggregate.totalCount += 1;
1855
+ if (evaluated.status === 'available' || evaluated.status === 'limited') {
1856
+ aggregate.availableCount += 1;
938
1857
  }
939
- assignments.push({
940
- productId: interval.productId,
941
- resourceId: availableCell.resourceId,
942
- start_at: interval.start_at,
943
- end_at: interval.end_at,
944
- capacityRequired: interval.capacityRequired
1858
+ aggregate.maxPartySize = Math.max(aggregate.maxPartySize, evaluated.maxPartySize);
1859
+ aggregate.remainingCapacity += evaluated.remainingCapacity;
1860
+ aggregate.maxCapacity += evaluated.maxCapacity;
1861
+ pushStatusAggregate(aggregate.statusAggregate, evaluated.status);
1862
+ evaluated.reasonCodes.forEach(function (reasonCode) {
1863
+ return aggregate.reasonCodeSet.add(reasonCode);
945
1864
  });
946
- },
947
- _ret2;
948
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
949
- _ret2 = _loop3();
950
- if (_ret2 === 0) continue;
951
- }
952
- } catch (err) {
953
- _iterator5.e(err);
954
- } finally {
955
- _iterator5.f();
1865
+ byDate.set(candidate.date, aggregate);
1866
+ });
1867
+ var dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(function (date) {
1868
+ var aggregate = byDate.get(date);
1869
+ return {
1870
+ date: date,
1871
+ status: aggregate ? summarizeStatusAggregate(aggregate.statusAggregate) : 'unavailable',
1872
+ availableCount: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.availableCount) || 0,
1873
+ totalCount: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.totalCount) || 0,
1874
+ maxPartySize: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.maxPartySize) || 0,
1875
+ remainingCapacity: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.remainingCapacity) || 0,
1876
+ maxCapacity: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.maxCapacity) || 0,
1877
+ reasonCodes: aggregate ? Array.from(aggregate.reasonCodeSet) : []
1878
+ };
1879
+ });
1880
+ return {
1881
+ productId: product.id,
1882
+ title: product.title,
1883
+ dates: dates
1884
+ };
1885
+ });
1886
+ return {
1887
+ target: 'date',
1888
+ evaluatedAt: evaluatedAt,
1889
+ snapshotId: projection.meta.snapshotId,
1890
+ data: _data3
1891
+ };
956
1892
  }
1893
+ var data = projection.products.filter(function (product) {
1894
+ return !productIdSet || productIdSet.has(String(product.id));
1895
+ }).map(function (product) {
1896
+ var group = candidateGroupMap.get(String(product.id)) || {
1897
+ productId: product.id,
1898
+ title: product.title,
1899
+ kind: product.kind,
1900
+ ranges: []
1901
+ };
1902
+ var productStatusAggregate = createStatusAggregate();
1903
+ var productReasonCodeSet = new Set();
1904
+ var dateStatusMap = new Map();
1905
+ var availableTimeCount = 0;
1906
+ var totalTimeCount = 0;
1907
+ var maxPartySize = 0;
1908
+ group.ranges.forEach(function (candidate) {
1909
+ var evaluated = evaluateCandidateSummary(projection, candidate, {
1910
+ partySize: normalizedPartySize,
1911
+ resourceIds: request.resourceIds,
1912
+ evaluatedAt: evaluatedAt,
1913
+ cellIndex: cellIndex,
1914
+ productIndex: productIndex,
1915
+ groupResourceIndex: groupResourceIndex,
1916
+ candidateRangeCache: candidateRangeCache,
1917
+ evaluationContext: evaluationContext
1918
+ });
1919
+ totalTimeCount += 1;
1920
+ if (evaluated.status === 'available' || evaluated.status === 'limited') {
1921
+ availableTimeCount += 1;
1922
+ }
1923
+ maxPartySize = Math.max(maxPartySize, evaluated.maxPartySize);
1924
+ pushStatusAggregate(productStatusAggregate, evaluated.status);
1925
+ evaluated.reasonCodes.forEach(function (reasonCode) {
1926
+ return productReasonCodeSet.add(reasonCode);
1927
+ });
1928
+ var dateAggregate = dateStatusMap.get(candidate.date) || createStatusAggregate();
1929
+ pushStatusAggregate(dateAggregate, evaluated.status);
1930
+ dateStatusMap.set(candidate.date, dateAggregate);
1931
+ });
1932
+ var availableDateCount = Array.from(dateStatusMap.values()).filter(function (aggregate) {
1933
+ var status = summarizeStatusAggregate(aggregate);
1934
+ return status === 'available' || status === 'limited';
1935
+ }).length;
1936
+ var totalDateCount = dateStatusMap.size;
1937
+ var status = summarizeStatusAggregate(productStatusAggregate);
1938
+ var item = {
1939
+ productId: group.productId,
1940
+ title: group.title,
1941
+ status: status,
1942
+ reasonCodes: Array.from(productReasonCodeSet),
1943
+ availableDateCount: availableDateCount,
1944
+ totalDateCount: totalDateCount,
1945
+ availableTimeCount: availableTimeCount,
1946
+ totalTimeCount: totalTimeCount,
1947
+ maxPartySize: maxPartySize
1948
+ };
1949
+ return {
1950
+ productId: product.id,
1951
+ title: product.title,
1952
+ item: item
1953
+ };
1954
+ });
957
1955
  return {
958
- assignments: assignments,
959
- conflicts: conflicts
1956
+ target: 'product',
1957
+ evaluatedAt: evaluatedAt,
1958
+ snapshotId: projection.meta.snapshotId,
1959
+ data: data
960
1960
  };
961
1961
  }
962
- function withAssignmentEvents(context, assignments) {
963
- if (!assignments.length) return context;
964
- return _objectSpread(_objectSpread({}, context), {}, {
965
- externalEvents: [].concat(_toConsumableArray(context.externalEvents), _toConsumableArray(assignments.map(function (assignment) {
966
- return {
967
- resourceId: assignment.resourceId,
968
- start_at: assignment.start_at,
969
- end_at: assignment.end_at,
970
- pax: assignment.capacityRequired || 1,
971
- source: 'selection'
972
- };
973
- })))
974
- });
1962
+ function summarizeDateStatus(statuses) {
1963
+ if (!statuses.length) return 'unavailable';
1964
+ if (statuses.some(function (status) {
1965
+ return status === 'available';
1966
+ })) return 'available';
1967
+ if (statuses.some(function (status) {
1968
+ return status === 'limited';
1969
+ })) return 'limited';
1970
+ if (statuses.some(function (status) {
1971
+ return status === 'full';
1972
+ })) return 'full';
1973
+ if (statuses.every(function (status) {
1974
+ return status === 'past';
1975
+ })) return 'past';
1976
+ return 'unavailable';
975
1977
  }
976
- export function validateAssignments(contextInput, selection) {
977
- if (selection.assignments.length > 0) {
978
- var context = normalizePlannerContext(contextInput);
979
- var _conflicts = [];
980
- var _loop4 = function _loop4() {
981
- var assignment = selection.assignments[index];
982
- var product = context.products.find(function (item) {
983
- return sameId(item.id, assignment.productId);
984
- });
985
- var resource = context.resources.find(function (item) {
986
- return sameId(item.id, assignment.resourceId);
987
- });
988
- if (!product) {
989
- _conflicts.push({
990
- type: 'missing_resource',
991
- message: 'assignment 对应的商品不存在',
992
- productId: assignment.productId
993
- });
994
- return 0; // continue
995
- }
996
- if (!resource) {
997
- _conflicts.push({
998
- type: 'missing_resource',
999
- message: 'assignment 对应的资源不存在',
1000
- productId: assignment.productId,
1001
- resourceId: assignment.resourceId
1002
- });
1003
- return 0; // continue
1004
- }
1005
- var start = dayjs(assignment.start_at);
1006
- var end = dayjs(assignment.end_at);
1007
- var interval = {
1008
- id: "".concat(assignment.productId, ":").concat(assignment.resourceId, ":").concat(assignment.start_at, ":").concat(assignment.end_at),
1009
- productId: assignment.productId,
1010
- source: product.kind === 'session' ? 'session' : product.kind === 'venue_slot' ? 'venue_slot' : 'duration',
1011
- date: start.format('YYYY-MM-DD'),
1012
- start_at: assignment.start_at,
1013
- end_at: assignment.end_at,
1014
- durationMinutes: Math.max(1, end.diff(start, 'minute')),
1015
- quantity: Math.max(1, Number(product.quantity || 1)),
1016
- capacityRequired: Math.max(1, Number(assignment.capacityRequired || product.capacityRequired || 1)),
1017
- resourceRequirements: product.resourceRequirements || []
1018
- };
1019
- var cell = evaluateResourceForInterval({
1020
- resource: resource,
1021
- interval: interval,
1022
- context: context
1023
- });
1024
- if (cell.status !== 'available' && cell.status !== 'limited') {
1025
- _conflicts.push.apply(_conflicts, _toConsumableArray(cell.conflicts));
1026
- }
1027
- var priorContext = withAssignmentEvents(context, selection.assignments.slice(0, index));
1028
- var selfConflictCell = evaluateResourceForInterval({
1029
- resource: resource,
1030
- interval: interval,
1031
- context: priorContext
1032
- });
1033
- if (selfConflictCell.status !== 'available' && selfConflictCell.status !== 'limited') {
1034
- _conflicts.push.apply(_conflicts, _toConsumableArray(selfConflictCell.conflicts.map(function (conflict) {
1035
- return _objectSpread(_objectSpread({}, conflict), {}, {
1036
- type: 'cart_overlap',
1037
- message: '本次选择中存在资源时间冲突'
1038
- });
1039
- })));
1040
- }
1041
- },
1042
- _ret3;
1043
- for (var index = 0; index < selection.assignments.length; index += 1) {
1044
- _ret3 = _loop4();
1045
- if (_ret3 === 0) continue;
1046
- }
1978
+ function summarizeProductStatus(statuses) {
1979
+ if (!statuses.length) return 'unavailable';
1980
+ if (statuses.some(function (status) {
1981
+ return status === 'available';
1982
+ })) return 'available';
1983
+ if (statuses.some(function (status) {
1984
+ return status === 'limited';
1985
+ })) return 'limited';
1986
+ if (statuses.some(function (status) {
1987
+ return status === 'full';
1988
+ })) return 'full';
1989
+ if (statuses.every(function (status) {
1990
+ return status === 'past';
1991
+ })) return 'past';
1992
+ return 'unavailable';
1993
+ }
1994
+ export function validateAvailabilitySelection(projection, params) {
1995
+ var _extractFixedOffsetMi7;
1996
+ var normalizedPartySize = normalizePartySize(params.partySize);
1997
+ var normalizedBookingCount = normalizeBookingCount(params.bookingCount);
1998
+ assertRangesWithinProjection(projection, [params.candidate]);
1999
+ var productIndex = buildProductIndex(projection);
2000
+ var evaluatedAt = formatDateTime(localDateToScalar(new Date()), projection.meta.timezone, (_extractFixedOffsetMi7 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi7 !== void 0 ? _extractFixedOffsetMi7 : captureRuntimeOffsetMinutes());
2001
+ var evaluated = evaluateCandidate(projection, params.candidate, {
2002
+ partySize: normalizedPartySize,
2003
+ evaluatedAt: evaluatedAt,
2004
+ requirementSelections: params.requirementSelections,
2005
+ allowImplicitSelection: false,
2006
+ cellIndex: buildAvailabilityCellIndex(projection),
2007
+ productIndex: productIndex,
2008
+ groupResourceIndex: buildGroupResourceIndex({
2009
+ projection: projection,
2010
+ partySize: normalizedPartySize
2011
+ }),
2012
+ candidateRangeCache: new Map(),
2013
+ evaluationContext: buildEvaluationContext({
2014
+ projection: projection,
2015
+ evaluatedAt: evaluatedAt,
2016
+ requirementSelections: params.requirementSelections
2017
+ })
2018
+ });
2019
+ var conflicts = [];
2020
+ if (evaluated.gate) {
2021
+ conflicts.push({
2022
+ code: evaluated.gate.conflictCode,
2023
+ message: evaluated.gate.status === 'past' ? '候选时间已过期' : '候选时间不满足预订时间门禁',
2024
+ reasonCodes: evaluated.gate.reasonCodes,
2025
+ productId: params.candidate.productId,
2026
+ candidateKey: params.candidate.key
2027
+ });
1047
2028
  return {
1048
- ok: _conflicts.length === 0,
1049
- conflicts: _conflicts
2029
+ ok: false,
2030
+ status: evaluated.gate.status === 'past' ? 'past' : 'conflict',
2031
+ assignments: [],
2032
+ conflicts: conflicts,
2033
+ availability: evaluated.item
1050
2034
  };
1051
2035
  }
1052
- var projection = queryPlannerAvailability(contextInput, {
1053
- productIds: selection.productIds,
1054
- date: selection.date,
1055
- startTime: selection.startTime,
1056
- endTime: selection.endTime,
1057
- resourceIds: selection.resourceIds,
1058
- mode: selection.mode
2036
+ var requiredGroups = evaluated.item.requirementGroups.filter(function (group) {
2037
+ return group.required;
2038
+ });
2039
+ var findSelectionForGroup = function findSelectionForGroup(group) {
2040
+ return params.requirementSelections.find(function (selection) {
2041
+ return selection.requirementGroupId && selection.requirementGroupId === group.groupId || selection.formId !== undefined && sameId(selection.formId, group.formId);
2042
+ });
2043
+ };
2044
+ requiredGroups.forEach(function (group) {
2045
+ var selection = findSelectionForGroup(group);
2046
+ var selectedCount = new Set(((selection === null || selection === void 0 ? void 0 : selection.resourceIds) || []).map(String)).size;
2047
+ if (selectedCount < group.min) {
2048
+ conflicts.push({
2049
+ code: 'missing_resource',
2050
+ message: "\u8D44\u6E90\u7EC4 ".concat(group.groupId, " \u7F3A\u5C11\u5DF2\u9009\u8D44\u6E90"),
2051
+ productId: params.candidate.productId,
2052
+ requirementGroupId: group.groupId,
2053
+ formId: group.formId,
2054
+ candidateKey: params.candidate.key
2055
+ });
2056
+ return;
2057
+ }
2058
+ if (selectedCount > group.max) {
2059
+ conflicts.push({
2060
+ code: 'invalid_selection_count',
2061
+ message: "\u8D44\u6E90\u7EC4 ".concat(group.groupId, " \u5DF2\u9009\u8D44\u6E90\u6570\u91CF\u8D85\u51FA\u4E0A\u9650"),
2062
+ productId: params.candidate.productId,
2063
+ requirementGroupId: group.groupId,
2064
+ formId: group.formId,
2065
+ candidateKey: params.candidate.key
2066
+ });
2067
+ return;
2068
+ }
2069
+ if (group.status === 'available' || group.status === 'limited') return;
2070
+ conflicts.push({
2071
+ code: group.status === 'past' ? 'past' : group.status === 'full' ? 'resource_full' : 'missing_resource',
2072
+ message: "\u8D44\u6E90\u7EC4 ".concat(group.groupId, " \u5F53\u524D\u4E0D\u53EF\u63D0\u4EA4"),
2073
+ productId: params.candidate.productId,
2074
+ requirementGroupId: group.groupId,
2075
+ formId: group.formId,
2076
+ candidateKey: params.candidate.key
2077
+ });
1059
2078
  });
1060
- var conflicts = projection.cells.filter(function (cell) {
1061
- return cell.status !== 'available' && cell.status !== 'limited';
1062
- }).flatMap(function (cell) {
1063
- return cell.conflicts;
2079
+ params.requirementSelections.forEach(function (selection) {
2080
+ var matchedGroup = evaluated.item.requirementGroups.find(function (group) {
2081
+ return selection.requirementGroupId ? group.groupId === selection.requirementGroupId : sameId(group.formId, selection.formId);
2082
+ });
2083
+ if (!matchedGroup) {
2084
+ conflicts.push({
2085
+ code: 'invalid_requirement_group',
2086
+ message: '选择的资源组不属于当前商品',
2087
+ productId: params.candidate.productId,
2088
+ requirementGroupId: selection.requirementGroupId,
2089
+ formId: selection.formId,
2090
+ candidateKey: params.candidate.key
2091
+ });
2092
+ return;
2093
+ }
2094
+ var selectedCount = new Set(selection.resourceIds.map(String)).size;
2095
+ if (!matchedGroup.required && (selectedCount < matchedGroup.min || selectedCount > matchedGroup.max)) {
2096
+ conflicts.push({
2097
+ code: 'invalid_selection_count',
2098
+ message: "\u8D44\u6E90\u7EC4 ".concat(matchedGroup.groupId, " \u5DF2\u9009\u8D44\u6E90\u6570\u91CF\u4E0D\u6EE1\u8DB3 ").concat(matchedGroup.min, "-").concat(matchedGroup.max),
2099
+ productId: params.candidate.productId,
2100
+ requirementGroupId: matchedGroup.groupId,
2101
+ formId: matchedGroup.formId,
2102
+ candidateKey: params.candidate.key
2103
+ });
2104
+ }
2105
+ selection.resourceIds.forEach(function (resourceId) {
2106
+ var option = matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.options.find(function (item) {
2107
+ return sameId(item.resourceId, resourceId);
2108
+ });
2109
+ var matchedCell = option === null || option === void 0 ? void 0 : option.matchedCell;
2110
+ var remainingCapacity = matchedCell === null || matchedCell === void 0 ? void 0 : matchedCell.remainingCapacity;
2111
+ if (option && (option.status === 'available' || option.status === 'limited') && remainingCapacity !== undefined) {
2112
+ var capacityPerBooking = normalizedPartySize !== null && normalizedPartySize !== void 0 ? normalizedPartySize : 1;
2113
+ var availableQuantity = Math.max(0, Math.floor(remainingCapacity / capacityPerBooking));
2114
+ if (normalizedBookingCount > availableQuantity) {
2115
+ conflicts.push({
2116
+ code: 'resource_capacity_exceeded',
2117
+ message: "\u6240\u9009\u8D44\u6E90\u300C".concat(option.resourceName || String(resourceId), "\u300D\u5F53\u524D\u6700\u591A\u53EF\u9884\u8BA2 ").concat(availableQuantity, " \u4E2A"),
2118
+ productId: params.candidate.productId,
2119
+ requirementGroupId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.groupId,
2120
+ formId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.formId,
2121
+ resourceId: resourceId,
2122
+ resourceName: option.resourceName,
2123
+ candidateKey: params.candidate.key,
2124
+ requestedQuantity: normalizedBookingCount,
2125
+ availableQuantity: availableQuantity,
2126
+ remainingCapacity: remainingCapacity,
2127
+ capacityPerBooking: capacityPerBooking
2128
+ });
2129
+ return;
2130
+ }
2131
+ }
2132
+ if (!option || option.status !== 'available' && option.status !== 'limited') {
2133
+ conflicts.push({
2134
+ code: 'resource_unavailable',
2135
+ message: "\u8D44\u6E90 ".concat(String(resourceId), " \u5F53\u524D\u4E0D\u53EF\u63D0\u4EA4"),
2136
+ productId: params.candidate.productId,
2137
+ requirementGroupId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.groupId,
2138
+ formId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.formId,
2139
+ resourceId: resourceId,
2140
+ candidateKey: params.candidate.key
2141
+ });
2142
+ }
2143
+ });
1064
2144
  });
1065
2145
  return {
1066
- ok: conflicts.length === 0,
1067
- conflicts: conflicts
2146
+ ok: conflicts.length === 0 && (evaluated.item.status === 'available' || evaluated.item.status === 'limited'),
2147
+ status: conflicts.length === 0 ? evaluated.item.status : 'conflict',
2148
+ assignments: evaluated.assignments,
2149
+ conflicts: conflicts,
2150
+ availability: evaluated.item
1068
2151
  };
1069
2152
  }