@pisell/pisellos 2.3.42 → 2.3.44
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/Discount/index.d.ts +1 -0
- package/dist/modules/Discount/index.js +40 -11
- package/dist/modules/Discount/types.d.ts +1 -0
- package/dist/modules/Holder/index.d.ts +48 -0
- package/dist/modules/Holder/index.js +640 -0
- package/dist/modules/Holder/types.d.ts +123 -0
- package/dist/modules/Holder/types.js +1 -0
- package/dist/modules/Holder/utils.d.ts +13 -0
- package/dist/modules/Holder/utils.js +34 -0
- package/dist/modules/OpenData/index.js +2 -2
- package/dist/modules/Order/index.d.ts +5 -2
- package/dist/modules/Order/index.js +127 -53
- package/dist/modules/Order/types.d.ts +11 -0
- package/dist/modules/Order/utils.d.ts +10 -0
- package/dist/modules/Order/utils.js +35 -3
- package/dist/modules/Payment/walletpass.d.ts +10 -3
- package/dist/modules/Payment/walletpass.js +425 -282
- package/dist/modules/Product/types.d.ts +1 -0
- package/dist/modules/ProductList/index.js +33 -14
- package/dist/modules/Quotation/index.d.ts +1 -1
- package/dist/modules/Quotation/index.js +2 -2
- package/dist/modules/ResourcePlanner/index.d.ts +24 -0
- package/dist/modules/ResourcePlanner/index.js +181 -0
- package/dist/modules/ResourcePlanner/planner.d.ts +14 -0
- package/dist/modules/ResourcePlanner/planner.js +1069 -0
- package/dist/modules/ResourcePlanner/types.d.ts +227 -0
- package/dist/modules/ResourcePlanner/types.js +1 -0
- package/dist/modules/Rules/index.d.ts +2 -0
- package/dist/modules/Rules/index.js +106 -68
- package/dist/modules/SalesSummary/index.js +12 -13
- package/dist/modules/ScanOrderLogger/index.d.ts +2 -0
- package/dist/modules/ScanOrderLogger/index.js +15 -2
- package/dist/modules/ScanOrderLogger/providers/feishu.js +45 -27
- package/dist/modules/ScanOrderLogger/types.d.ts +1 -0
- package/dist/modules/Schedule/index.d.ts +3 -1
- package/dist/modules/Schedule/index.js +21 -16
- package/dist/modules/Summary/utils.js +38 -13
- package/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +3 -1
- package/dist/plugins/window.d.ts +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +772 -657
- package/dist/solution/BaseSales/index.d.ts +7 -0
- package/dist/solution/BaseSales/index.js +70 -45
- package/dist/solution/BaseSales/types.d.ts +3 -1
- package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +6 -3
- package/dist/solution/BookingByStep/index.d.ts +17 -2
- package/dist/solution/BookingByStep/index.js +294 -215
- package/dist/solution/BookingByStep/types.d.ts +2 -1
- package/dist/solution/BookingByStep/types.js +3 -1
- package/dist/solution/BookingByStep/utils/capacity.js +17 -1
- package/dist/solution/BookingByStep/utils/timeslots.d.ts +3 -1
- package/dist/solution/BookingByStep/utils/timeslots.js +19 -12
- package/dist/solution/BookingTicket/index.js +8 -5
- package/dist/solution/BookingTicket/types.d.ts +1 -0
- package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +3 -3
- package/dist/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
- package/dist/solution/BookingTicket/utils/scan/handleScan.js +10 -2
- package/dist/solution/ScanOrder/index.d.ts +26 -14
- package/dist/solution/ScanOrder/index.js +1142 -739
- package/dist/solution/ScanOrder/types.d.ts +21 -1
- package/dist/solution/ScanOrder/utils.d.ts +8 -0
- package/dist/solution/ScanOrder/utils.js +66 -25
- package/dist/solution/ShopDiscount/index.d.ts +1 -0
- package/dist/solution/ShopDiscount/index.js +125 -87
- package/dist/solution/UnifiedBookingSales/index.d.ts +183 -0
- package/dist/solution/UnifiedBookingSales/index.js +1891 -0
- package/dist/solution/UnifiedBookingSales/types.d.ts +143 -0
- package/dist/solution/UnifiedBookingSales/types.js +11 -0
- package/dist/solution/VenueBooking/index.d.ts +40 -11
- package/dist/solution/VenueBooking/index.js +1167 -841
- package/dist/solution/VenueBooking/types.d.ts +3 -0
- package/dist/solution/VenueBooking/utils/resource.js +1 -0
- package/dist/solution/VenueBooking/utils/slotMerge.d.ts +4 -4
- package/dist/solution/VenueBooking/utils/slotMerge.js +10 -5
- package/dist/solution/index.d.ts +1 -0
- package/dist/solution/index.js +2 -1
- package/dist/types/index.d.ts +1 -0
- package/lib/modules/Discount/index.d.ts +1 -0
- package/lib/modules/Discount/index.js +9 -0
- package/lib/modules/Discount/types.d.ts +1 -0
- package/lib/modules/Holder/index.d.ts +48 -0
- package/lib/modules/Holder/index.js +402 -0
- package/lib/modules/Holder/types.d.ts +123 -0
- package/lib/modules/Holder/types.js +17 -0
- package/lib/modules/Holder/utils.d.ts +13 -0
- package/lib/modules/Holder/utils.js +71 -0
- package/lib/modules/OpenData/index.js +1 -1
- package/lib/modules/Order/index.d.ts +5 -2
- package/lib/modules/Order/index.js +72 -17
- package/lib/modules/Order/types.d.ts +11 -0
- package/lib/modules/Order/utils.d.ts +10 -0
- package/lib/modules/Order/utils.js +36 -5
- package/lib/modules/Payment/walletpass.d.ts +10 -3
- package/lib/modules/Payment/walletpass.js +218 -56
- package/lib/modules/Product/types.d.ts +1 -0
- package/lib/modules/ProductList/index.js +40 -31
- package/lib/modules/Quotation/index.d.ts +1 -1
- package/lib/modules/Quotation/index.js +2 -2
- package/lib/modules/ResourcePlanner/index.d.ts +24 -0
- package/lib/modules/ResourcePlanner/index.js +148 -0
- package/lib/modules/ResourcePlanner/planner.d.ts +14 -0
- package/lib/modules/ResourcePlanner/planner.js +933 -0
- package/lib/modules/ResourcePlanner/types.d.ts +227 -0
- package/lib/modules/ResourcePlanner/types.js +17 -0
- package/lib/modules/Rules/index.d.ts +2 -0
- package/lib/modules/Rules/index.js +41 -19
- package/lib/modules/SalesSummary/index.js +6 -7
- package/lib/modules/ScanOrderLogger/index.d.ts +2 -0
- package/lib/modules/ScanOrderLogger/index.js +13 -1
- package/lib/modules/ScanOrderLogger/providers/feishu.js +15 -6
- package/lib/modules/ScanOrderLogger/types.d.ts +1 -0
- package/lib/modules/Schedule/index.d.ts +3 -1
- package/lib/modules/Schedule/index.js +10 -8
- package/lib/modules/Summary/utils.js +21 -1
- package/lib/modules/index.d.ts +2 -0
- package/lib/modules/index.js +5 -1
- package/lib/plugins/window.d.ts +1 -0
- package/lib/server/index.d.ts +14 -0
- package/lib/server/index.js +65 -5
- package/lib/solution/BaseSales/index.d.ts +7 -0
- package/lib/solution/BaseSales/index.js +25 -8
- package/lib/solution/BaseSales/types.d.ts +3 -1
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
- package/lib/solution/BookingByStep/index.d.ts +17 -2
- package/lib/solution/BookingByStep/index.js +60 -18
- package/lib/solution/BookingByStep/types.d.ts +2 -1
- package/lib/solution/BookingByStep/types.js +5 -0
- package/lib/solution/BookingByStep/utils/capacity.js +20 -1
- package/lib/solution/BookingByStep/utils/timeslots.d.ts +3 -1
- package/lib/solution/BookingByStep/utils/timeslots.js +19 -11
- package/lib/solution/BookingTicket/index.js +11 -2
- package/lib/solution/BookingTicket/types.d.ts +1 -0
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +2 -2
- package/lib/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
- package/lib/solution/BookingTicket/utils/scan/handleScan.js +2 -2
- package/lib/solution/ScanOrder/index.d.ts +26 -14
- package/lib/solution/ScanOrder/index.js +449 -182
- package/lib/solution/ScanOrder/types.d.ts +21 -1
- package/lib/solution/ScanOrder/utils.d.ts +8 -0
- package/lib/solution/ScanOrder/utils.js +31 -0
- package/lib/solution/ShopDiscount/index.d.ts +1 -0
- package/lib/solution/ShopDiscount/index.js +14 -0
- package/lib/solution/UnifiedBookingSales/index.d.ts +183 -0
- package/lib/solution/UnifiedBookingSales/index.js +1084 -0
- package/lib/solution/UnifiedBookingSales/types.d.ts +143 -0
- package/lib/solution/UnifiedBookingSales/types.js +33 -0
- package/lib/solution/VenueBooking/index.d.ts +40 -11
- package/lib/solution/VenueBooking/index.js +322 -110
- package/lib/solution/VenueBooking/types.d.ts +3 -0
- package/lib/solution/VenueBooking/utils/resource.js +1 -0
- package/lib/solution/VenueBooking/utils/slotMerge.d.ts +4 -4
- package/lib/solution/VenueBooking/utils/slotMerge.js +6 -4
- package/lib/solution/index.d.ts +1 -0
- package/lib/solution/index.js +3 -1
- package/lib/types/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
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
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/modules/ResourcePlanner/planner.ts
|
|
30
|
+
var planner_exports = {};
|
|
31
|
+
__export(planner_exports, {
|
|
32
|
+
DEFAULT_RESOURCE_PLANNER_CONTEXT: () => DEFAULT_RESOURCE_PLANNER_CONTEXT,
|
|
33
|
+
DEFAULT_RESOURCE_PLANNER_SELECTION: () => DEFAULT_RESOURCE_PLANNER_SELECTION,
|
|
34
|
+
autoAssignResources: () => autoAssignResources,
|
|
35
|
+
buildDemandIntervals: () => buildDemandIntervals,
|
|
36
|
+
evaluateResourceForInterval: () => evaluateResourceForInterval,
|
|
37
|
+
normalizePlannerContext: () => normalizePlannerContext,
|
|
38
|
+
queryPlannerAvailability: () => queryPlannerAvailability,
|
|
39
|
+
resolveAssignableSlots: () => resolveAssignableSlots,
|
|
40
|
+
validateAssignments: () => validateAssignments
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(planner_exports);
|
|
43
|
+
var import_dayjs = __toESM(require("dayjs"));
|
|
44
|
+
var DEFAULT_RESOURCE_PLANNER_CONTEXT = {
|
|
45
|
+
products: [],
|
|
46
|
+
resources: [],
|
|
47
|
+
externalEvents: [],
|
|
48
|
+
slotDurationMinutes: 30,
|
|
49
|
+
businessStartTime: "09:00",
|
|
50
|
+
businessEndTime: "18:00"
|
|
51
|
+
};
|
|
52
|
+
var DEFAULT_RESOURCE_PLANNER_SELECTION = {
|
|
53
|
+
productIds: [],
|
|
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
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function sameId(left, right) {
|
|
68
|
+
return String(left) === String(right);
|
|
69
|
+
}
|
|
70
|
+
function dateOf(value) {
|
|
71
|
+
return (0, import_dayjs.default)(value).format("YYYY-MM-DD");
|
|
72
|
+
}
|
|
73
|
+
function timeOf(value) {
|
|
74
|
+
return (0, import_dayjs.default)(value).format("HH:mm");
|
|
75
|
+
}
|
|
76
|
+
function toDateTime(date, time) {
|
|
77
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(time))
|
|
78
|
+
return (0, import_dayjs.default)(time);
|
|
79
|
+
return (0, import_dayjs.default)(`${date} ${time.length === 5 ? `${time}:00` : time}`);
|
|
80
|
+
}
|
|
81
|
+
function overlaps(startA, endA, startB, endB) {
|
|
82
|
+
return startA.isBefore(endB) && endA.isAfter(startB);
|
|
83
|
+
}
|
|
84
|
+
function containsRange(outerStart, outerEnd, innerStart, innerEnd) {
|
|
85
|
+
return !innerStart.isBefore(outerStart) && !innerEnd.isAfter(outerEnd);
|
|
86
|
+
}
|
|
87
|
+
function maxDayjs(left, right) {
|
|
88
|
+
return left.isAfter(right) ? left : right;
|
|
89
|
+
}
|
|
90
|
+
function minDayjs(left, right) {
|
|
91
|
+
return left.isBefore(right) ? left : right;
|
|
92
|
+
}
|
|
93
|
+
function addDays(start, end) {
|
|
94
|
+
const result = [];
|
|
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;
|
|
102
|
+
}
|
|
103
|
+
function getQueryProducts(products, query) {
|
|
104
|
+
var _a;
|
|
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)));
|
|
108
|
+
}
|
|
109
|
+
function getQueryResources(resources, query) {
|
|
110
|
+
var _a;
|
|
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)));
|
|
114
|
+
}
|
|
115
|
+
function matchesResourceRequirement(resource, requirement) {
|
|
116
|
+
var _a;
|
|
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;
|
|
121
|
+
}
|
|
122
|
+
function resolveProductTitle(product) {
|
|
123
|
+
var _a, _b, _c;
|
|
124
|
+
const rawTitle = ((_a = product.raw) == null ? void 0 : _a.title) || ((_b = product.raw) == null ? void 0 : _b.name) || ((_c = product.raw) == null ? void 0 : _c.product_title);
|
|
125
|
+
if (typeof product.title === "string")
|
|
126
|
+
return product.title;
|
|
127
|
+
if (typeof rawTitle === "string")
|
|
128
|
+
return rawTitle;
|
|
129
|
+
if (rawTitle && typeof rawTitle === "object") {
|
|
130
|
+
return rawTitle.en || rawTitle.zh || rawTitle.default || Object.values(rawTitle)[0];
|
|
131
|
+
}
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
function buildDemandIntervals(products, query, context) {
|
|
135
|
+
const dates = query.date ? [query.date] : query.dateRange ? addDays(query.dateRange.start, query.dateRange.end) : [];
|
|
136
|
+
const selectedProducts = getQueryProducts(products, query);
|
|
137
|
+
const intervals = [];
|
|
138
|
+
for (const product of selectedProducts) {
|
|
139
|
+
const quantity = Math.max(1, Number(product.quantity || 1));
|
|
140
|
+
const capacityRequired = Math.max(1, Number(query.pax || product.capacityRequired || 1));
|
|
141
|
+
const requirements = product.resourceRequirements || [];
|
|
142
|
+
if (product.kind === "session" || product.kind === "venue_slot") {
|
|
143
|
+
const schedules = product.schedule || [];
|
|
144
|
+
for (const schedule of schedules) {
|
|
145
|
+
const scheduleDate = dateOf(schedule.start_at);
|
|
146
|
+
if (dates.length > 0 && !dates.includes(scheduleDate))
|
|
147
|
+
continue;
|
|
148
|
+
const start = (0, import_dayjs.default)(schedule.start_at);
|
|
149
|
+
const end = (0, import_dayjs.default)(schedule.end_at);
|
|
150
|
+
intervals.push({
|
|
151
|
+
id: `${product.id}:${start.toISOString()}:${end.toISOString()}`,
|
|
152
|
+
productId: product.id,
|
|
153
|
+
source: product.kind,
|
|
154
|
+
date: dateOf(schedule.start_at),
|
|
155
|
+
start_at: start.toISOString(),
|
|
156
|
+
end_at: end.toISOString(),
|
|
157
|
+
durationMinutes: Math.max(1, end.diff(start, "minute")),
|
|
158
|
+
quantity,
|
|
159
|
+
capacityRequired,
|
|
160
|
+
resourceRequirements: requirements
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (product.kind === "duration") {
|
|
166
|
+
const duration = Math.max(1, Number(product.durationMinutes || 30));
|
|
167
|
+
for (const currentDate of dates) {
|
|
168
|
+
const start = query.startTime ? toDateTime(currentDate, query.startTime) : toDateTime(currentDate, (context == null ? void 0 : context.businessStartTime) || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessStartTime);
|
|
169
|
+
let end = query.startTime && query.endTime ? toDateTime(currentDate, query.endTime) : query.startTime ? start.add(duration, "minute") : toDateTime(currentDate, (context == null ? void 0 : context.businessEndTime) || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessEndTime);
|
|
170
|
+
if (!end.isAfter(start))
|
|
171
|
+
end = end.add(1, "day");
|
|
172
|
+
intervals.push({
|
|
173
|
+
id: `${product.id}:${currentDate}:${query.startTime || "range"}`,
|
|
174
|
+
productId: product.id,
|
|
175
|
+
source: "duration",
|
|
176
|
+
date: currentDate,
|
|
177
|
+
start_at: start.toISOString(),
|
|
178
|
+
end_at: end.toISOString(),
|
|
179
|
+
durationMinutes: duration,
|
|
180
|
+
quantity,
|
|
181
|
+
capacityRequired,
|
|
182
|
+
resourceRequirements: requirements
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return intervals;
|
|
188
|
+
}
|
|
189
|
+
function getEventsForResource(resource, contextEvents) {
|
|
190
|
+
const embeddedEvents = resource.times.flatMap((time) => time.event_list || []);
|
|
191
|
+
return [
|
|
192
|
+
...embeddedEvents,
|
|
193
|
+
...contextEvents.filter((event) => sameId(event.resourceId ?? event.resource_id, resource.id))
|
|
194
|
+
].filter(Boolean);
|
|
195
|
+
}
|
|
196
|
+
function collectEventsByResource(resources, contextEvents) {
|
|
197
|
+
const eventsByResource = /* @__PURE__ */ new Map();
|
|
198
|
+
resources.forEach((resource) => {
|
|
199
|
+
eventsByResource.set(String(resource.id), getEventsForResource(resource, contextEvents));
|
|
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));
|
|
211
|
+
}
|
|
212
|
+
function mergeBusySegments(segments) {
|
|
213
|
+
const sorted = [...segments].filter((segment) => segment.end.isAfter(segment.start)).sort((left, right) => left.start.valueOf() - right.start.valueOf());
|
|
214
|
+
const merged = [];
|
|
215
|
+
sorted.forEach((segment) => {
|
|
216
|
+
const previous = merged[merged.length - 1];
|
|
217
|
+
if (previous && !segment.start.isAfter(previous.end)) {
|
|
218
|
+
previous.end = maxDayjs(previous.end, segment.end);
|
|
219
|
+
} else {
|
|
220
|
+
merged.push({ ...segment });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return merged;
|
|
224
|
+
}
|
|
225
|
+
function subtractBusyIntervals(workingSegments, events) {
|
|
226
|
+
const available = [];
|
|
227
|
+
workingSegments.forEach((working) => {
|
|
228
|
+
const busySegments = mergeBusySegments(events.filter((event) => overlaps(working.start, working.end, (0, import_dayjs.default)(event.start_at), (0, import_dayjs.default)(event.end_at))).map((event) => ({
|
|
229
|
+
start: maxDayjs((0, import_dayjs.default)(event.start_at), working.start),
|
|
230
|
+
end: minDayjs((0, import_dayjs.default)(event.end_at), working.end)
|
|
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;
|
|
239
|
+
});
|
|
240
|
+
if (working.end.isAfter(cursor)) {
|
|
241
|
+
available.push({ start: cursor, end: working.end });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return available;
|
|
245
|
+
}
|
|
246
|
+
function buildCapacityRanges(params) {
|
|
247
|
+
const { workingSegments, events, maxCapacity, requiredCapacity } = params;
|
|
248
|
+
const ranges = [];
|
|
249
|
+
workingSegments.forEach((working) => {
|
|
250
|
+
const boundaries = /* @__PURE__ */ new Map([
|
|
251
|
+
[working.start.valueOf(), working.start],
|
|
252
|
+
[working.end.valueOf(), working.end]
|
|
253
|
+
]);
|
|
254
|
+
events.forEach((event) => {
|
|
255
|
+
const eventStart = (0, import_dayjs.default)(event.start_at);
|
|
256
|
+
const eventEnd = (0, import_dayjs.default)(event.end_at);
|
|
257
|
+
if (!overlaps(working.start, working.end, eventStart, eventEnd))
|
|
258
|
+
return;
|
|
259
|
+
const clippedStart = maxDayjs(eventStart, working.start);
|
|
260
|
+
const clippedEnd = minDayjs(eventEnd, working.end);
|
|
261
|
+
boundaries.set(clippedStart.valueOf(), clippedStart);
|
|
262
|
+
boundaries.set(clippedEnd.valueOf(), clippedEnd);
|
|
263
|
+
});
|
|
264
|
+
const points = Array.from(boundaries.values()).sort((left, right) => left.valueOf() - right.valueOf());
|
|
265
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
266
|
+
const start = points[index];
|
|
267
|
+
const end = points[index + 1];
|
|
268
|
+
if (!end.isAfter(start))
|
|
269
|
+
continue;
|
|
270
|
+
const usedCapacity = events.filter((event) => overlaps(start, end, (0, import_dayjs.default)(event.start_at), (0, import_dayjs.default)(event.end_at))).reduce((sum, event) => sum + Math.max(1, Number(event.pax || 1)), 0);
|
|
271
|
+
const remainingCapacity = Math.max(0, maxCapacity - usedCapacity);
|
|
272
|
+
if (remainingCapacity < requiredCapacity)
|
|
273
|
+
continue;
|
|
274
|
+
const status = remainingCapacity < maxCapacity ? "limited" : "available";
|
|
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
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return ranges;
|
|
290
|
+
}
|
|
291
|
+
function rangeFitsDuration(segment, durationMinutes) {
|
|
292
|
+
return segment.end.diff(segment.start, "minute") >= durationMinutes;
|
|
293
|
+
}
|
|
294
|
+
function makeAvailabilityRangeCell(params) {
|
|
295
|
+
const { interval, resource, segment } = params;
|
|
296
|
+
const maxCapacity = Math.max(1, Number(segment.maxCapacity || resource.capacity || 1));
|
|
297
|
+
const remainingCapacity = resource.resourceType === "single" ? 1 : Math.max(0, Number(segment.remainingCapacity ?? maxCapacity));
|
|
298
|
+
const status = resource.resourceType === "single" ? "available" : segment.status || (remainingCapacity < maxCapacity ? "limited" : "available");
|
|
299
|
+
return {
|
|
300
|
+
key: `${interval.productId}:${resource.id}:${segment.start.toISOString()}:${segment.end.toISOString()}`,
|
|
301
|
+
productId: interval.productId,
|
|
302
|
+
resourceId: resource.id,
|
|
303
|
+
date: interval.date,
|
|
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: []
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function buildAvailabilityCellsForResource(params) {
|
|
318
|
+
const { resource, interval, events } = params;
|
|
319
|
+
const requiredCapacity = Math.max(1, Number(interval.capacityRequired || 1));
|
|
320
|
+
const durationMinutes = Math.max(1, Number(interval.durationMinutes || 1));
|
|
321
|
+
const maxCapacity = Math.max(1, Number(resource.capacity || 1));
|
|
322
|
+
let workingSegments = getWorkingSegmentsForInterval(resource, interval);
|
|
323
|
+
const now = (0, import_dayjs.default)();
|
|
324
|
+
workingSegments = workingSegments.map((segment) => ({
|
|
325
|
+
start: segment.end.isAfter(now) ? maxDayjs(segment.start, now) : segment.start,
|
|
326
|
+
end: segment.end
|
|
327
|
+
})).filter((segment) => segment.end.isAfter(segment.start));
|
|
328
|
+
const availableSegments = resource.resourceType === "single" ? subtractBusyIntervals(workingSegments, events).map((segment) => ({
|
|
329
|
+
...segment,
|
|
330
|
+
remainingCapacity: 1,
|
|
331
|
+
maxCapacity: 1,
|
|
332
|
+
status: "available"
|
|
333
|
+
})) : buildCapacityRanges({
|
|
334
|
+
workingSegments,
|
|
335
|
+
events,
|
|
336
|
+
maxCapacity,
|
|
337
|
+
requiredCapacity
|
|
338
|
+
});
|
|
339
|
+
return availableSegments.filter((segment) => rangeFitsDuration(segment, durationMinutes)).map((segment) => makeAvailabilityRangeCell({
|
|
340
|
+
interval,
|
|
341
|
+
resource,
|
|
342
|
+
segment
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
function evaluateResourceForInterval(params) {
|
|
346
|
+
const { resource, interval, context } = params;
|
|
347
|
+
const start = (0, import_dayjs.default)(interval.start_at);
|
|
348
|
+
const end = (0, import_dayjs.default)(interval.end_at);
|
|
349
|
+
const conflicts = [];
|
|
350
|
+
const reasonCodes = [];
|
|
351
|
+
const maxCapacity = Math.max(1, Number(resource.capacity || 1));
|
|
352
|
+
const requiredCapacity = Math.max(1, Number(interval.capacityRequired || 1));
|
|
353
|
+
if (start.isBefore((0, import_dayjs.default)())) {
|
|
354
|
+
reasonCodes.push("past");
|
|
355
|
+
conflicts.push({
|
|
356
|
+
type: "past",
|
|
357
|
+
message: "时间已过期",
|
|
358
|
+
resourceId: resource.id,
|
|
359
|
+
productId: interval.productId
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const insideResourceTime = resource.times.some((time) => {
|
|
363
|
+
return containsRange((0, import_dayjs.default)(time.start_at), (0, import_dayjs.default)(time.end_at), start, end);
|
|
364
|
+
});
|
|
365
|
+
if (!insideResourceTime) {
|
|
366
|
+
reasonCodes.push("outside_resource_time");
|
|
367
|
+
conflicts.push({
|
|
368
|
+
type: "outside_resource_time",
|
|
369
|
+
message: "所选时间不在资源可用时间内",
|
|
370
|
+
resourceId: resource.id,
|
|
371
|
+
productId: interval.productId
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const events = getEventsForResource(resource, context.externalEvents);
|
|
375
|
+
const overlapping = events.filter((event) => overlaps(start, end, (0, import_dayjs.default)(event.start_at), (0, import_dayjs.default)(event.end_at)));
|
|
376
|
+
const usedCapacity = resource.resourceType === "single" ? overlapping.length : overlapping.reduce((sum, event) => sum + Math.max(1, Number(event.pax || 1)), 0);
|
|
377
|
+
const remainingCapacity = resource.resourceType === "single" ? overlapping.length > 0 ? 0 : 1 : Math.max(0, maxCapacity - usedCapacity);
|
|
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
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
let status = "available";
|
|
398
|
+
if (reasonCodes.includes("past"))
|
|
399
|
+
status = "past";
|
|
400
|
+
else if (reasonCodes.includes("outside_resource_time"))
|
|
401
|
+
status = "unavailable";
|
|
402
|
+
else if (reasonCodes.includes("remote_event_overlap") || reasonCodes.includes("capacity_full")) {
|
|
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
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function aggregateStatus(cells) {
|
|
426
|
+
if (!cells.length)
|
|
427
|
+
return "unavailable";
|
|
428
|
+
if (cells.some((cell) => cell.status === "available"))
|
|
429
|
+
return "available";
|
|
430
|
+
if (cells.some((cell) => cell.status === "limited"))
|
|
431
|
+
return "limited";
|
|
432
|
+
if (cells.some((cell) => cell.status === "past"))
|
|
433
|
+
return "past";
|
|
434
|
+
if (cells.some((cell) => cell.status === "full"))
|
|
435
|
+
return "full";
|
|
436
|
+
if (cells.some((cell) => cell.status === "conflict"))
|
|
437
|
+
return "conflict";
|
|
438
|
+
return "unavailable";
|
|
439
|
+
}
|
|
440
|
+
function uniqueReasonCodes(cells) {
|
|
441
|
+
return Array.from(new Set(cells.flatMap((cell) => cell.reasonCodes || [])));
|
|
442
|
+
}
|
|
443
|
+
function buildProductOptions(products, cells, query) {
|
|
444
|
+
const queryProducts = getQueryProducts(products, query);
|
|
445
|
+
return queryProducts.map((product) => {
|
|
446
|
+
const productCells = cells.filter((cell) => sameId(cell.productId, product.id));
|
|
447
|
+
const status = product.kind === "normal" ? "available" : aggregateStatus(productCells);
|
|
448
|
+
return {
|
|
449
|
+
productId: product.id,
|
|
450
|
+
title: resolveProductTitle(product),
|
|
451
|
+
status,
|
|
452
|
+
reasonCodes: uniqueReasonCodes(productCells)
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function buildResourceOptions(resources, cells, query) {
|
|
457
|
+
const queryResources = getQueryResources(resources, query);
|
|
458
|
+
return queryResources.map((resource) => {
|
|
459
|
+
const resourceCells = cells.filter((cell) => cell.resourceId !== void 0 && sameId(cell.resourceId, resource.id));
|
|
460
|
+
const bestCell = resourceCells.find((cell) => cell.status === "available") || resourceCells.find((cell) => cell.status === "limited") || resourceCells[0];
|
|
461
|
+
return {
|
|
462
|
+
resourceId: resource.id,
|
|
463
|
+
resourceName: resource.name,
|
|
464
|
+
resourceFormId: resource.formId,
|
|
465
|
+
resourceType: resource.resourceType,
|
|
466
|
+
status: resourceCells.length ? aggregateStatus(resourceCells) : "unavailable",
|
|
467
|
+
remainingCapacity: bestCell == null ? void 0 : bestCell.remainingCapacity,
|
|
468
|
+
maxCapacity: (bestCell == null ? void 0 : bestCell.maxCapacity) || resource.capacity,
|
|
469
|
+
reasonCodes: uniqueReasonCodes(resourceCells)
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function buildDateSummary(cells, query) {
|
|
474
|
+
const dates = query.date ? [query.date] : query.dateRange ? addDays(query.dateRange.start, query.dateRange.end) : Array.from(new Set(cells.map((cell) => cell.date).filter(Boolean)));
|
|
475
|
+
return dates.map((date) => {
|
|
476
|
+
const dateCells = cells.filter((cell) => cell.date === date);
|
|
477
|
+
const availableCount = dateCells.filter((cell) => cell.status === "available" || cell.status === "limited").length;
|
|
478
|
+
return {
|
|
479
|
+
date,
|
|
480
|
+
status: dateCells.length ? aggregateStatus(dateCells) : "unavailable",
|
|
481
|
+
availableCount,
|
|
482
|
+
totalCount: dateCells.length
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
function buildTimeLabels(date, context, query) {
|
|
487
|
+
if (!date)
|
|
488
|
+
return [];
|
|
489
|
+
const slotMinutes = query.slotDurationMinutes || context.slotDurationMinutes;
|
|
490
|
+
const labels = [];
|
|
491
|
+
let cursor = toDateTime(date, context.businessStartTime);
|
|
492
|
+
let end = toDateTime(date, context.businessEndTime);
|
|
493
|
+
if (!end.isAfter(cursor))
|
|
494
|
+
end = end.add(1, "day");
|
|
495
|
+
while (cursor.isBefore(end)) {
|
|
496
|
+
labels.push(cursor.format("HH:mm"));
|
|
497
|
+
cursor = cursor.add(slotMinutes, "minute");
|
|
498
|
+
}
|
|
499
|
+
return labels;
|
|
500
|
+
}
|
|
501
|
+
function buildGridSlotInterval(product, date, label, context, query) {
|
|
502
|
+
const slotMinutes = product.kind === "duration" ? Math.max(1, Number(product.durationMinutes || query.slotDurationMinutes || context.slotDurationMinutes)) : query.slotDurationMinutes || context.slotDurationMinutes;
|
|
503
|
+
const start = toDateTime(date, label);
|
|
504
|
+
const end = start.add(slotMinutes, "minute");
|
|
505
|
+
return {
|
|
506
|
+
id: `${product.id}:${date}:${label}`,
|
|
507
|
+
productId: product.id,
|
|
508
|
+
source: product.kind === "session" ? "session" : product.kind === "venue_slot" ? "venue_slot" : "duration",
|
|
509
|
+
date,
|
|
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
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function buildTimeGrid(context, query, cells) {
|
|
519
|
+
var _a;
|
|
520
|
+
const date = query.date || ((_a = query.dateRange) == null ? void 0 : _a.start);
|
|
521
|
+
const products = getQueryProducts(context.products, query).filter((product) => product.kind !== "normal");
|
|
522
|
+
const resources = getQueryResources(context.resources, query);
|
|
523
|
+
const timeLabels = buildTimeLabels(date, context, query);
|
|
524
|
+
if (!date || !products.length || !resources.length) {
|
|
525
|
+
return { date, timeLabels, resources: [] };
|
|
526
|
+
}
|
|
527
|
+
const rows = resources.map((resource) => {
|
|
528
|
+
const slots = timeLabels.map((label) => {
|
|
529
|
+
const product = products[0];
|
|
530
|
+
const interval = buildGridSlotInterval(product, date, label, context, query);
|
|
531
|
+
const start = (0, import_dayjs.default)(interval.start_at);
|
|
532
|
+
const end = (0, import_dayjs.default)(interval.end_at);
|
|
533
|
+
const cell = cells.find((item) => sameId(item.productId, product.id) && sameId(item.resourceId, resource.id) && item.date === date && (item.status === "available" || item.status === "limited") && containsRange((0, import_dayjs.default)(item.start_at), (0, import_dayjs.default)(item.end_at), start, end));
|
|
534
|
+
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
|
+
productId: product.id,
|
|
540
|
+
remainingCapacity: cell == null ? void 0 : cell.remainingCapacity,
|
|
541
|
+
maxCapacity: (cell == null ? void 0 : cell.maxCapacity) || resource.capacity,
|
|
542
|
+
reasonCodes: (cell == null ? void 0 : cell.reasonCodes) || ["outside_available_range"]
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
return {
|
|
546
|
+
resourceId: resource.id,
|
|
547
|
+
resourceName: resource.name,
|
|
548
|
+
resourceFormId: resource.formId,
|
|
549
|
+
slots
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
return { date, timeLabels, resources: rows };
|
|
553
|
+
}
|
|
554
|
+
function queryPlannerAvailability(contextInput, query = {}) {
|
|
555
|
+
const context = normalizePlannerContext(contextInput);
|
|
556
|
+
const intervals = buildDemandIntervals(context.products, query, context);
|
|
557
|
+
const resources = getQueryResources(context.resources, query);
|
|
558
|
+
const eventsByResource = collectEventsByResource(resources, context.externalEvents);
|
|
559
|
+
const cells = [];
|
|
560
|
+
for (const interval of intervals) {
|
|
561
|
+
const candidateResources = resources.filter((resource) => {
|
|
562
|
+
const requirements = interval.resourceRequirements || [];
|
|
563
|
+
if (!requirements.length)
|
|
564
|
+
return true;
|
|
565
|
+
return requirements.some((requirement) => matchesResourceRequirement(resource, requirement));
|
|
566
|
+
});
|
|
567
|
+
if (!candidateResources.length) {
|
|
568
|
+
cells.push({
|
|
569
|
+
key: `${interval.productId}:missing-resource:${interval.start_at}`,
|
|
570
|
+
productId: interval.productId,
|
|
571
|
+
date: interval.date,
|
|
572
|
+
start_at: interval.start_at,
|
|
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
|
+
});
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
candidateResources.forEach((resource) => {
|
|
589
|
+
if (interval.source === "duration" && !query.startTime) {
|
|
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
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
query,
|
|
602
|
+
demandIntervals: intervals,
|
|
603
|
+
productOptions: buildProductOptions(context.products, cells, query),
|
|
604
|
+
resourceOptions: buildResourceOptions(context.resources, cells, query),
|
|
605
|
+
dateSummary: buildDateSummary(cells, query),
|
|
606
|
+
timeGrid: buildTimeGrid(context, query, cells),
|
|
607
|
+
cells
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function getIntervalRequirements(interval) {
|
|
611
|
+
return interval.resourceRequirements.length ? interval.resourceRequirements : [{
|
|
612
|
+
capacityRequired: interval.capacityRequired,
|
|
613
|
+
required: true
|
|
614
|
+
}];
|
|
615
|
+
}
|
|
616
|
+
function buildAssignableCandidateGroups(params) {
|
|
617
|
+
const { interval, cells, resources } = params;
|
|
618
|
+
const conflicts = [];
|
|
619
|
+
const groups = getIntervalRequirements(interval).map((requirement) => {
|
|
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)
|
|
645
|
+
return;
|
|
646
|
+
conflicts.push({
|
|
647
|
+
type: "missing_resource",
|
|
648
|
+
message: "没有匹配商品需求的可用资源",
|
|
649
|
+
productId: interval.productId,
|
|
650
|
+
resourceId: (_b = (_a = interval.resourceRequirements[index]) == null ? void 0 : _a.resourceIds) == null ? void 0 : _b[0]
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
return { groups, conflicts };
|
|
654
|
+
}
|
|
655
|
+
function combineCandidateGroups(groups) {
|
|
656
|
+
if (!groups.length || groups.some((group) => group.length === 0))
|
|
657
|
+
return [];
|
|
658
|
+
return groups.reduce((combinations, group) => combinations.flatMap((combination) => group.map((candidate) => [...combination, candidate])), [[]]);
|
|
659
|
+
}
|
|
660
|
+
function buildSlotFromCandidates(params) {
|
|
661
|
+
const { interval, candidates, start, end } = params;
|
|
662
|
+
if (!end.isAfter(start))
|
|
663
|
+
return null;
|
|
664
|
+
const status = candidates.some((candidate) => candidate.cell.status === "limited") ? "limited" : "available";
|
|
665
|
+
const assignments = candidates.map((candidate) => ({
|
|
666
|
+
productId: interval.productId,
|
|
667
|
+
resourceId: candidate.resource.id,
|
|
668
|
+
start_at: start.toISOString(),
|
|
669
|
+
end_at: end.toISOString(),
|
|
670
|
+
capacityRequired: Math.max(1, Number(
|
|
671
|
+
candidate.requirement.capacityRequired || interval.capacityRequired || 1
|
|
672
|
+
))
|
|
673
|
+
}));
|
|
674
|
+
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
|
+
status,
|
|
689
|
+
resources: candidates.map((candidate) => ({
|
|
690
|
+
resourceId: candidate.resource.id,
|
|
691
|
+
resourceName: candidate.resource.name,
|
|
692
|
+
resourceFormId: candidate.resource.formId,
|
|
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: []
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function buildSlotsForInterval(params) {
|
|
705
|
+
const { interval, candidates, query, context } = params;
|
|
706
|
+
const start = candidates.reduce(
|
|
707
|
+
(current, candidate) => maxDayjs(current, (0, import_dayjs.default)(candidate.cell.start_at)),
|
|
708
|
+
(0, import_dayjs.default)(interval.start_at)
|
|
709
|
+
);
|
|
710
|
+
const end = candidates.reduce(
|
|
711
|
+
(current, candidate) => minDayjs(current, (0, import_dayjs.default)(candidate.cell.end_at)),
|
|
712
|
+
(0, import_dayjs.default)(interval.end_at)
|
|
713
|
+
);
|
|
714
|
+
const durationMinutes = Math.max(1, Number(interval.durationMinutes || 1));
|
|
715
|
+
if (interval.source !== "duration" || query.startTime) {
|
|
716
|
+
const fixedStart = (0, import_dayjs.default)(interval.start_at);
|
|
717
|
+
const fixedEnd = (0, import_dayjs.default)(interval.end_at);
|
|
718
|
+
if (containsRange(start, end, fixedStart, fixedEnd) || candidates.every((candidate) => candidate.cell.cellType === "fixed_interval")) {
|
|
719
|
+
const slot = buildSlotFromCandidates({
|
|
720
|
+
interval,
|
|
721
|
+
candidates,
|
|
722
|
+
start: fixedStart,
|
|
723
|
+
end: fixedEnd
|
|
724
|
+
});
|
|
725
|
+
return slot ? [slot] : [];
|
|
726
|
+
}
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
const stepMinutes = Math.max(1, Number(query.slotDurationMinutes || context.slotDurationMinutes || 30));
|
|
730
|
+
const slots = [];
|
|
731
|
+
let cursor = start;
|
|
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
|
|
739
|
+
});
|
|
740
|
+
if (slot)
|
|
741
|
+
slots.push(slot);
|
|
742
|
+
cursor = cursor.add(stepMinutes, "minute");
|
|
743
|
+
}
|
|
744
|
+
return slots;
|
|
745
|
+
}
|
|
746
|
+
function resolveAssignableSlots(contextInput, query = {}) {
|
|
747
|
+
const context = normalizePlannerContext(contextInput);
|
|
748
|
+
const projection = queryPlannerAvailability(context, query);
|
|
749
|
+
const resources = getQueryResources(context.resources, query);
|
|
750
|
+
const slotsByKey = /* @__PURE__ */ new Map();
|
|
751
|
+
const conflicts = [];
|
|
752
|
+
projection.demandIntervals.forEach((interval) => {
|
|
753
|
+
const { groups, conflicts: groupConflicts } = buildAssignableCandidateGroups({
|
|
754
|
+
interval,
|
|
755
|
+
cells: projection.cells,
|
|
756
|
+
resources
|
|
757
|
+
});
|
|
758
|
+
conflicts.push(...groupConflicts);
|
|
759
|
+
combineCandidateGroups(groups).forEach((candidates) => {
|
|
760
|
+
buildSlotsForInterval({
|
|
761
|
+
interval,
|
|
762
|
+
candidates,
|
|
763
|
+
query,
|
|
764
|
+
context
|
|
765
|
+
}).forEach((slot) => {
|
|
766
|
+
if (!slotsByKey.has(slot.key))
|
|
767
|
+
slotsByKey.set(slot.key, slot);
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
return {
|
|
772
|
+
projection,
|
|
773
|
+
slots: Array.from(slotsByKey.values()),
|
|
774
|
+
conflicts
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function autoAssignResources(contextInput, query = {}) {
|
|
778
|
+
const baseContext = normalizePlannerContext(contextInput);
|
|
779
|
+
const projection = queryPlannerAvailability(baseContext, query);
|
|
780
|
+
const assignments = [];
|
|
781
|
+
const conflicts = [];
|
|
782
|
+
const missingTimeProductIds = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const interval of projection.demandIntervals) {
|
|
784
|
+
if (interval.source === "duration" && !query.startTime) {
|
|
785
|
+
const productKey = String(interval.productId);
|
|
786
|
+
if (!missingTimeProductIds.has(productKey)) {
|
|
787
|
+
missingTimeProductIds.add(productKey);
|
|
788
|
+
conflicts.push({
|
|
789
|
+
type: "missing_time",
|
|
790
|
+
message: "duration 商品需要先确定开始时间后才能自动分配资源",
|
|
791
|
+
productId: interval.productId
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const contextWithAssignments = withAssignmentEvents(baseContext, assignments);
|
|
797
|
+
const candidateResources = getQueryResources(contextWithAssignments.resources, query).filter((resource) => {
|
|
798
|
+
const requirements = interval.resourceRequirements || [];
|
|
799
|
+
if (!requirements.length)
|
|
800
|
+
return true;
|
|
801
|
+
return requirements.some((requirement) => matchesResourceRequirement(resource, requirement));
|
|
802
|
+
});
|
|
803
|
+
const availableCell = candidateResources.map((resource) => evaluateResourceForInterval({
|
|
804
|
+
resource,
|
|
805
|
+
interval,
|
|
806
|
+
context: contextWithAssignments
|
|
807
|
+
})).find((cell) => cell.status === "available" || cell.status === "limited");
|
|
808
|
+
if (!(availableCell == null ? void 0 : availableCell.resourceId)) {
|
|
809
|
+
conflicts.push({
|
|
810
|
+
type: "missing_resource",
|
|
811
|
+
message: "无法为商品自动分配可用资源",
|
|
812
|
+
productId: interval.productId
|
|
813
|
+
});
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
assignments.push({
|
|
817
|
+
productId: interval.productId,
|
|
818
|
+
resourceId: availableCell.resourceId,
|
|
819
|
+
start_at: interval.start_at,
|
|
820
|
+
end_at: interval.end_at,
|
|
821
|
+
capacityRequired: interval.capacityRequired
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return { assignments, conflicts };
|
|
825
|
+
}
|
|
826
|
+
function withAssignmentEvents(context, assignments) {
|
|
827
|
+
if (!assignments.length)
|
|
828
|
+
return context;
|
|
829
|
+
return {
|
|
830
|
+
...context,
|
|
831
|
+
externalEvents: [
|
|
832
|
+
...context.externalEvents,
|
|
833
|
+
...assignments.map((assignment) => ({
|
|
834
|
+
resourceId: assignment.resourceId,
|
|
835
|
+
start_at: assignment.start_at,
|
|
836
|
+
end_at: assignment.end_at,
|
|
837
|
+
pax: assignment.capacityRequired || 1,
|
|
838
|
+
source: "selection"
|
|
839
|
+
}))
|
|
840
|
+
]
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function validateAssignments(contextInput, selection) {
|
|
844
|
+
if (selection.assignments.length > 0) {
|
|
845
|
+
const context = normalizePlannerContext(contextInput);
|
|
846
|
+
const conflicts2 = [];
|
|
847
|
+
for (let index = 0; index < selection.assignments.length; index += 1) {
|
|
848
|
+
const assignment = selection.assignments[index];
|
|
849
|
+
const product = context.products.find((item) => sameId(item.id, assignment.productId));
|
|
850
|
+
const resource = context.resources.find((item) => sameId(item.id, assignment.resourceId));
|
|
851
|
+
if (!product) {
|
|
852
|
+
conflicts2.push({
|
|
853
|
+
type: "missing_resource",
|
|
854
|
+
message: "assignment 对应的商品不存在",
|
|
855
|
+
productId: assignment.productId
|
|
856
|
+
});
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (!resource) {
|
|
860
|
+
conflicts2.push({
|
|
861
|
+
type: "missing_resource",
|
|
862
|
+
message: "assignment 对应的资源不存在",
|
|
863
|
+
productId: assignment.productId,
|
|
864
|
+
resourceId: assignment.resourceId
|
|
865
|
+
});
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const start = (0, import_dayjs.default)(assignment.start_at);
|
|
869
|
+
const end = (0, import_dayjs.default)(assignment.end_at);
|
|
870
|
+
const interval = {
|
|
871
|
+
id: `${assignment.productId}:${assignment.resourceId}:${assignment.start_at}:${assignment.end_at}`,
|
|
872
|
+
productId: assignment.productId,
|
|
873
|
+
source: product.kind === "session" ? "session" : product.kind === "venue_slot" ? "venue_slot" : "duration",
|
|
874
|
+
date: start.format("YYYY-MM-DD"),
|
|
875
|
+
start_at: assignment.start_at,
|
|
876
|
+
end_at: assignment.end_at,
|
|
877
|
+
durationMinutes: Math.max(1, end.diff(start, "minute")),
|
|
878
|
+
quantity: Math.max(1, Number(product.quantity || 1)),
|
|
879
|
+
capacityRequired: Math.max(1, Number(assignment.capacityRequired || product.capacityRequired || 1)),
|
|
880
|
+
resourceRequirements: product.resourceRequirements || []
|
|
881
|
+
};
|
|
882
|
+
const cell = evaluateResourceForInterval({ resource, interval, context });
|
|
883
|
+
if (cell.status !== "available" && cell.status !== "limited") {
|
|
884
|
+
conflicts2.push(...cell.conflicts);
|
|
885
|
+
}
|
|
886
|
+
const priorContext = withAssignmentEvents(
|
|
887
|
+
context,
|
|
888
|
+
selection.assignments.slice(0, index)
|
|
889
|
+
);
|
|
890
|
+
const selfConflictCell = evaluateResourceForInterval({
|
|
891
|
+
resource,
|
|
892
|
+
interval,
|
|
893
|
+
context: priorContext
|
|
894
|
+
});
|
|
895
|
+
if (selfConflictCell.status !== "available" && selfConflictCell.status !== "limited") {
|
|
896
|
+
conflicts2.push(...selfConflictCell.conflicts.map((conflict) => ({
|
|
897
|
+
...conflict,
|
|
898
|
+
type: "cart_overlap",
|
|
899
|
+
message: "本次选择中存在资源时间冲突"
|
|
900
|
+
})));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
ok: conflicts2.length === 0,
|
|
905
|
+
conflicts: conflicts2
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const projection = queryPlannerAvailability(contextInput, {
|
|
909
|
+
productIds: selection.productIds,
|
|
910
|
+
date: selection.date,
|
|
911
|
+
startTime: selection.startTime,
|
|
912
|
+
endTime: selection.endTime,
|
|
913
|
+
resourceIds: selection.resourceIds,
|
|
914
|
+
mode: selection.mode
|
|
915
|
+
});
|
|
916
|
+
const conflicts = projection.cells.filter((cell) => cell.status !== "available" && cell.status !== "limited").flatMap((cell) => cell.conflicts);
|
|
917
|
+
return {
|
|
918
|
+
ok: conflicts.length === 0,
|
|
919
|
+
conflicts
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
923
|
+
0 && (module.exports = {
|
|
924
|
+
DEFAULT_RESOURCE_PLANNER_CONTEXT,
|
|
925
|
+
DEFAULT_RESOURCE_PLANNER_SELECTION,
|
|
926
|
+
autoAssignResources,
|
|
927
|
+
buildDemandIntervals,
|
|
928
|
+
evaluateResourceForInterval,
|
|
929
|
+
normalizePlannerContext,
|
|
930
|
+
queryPlannerAvailability,
|
|
931
|
+
resolveAssignableSlots,
|
|
932
|
+
validateAssignments
|
|
933
|
+
});
|