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