@pisell/pisellos 2.3.144 → 2.3.145
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/BookingContext/utils/productExtend.d.ts +7 -0
- package/dist/modules/BookingContext/utils/productExtend.js +62 -18
- package/dist/modules/Order/index.d.ts +1 -0
- package/dist/modules/Order/index.js +380 -198
- package/dist/modules/Order/types.d.ts +9 -0
- package/dist/modules/ResourcePlanner/planner.d.ts +2 -0
- package/dist/modules/ResourcePlanner/planner.js +327 -22
- package/dist/modules/ResourcePlanner/types.d.ts +29 -0
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
- package/dist/solution/BookingTicket/utils/addProductDecision.js +1 -1
- package/dist/solution/UnifiedBookingSales/index.d.ts +3 -0
- package/dist/solution/UnifiedBookingSales/index.js +432 -163
- package/dist/solution/UnifiedBookingSales/types.d.ts +14 -2
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/BookingContext/utils/productExtend.d.ts +7 -0
- package/lib/modules/BookingContext/utils/productExtend.js +45 -2
- package/lib/modules/Order/index.d.ts +1 -0
- package/lib/modules/Order/index.js +92 -10
- package/lib/modules/Order/types.d.ts +9 -0
- package/lib/modules/ResourcePlanner/planner.d.ts +2 -0
- package/lib/modules/ResourcePlanner/planner.js +247 -7
- package/lib/modules/ResourcePlanner/types.d.ts +29 -0
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
- package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -1
- package/lib/solution/UnifiedBookingSales/index.d.ts +3 -0
- package/lib/solution/UnifiedBookingSales/index.js +220 -27
- package/lib/solution/UnifiedBookingSales/types.d.ts +14 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.createAvailabilityProjection = createAvailabilityProjection;
|
|
7
|
+
exports.getDurationBusinessDate = getDurationBusinessDate;
|
|
7
8
|
exports.getProductTimeRanges = getProductTimeRanges;
|
|
8
9
|
exports.queryAvailabilityProjection = queryAvailabilityProjection;
|
|
9
10
|
exports.validateAvailabilitySelection = validateAvailabilitySelection;
|
|
@@ -250,6 +251,18 @@ function evaluateCandidateTimeGate(params) {
|
|
|
250
251
|
};
|
|
251
252
|
}
|
|
252
253
|
const policy = product.cutOffPolicy;
|
|
254
|
+
// Current duration items can be shown outside business hours. Their real
|
|
255
|
+
// interval must remain unavailable, including products with no resource groups.
|
|
256
|
+
if (evaluationContext.useDurationTimeGrid && product.kind === 'duration' && !projection.meta.defaults.operatingHours.windows.some(window => {
|
|
257
|
+
const range = normalizeAbsoluteRange(window);
|
|
258
|
+
return range && contains(range, candidateRange);
|
|
259
|
+
})) {
|
|
260
|
+
return {
|
|
261
|
+
status: 'unavailable',
|
|
262
|
+
reasonCodes: ['outside_operating_hours'],
|
|
263
|
+
conflictCode: 'booking_cutoff'
|
|
264
|
+
};
|
|
265
|
+
}
|
|
253
266
|
if (!policy) return null;
|
|
254
267
|
if (resolveCutOffPolicyIssue(policy)) {
|
|
255
268
|
return {
|
|
@@ -286,8 +299,8 @@ function evaluateCandidateTimeGate(params) {
|
|
|
286
299
|
// otherwise a minute refresh turns it into 0 capacity and incorrectly
|
|
287
300
|
// advances a create flow to the next slot. The earlier `past` gate still
|
|
288
301
|
// rejects the anchor once its booking range has ended.
|
|
289
|
-
const isActivePosDurationAnchor = evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration' && !product.isFlexibleDuration && evaluationContext.durationCandidateAnchorAtMs === candidateRange.start;
|
|
290
|
-
const usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
|
|
302
|
+
const isActivePosDurationAnchor = evaluationContext.useExactDurationCandidateMinute && !evaluationContext.enforceSaleCutoff && product.kind === 'duration' && !product.isFlexibleDuration && evaluationContext.durationCandidateAnchorAtMs === candidateRange.start;
|
|
303
|
+
const usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useDurationTimeGrid || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
|
|
291
304
|
const hasNotReachedStart = isActivePosDurationAnchor || (usesMinutePrecision ? floorToMinute(now) <= candidateRange.start : now < candidateRange.start);
|
|
292
305
|
if (hasNotReachedStart) return null;
|
|
293
306
|
return {
|
|
@@ -489,11 +502,34 @@ function normalizeOperatingHours(params) {
|
|
|
489
502
|
}));
|
|
490
503
|
const windows = normalizeWindows(sourceWindows);
|
|
491
504
|
const excludedWindows = normalizeWindows(params.input?.excludedWindows || []);
|
|
505
|
+
// Keep original dates and opening anchors separately: the availability
|
|
506
|
+
// windows below may be clipped at midnight or merged without segment identity.
|
|
507
|
+
const periodMap = new Map();
|
|
508
|
+
const originalPeriods = params.input?.businessPeriods || [...sourceWindows, ...(params.input?.excludedWindows || [])].map(window => ({
|
|
509
|
+
businessDate: window.businessDate || window.startAt.slice(0, 10),
|
|
510
|
+
startAt: window.openingAnchorAt || window.startAt,
|
|
511
|
+
endAt: window.endAt
|
|
512
|
+
}));
|
|
513
|
+
originalPeriods.forEach(period => {
|
|
514
|
+
const range = normalizeAbsoluteRange(period);
|
|
515
|
+
if (!range) return;
|
|
516
|
+
const previous = periodMap.get(period.businessDate);
|
|
517
|
+
periodMap.set(period.businessDate, {
|
|
518
|
+
start: previous ? Math.min(previous.start, range.start) : range.start,
|
|
519
|
+
end: previous ? Math.max(previous.end, range.end) : range.end
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
const businessPeriods = Array.from(periodMap, ([businessDate, range]) => ({
|
|
523
|
+
businessDate,
|
|
524
|
+
startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
|
|
525
|
+
endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
|
|
526
|
+
})).sort((left, right) => left.startAt.localeCompare(right.startAt));
|
|
492
527
|
return {
|
|
493
528
|
source: params.input?.source || params.fallbackSource,
|
|
494
529
|
scheduleIds: uniqById(params.input?.scheduleIds || []),
|
|
495
530
|
missingScheduleIds: uniqById(params.input?.missingScheduleIds || []),
|
|
496
531
|
windows,
|
|
532
|
+
businessPeriods,
|
|
497
533
|
...(excludedWindows.length > 0 ? {
|
|
498
534
|
excludedWindows
|
|
499
535
|
} : {})
|
|
@@ -992,6 +1028,162 @@ function createDurationTimeRangeWithEnd(product, start, bookingEnd, projection,
|
|
|
992
1028
|
scheduleRefs: []
|
|
993
1029
|
};
|
|
994
1030
|
}
|
|
1031
|
+
|
|
1032
|
+
/** Resolve the label without changing the real calendar timestamp. */
|
|
1033
|
+
function getDurationBusinessDate(projection, startAt) {
|
|
1034
|
+
const start = toDateTime(startAt);
|
|
1035
|
+
const periods = projection.meta.defaults.operatingHours.businessPeriods || [];
|
|
1036
|
+
const period = [...periods].reverse().find(item => {
|
|
1037
|
+
const range = normalizeAbsoluteRange(item);
|
|
1038
|
+
return range && range.start <= start && start < range.end;
|
|
1039
|
+
});
|
|
1040
|
+
return period?.businessDate || startAt.slice(0, 10);
|
|
1041
|
+
}
|
|
1042
|
+
function getAlignedDurationRanges(params) {
|
|
1043
|
+
const {
|
|
1044
|
+
projection,
|
|
1045
|
+
product,
|
|
1046
|
+
request,
|
|
1047
|
+
runtimeOptions,
|
|
1048
|
+
dateRange,
|
|
1049
|
+
operatingRanges,
|
|
1050
|
+
baseRanges,
|
|
1051
|
+
resourceWindowIndex,
|
|
1052
|
+
fixedOffsetMinutes
|
|
1053
|
+
} = params;
|
|
1054
|
+
const step = projection.meta.defaults.slotStepMinutes;
|
|
1055
|
+
const now = floorToMinute(toDateTime(runtimeOptions.evaluatedAt || projection.meta.durationCandidateAnchorAt));
|
|
1056
|
+
if (!Number.isFinite(now)) throw availabilityError('INVALID_TARGET_FILTER', 'evaluatedAt 必须是有效时间');
|
|
1057
|
+
const nowAt = formatDateTime(now, projection.meta.timezone, fixedOffsetMinutes);
|
|
1058
|
+
const currentBusinessDate = getDurationBusinessDate(projection, nowAt);
|
|
1059
|
+
const inRequestedDays = date => date >= dateRange.startDate && date <= dateRange.endDate;
|
|
1060
|
+
const periods = (projection.meta.defaults.operatingHours.businessPeriods || []).filter(period => inRequestedDays(period.businessDate));
|
|
1061
|
+
// A prepared month includes tomorrow's windows too; the single-day UI must
|
|
1062
|
+
// receive the selected business cycle's end, not tomorrow's closing time.
|
|
1063
|
+
const operatingEndAt = dateRange.startDate === dateRange.endDate && periods.length ? periods.reduce((latest, period) => period.endAt > latest ? period.endAt : latest, periods[0].endAt) : undefined;
|
|
1064
|
+
const ranges = [];
|
|
1065
|
+
const duration = resolveRequestedDurationMinutes(product, request);
|
|
1066
|
+
const addRange = (start, end, businessDate) => {
|
|
1067
|
+
if (end <= start) return;
|
|
1068
|
+
const candidate = createDurationTimeRangeWithEnd(product, start, end, projection, fixedOffsetMinutes);
|
|
1069
|
+
candidate.businessDate = businessDate || getDurationBusinessDate(projection, candidate.startAt);
|
|
1070
|
+
if (inRequestedDays(candidate.businessDate)) ranges.push(candidate);
|
|
1071
|
+
};
|
|
1072
|
+
// A flexible end comes from resources that actually cover this exact start,
|
|
1073
|
+
// never from the next future window. Keep alternative real ends separate.
|
|
1074
|
+
const flexibleEnds = start => {
|
|
1075
|
+
const operating = operatingRanges.find(range => range.start <= start && start < range.end);
|
|
1076
|
+
if (!operating) return [];
|
|
1077
|
+
const businessDate = getDurationBusinessDate(projection, formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes));
|
|
1078
|
+
const period = projection.meta.defaults.operatingHours.businessPeriods?.find(item => item.businessDate === businessDate);
|
|
1079
|
+
const operatingEnd = Math.min(operating.end, period ? toDateTime(period.endAt) : operating.end);
|
|
1080
|
+
const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false);
|
|
1081
|
+
const groups = requiredGroups.map(group => projection.resources.filter(resource => matchesRequirementGroup(group, resource, product, request.partySize)).filter(resource => !request.resourceIds?.length || request.resourceIds.some(id => sameId(id, resource.id))).flatMap(resource => (resourceWindowIndex.get(String(resource.id)) || []).filter(range => range.start <= start && start < range.end).map(range => Math.min(range.end, operatingEnd))));
|
|
1082
|
+
if (groups.some(ends => ends.length === 0)) return [];
|
|
1083
|
+
if (!groups.length) return [operatingEnd];
|
|
1084
|
+
return Array.from(new Set(groups.flat())).filter(end => groups.every(ends => ends.some(availableEnd => availableEnd >= end))).sort((left, right) => left - right);
|
|
1085
|
+
};
|
|
1086
|
+
if (product.isFlexibleDuration) {
|
|
1087
|
+
// Preserve the existing continuous-window UI, with the business-day label.
|
|
1088
|
+
periods.forEach(period => {
|
|
1089
|
+
const periodRange = normalizeAbsoluteRange(period);
|
|
1090
|
+
baseRanges.forEach(range => {
|
|
1091
|
+
const start = Math.max(range.start, periodRange.start, now);
|
|
1092
|
+
const end = Math.min(range.end, periodRange.end);
|
|
1093
|
+
if (start < end) addRange(start, end, period.businessDate);
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
} else {
|
|
1097
|
+
periods.forEach(period => {
|
|
1098
|
+
const periodRange = normalizeAbsoluteRange(period);
|
|
1099
|
+
const anchor = 60 % step === 0 ? toDateStart(period.businessDate) : periodRange.start;
|
|
1100
|
+
baseRanges.forEach(range => {
|
|
1101
|
+
const lower = Math.max(range.start, periodRange.start, now);
|
|
1102
|
+
const upper = Math.min(range.end, periodRange.end);
|
|
1103
|
+
let cursor = anchor + Math.ceil((lower - anchor) / (step * 60000)) * step * 60000;
|
|
1104
|
+
for (; cursor + duration * 60000 <= upper; cursor += step * 60000) {
|
|
1105
|
+
addRange(cursor, cursor + duration * 60000, period.businessDate);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
let unavailableCurrentTime;
|
|
1111
|
+
if (inRequestedDays(currentBusinessDate)) {
|
|
1112
|
+
if (product.isFlexibleDuration) {
|
|
1113
|
+
const ends = flexibleEnds(now);
|
|
1114
|
+
ends.forEach(end => addRange(now, end, currentBusinessDate));
|
|
1115
|
+
if (!ends.length) {
|
|
1116
|
+
unavailableCurrentTime = {
|
|
1117
|
+
productId: product.id,
|
|
1118
|
+
startAt: nowAt,
|
|
1119
|
+
date: formatDate(now),
|
|
1120
|
+
businessDate: currentBusinessDate,
|
|
1121
|
+
startTime: formatTime(now),
|
|
1122
|
+
status: 'unavailable',
|
|
1123
|
+
reasonCodes: [operatingRanges.some(range => range.start <= now && now < range.end) ? 'outside_resource_window' : 'outside_operating_hours']
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
// Even outside a working window the fixed product has a real end. Query
|
|
1128
|
+
// reports unavailable; POS manual capabilities remain a separate policy.
|
|
1129
|
+
addRange(now, now + duration * 60000, currentBusinessDate);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const rememberedAt = request.rememberedStartAtByProductId?.[String(product.id)];
|
|
1133
|
+
if (rememberedAt !== undefined) {
|
|
1134
|
+
const start = toDateTime(rememberedAt);
|
|
1135
|
+
if (!Number.isFinite(start)) throw availabilityError('INVALID_TARGET_FILTER', '记忆开始时间必须是完整日期时间');
|
|
1136
|
+
if (product.isFlexibleDuration) {
|
|
1137
|
+
flexibleEnds(start).forEach(end => addRange(start, end));
|
|
1138
|
+
} else {
|
|
1139
|
+
addRange(start, start + duration * 60000);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const editingAt = request.editingStartAtByProductId?.[String(product.id)];
|
|
1143
|
+
if (editingAt !== undefined) {
|
|
1144
|
+
const start = toDateTime(editingAt);
|
|
1145
|
+
const editingDuration = Number(request.durationMinutesByProductId?.[String(product.id)]);
|
|
1146
|
+
if (!Number.isFinite(start) || !Number.isInteger(editingDuration) || editingDuration <= 0) {
|
|
1147
|
+
throw availabilityError('INVALID_TARGET_FILTER', '编辑时间必须包含完整开始日期时间和明确的正整数时长');
|
|
1148
|
+
}
|
|
1149
|
+
// Editing restores the original interval, including its actual overnight
|
|
1150
|
+
// date. It neither adopts the current grid nor recomputes a flexible end.
|
|
1151
|
+
if (request.allowOutsideOperatingHoursForRequestedStartTimes || operatingRanges.some(range => range.start <= start && start < range.end)) {
|
|
1152
|
+
addRange(start, addMinutesZoned(start, editingDuration, projection.meta.timezone));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Explicit custom input keeps its existing start + requested duration
|
|
1157
|
+
// semantics. Remembered input above must never use this default-30 path.
|
|
1158
|
+
if (dateRange.startDate === dateRange.endDate) {
|
|
1159
|
+
resolveRequestedDurationStartTimes(product, request).forEach(startTime => {
|
|
1160
|
+
const sameDayStart = toDateTimeInDate(dateRange.startDate, startTime);
|
|
1161
|
+
const period = periods.find(item => item.businessDate === dateRange.startDate);
|
|
1162
|
+
const periodRange = period ? normalizeAbsoluteRange(period) : null;
|
|
1163
|
+
const nextDayStart = (0, _localClock.addLocalCalendarDays)(sameDayStart, 1);
|
|
1164
|
+
// HH:mm is entered within a selected business day, whose early hours
|
|
1165
|
+
// may belong to the next calendar day. Do not move other out-of-hours
|
|
1166
|
+
// manual input to tomorrow merely because the business closes overnight.
|
|
1167
|
+
const start = periodRange && !(periodRange.start <= sameDayStart && sameDayStart < periodRange.end) && periodRange.start <= nextDayStart && nextDayStart < periodRange.end ? nextDayStart : sameDayStart;
|
|
1168
|
+
if (request.allowOutsideOperatingHoursForRequestedStartTimes || operatingRanges.some(range => range.start <= start && start < range.end)) {
|
|
1169
|
+
addRange(start, start + duration * 60000);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
productId: product.id,
|
|
1175
|
+
title: product.title,
|
|
1176
|
+
kind: product.kind,
|
|
1177
|
+
slotStepMinutes: step,
|
|
1178
|
+
...(operatingEndAt ? {
|
|
1179
|
+
operatingEndAt
|
|
1180
|
+
} : {}),
|
|
1181
|
+
ranges: dedupeProductRanges(ranges),
|
|
1182
|
+
...(unavailableCurrentTime ? {
|
|
1183
|
+
unavailableCurrentTime
|
|
1184
|
+
} : {})
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
995
1187
|
function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
996
1188
|
const normalizedPartySize = normalizePartySize(request.partySize);
|
|
997
1189
|
const dateRange = resolveProjectionDateRange(projection, request.dateRange);
|
|
@@ -1044,6 +1236,19 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
1044
1236
|
includeExcludedWindows: allowExcludedScheduleDateOverride
|
|
1045
1237
|
})).reduce((current, ranges) => current === null ? ranges : intersectRangeLists(current, ranges), null) || [] : null;
|
|
1046
1238
|
const baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
|
|
1239
|
+
if (runtimeOptions.useDurationTimeGrid) {
|
|
1240
|
+
return getAlignedDurationRanges({
|
|
1241
|
+
projection,
|
|
1242
|
+
product,
|
|
1243
|
+
request,
|
|
1244
|
+
runtimeOptions,
|
|
1245
|
+
dateRange,
|
|
1246
|
+
operatingRanges,
|
|
1247
|
+
baseRanges,
|
|
1248
|
+
resourceWindowIndex,
|
|
1249
|
+
fixedOffsetMinutes
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1047
1252
|
const durationMinutes = resolveRequestedDurationMinutes(product, request);
|
|
1048
1253
|
const flexibleStartAt = (() => {
|
|
1049
1254
|
if (!product.isFlexibleDuration) return undefined;
|
|
@@ -1265,7 +1470,9 @@ function buildEvaluationContext(params) {
|
|
|
1265
1470
|
selectionsByGroupKey,
|
|
1266
1471
|
useExactDurationCandidateMinute: params.useExactDurationCandidateMinute === true,
|
|
1267
1472
|
durationCandidateAnchorAtMs: Number.isFinite(durationCandidateAnchorAt) ? floorToMinute(durationCandidateAnchorAt) : null,
|
|
1268
|
-
allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true
|
|
1473
|
+
allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true,
|
|
1474
|
+
useDurationTimeGrid: params.useDurationTimeGrid === true,
|
|
1475
|
+
enforceSaleCutoff: params.enforceSaleCutoff === true
|
|
1269
1476
|
};
|
|
1270
1477
|
}
|
|
1271
1478
|
function summarizeStatuses(statuses) {
|
|
@@ -1672,6 +1879,8 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
|
|
|
1672
1879
|
projection,
|
|
1673
1880
|
evaluatedAt,
|
|
1674
1881
|
resourceIds: request.resourceIds,
|
|
1882
|
+
useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
|
|
1883
|
+
enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
|
|
1675
1884
|
useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
|
|
1676
1885
|
allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
|
|
1677
1886
|
});
|
|
@@ -1748,6 +1957,8 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
|
|
|
1748
1957
|
partySize: normalizedPartySize
|
|
1749
1958
|
}, {
|
|
1750
1959
|
evaluatedAt,
|
|
1960
|
+
useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
|
|
1961
|
+
enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
|
|
1751
1962
|
useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
|
|
1752
1963
|
allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
|
|
1753
1964
|
});
|
|
@@ -1772,7 +1983,8 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
|
|
|
1772
1983
|
candidateRangeCache,
|
|
1773
1984
|
evaluationContext
|
|
1774
1985
|
});
|
|
1775
|
-
const
|
|
1986
|
+
const candidateDate = runtimeOptions.useDurationTimeGrid ? candidate.businessDate || candidate.date : candidate.date;
|
|
1987
|
+
const aggregate = byDate.get(candidateDate) || {
|
|
1776
1988
|
availableCount: 0,
|
|
1777
1989
|
totalCount: 0,
|
|
1778
1990
|
maxPartySize: 0,
|
|
@@ -1790,8 +2002,24 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
|
|
|
1790
2002
|
aggregate.maxCapacity += evaluated.maxCapacity;
|
|
1791
2003
|
pushStatusAggregate(aggregate.statusAggregate, evaluated.status);
|
|
1792
2004
|
evaluated.reasonCodes.forEach(reasonCode => aggregate.reasonCodeSet.add(reasonCode));
|
|
1793
|
-
byDate.set(
|
|
2005
|
+
byDate.set(candidateDate, aggregate);
|
|
1794
2006
|
});
|
|
2007
|
+
if (group.unavailableCurrentTime) {
|
|
2008
|
+
const current = group.unavailableCurrentTime;
|
|
2009
|
+
const aggregate = byDate.get(current.businessDate) || {
|
|
2010
|
+
availableCount: 0,
|
|
2011
|
+
totalCount: 0,
|
|
2012
|
+
maxPartySize: 0,
|
|
2013
|
+
remainingCapacity: 0,
|
|
2014
|
+
maxCapacity: 0,
|
|
2015
|
+
reasonCodeSet: new Set(),
|
|
2016
|
+
statusAggregate: createStatusAggregate()
|
|
2017
|
+
};
|
|
2018
|
+
aggregate.totalCount += 1;
|
|
2019
|
+
pushStatusAggregate(aggregate.statusAggregate, 'unavailable');
|
|
2020
|
+
current.reasonCodes.forEach(reason => aggregate.reasonCodeSet.add(reason));
|
|
2021
|
+
byDate.set(current.businessDate, aggregate);
|
|
2022
|
+
}
|
|
1795
2023
|
const dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(date => {
|
|
1796
2024
|
const aggregate = byDate.get(date);
|
|
1797
2025
|
return {
|
|
@@ -1849,10 +2077,20 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
|
|
|
1849
2077
|
maxPartySize = Math.max(maxPartySize, evaluated.maxPartySize);
|
|
1850
2078
|
pushStatusAggregate(productStatusAggregate, evaluated.status);
|
|
1851
2079
|
evaluated.reasonCodes.forEach(reasonCode => productReasonCodeSet.add(reasonCode));
|
|
1852
|
-
const
|
|
2080
|
+
const candidateDate = runtimeOptions.useDurationTimeGrid ? candidate.businessDate || candidate.date : candidate.date;
|
|
2081
|
+
const dateAggregate = dateStatusMap.get(candidateDate) || createStatusAggregate();
|
|
1853
2082
|
pushStatusAggregate(dateAggregate, evaluated.status);
|
|
1854
|
-
dateStatusMap.set(
|
|
2083
|
+
dateStatusMap.set(candidateDate, dateAggregate);
|
|
1855
2084
|
});
|
|
2085
|
+
if (group.unavailableCurrentTime) {
|
|
2086
|
+
const current = group.unavailableCurrentTime;
|
|
2087
|
+
totalTimeCount += 1;
|
|
2088
|
+
pushStatusAggregate(productStatusAggregate, 'unavailable');
|
|
2089
|
+
current.reasonCodes.forEach(reason => productReasonCodeSet.add(reason));
|
|
2090
|
+
const dateAggregate = dateStatusMap.get(current.businessDate) || createStatusAggregate();
|
|
2091
|
+
pushStatusAggregate(dateAggregate, 'unavailable');
|
|
2092
|
+
dateStatusMap.set(current.businessDate, dateAggregate);
|
|
2093
|
+
}
|
|
1856
2094
|
const availableDateCount = Array.from(dateStatusMap.values()).filter(aggregate => {
|
|
1857
2095
|
const status = summarizeStatusAggregate(aggregate);
|
|
1858
2096
|
return status === 'available' || status === 'limited';
|
|
@@ -1921,6 +2159,8 @@ function validateAvailabilitySelection(projection, params, runtimeOptions = {})
|
|
|
1921
2159
|
projection,
|
|
1922
2160
|
evaluatedAt,
|
|
1923
2161
|
requirementSelections: params.requirementSelections,
|
|
2162
|
+
useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
|
|
2163
|
+
enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
|
|
1924
2164
|
useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
|
|
1925
2165
|
allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
|
|
1926
2166
|
})
|
|
@@ -92,6 +92,9 @@ export interface AvailabilityProduct {
|
|
|
92
92
|
raw?: Record<string, any>;
|
|
93
93
|
}
|
|
94
94
|
export interface AvailabilityResourceWindow extends AvailabilityAbsoluteRange {
|
|
95
|
+
/** Original business day and opening anchor, before coverage clipping. */
|
|
96
|
+
businessDate?: string;
|
|
97
|
+
openingAnchorAt?: string;
|
|
95
98
|
source?: string;
|
|
96
99
|
raw?: Record<string, any>;
|
|
97
100
|
}
|
|
@@ -101,10 +104,15 @@ export interface AvailabilityOperatingHoursSnapshot {
|
|
|
101
104
|
scheduleIds: AvailabilityId[];
|
|
102
105
|
missingScheduleIds: AvailabilityId[];
|
|
103
106
|
windows: AvailabilityResourceWindow[];
|
|
107
|
+
/** Complete business cycles; lunch gaps remain in windows, not in this envelope. */
|
|
108
|
+
businessPeriods?: AvailabilityBusinessPeriod[];
|
|
104
109
|
/** Schedule windows omitted only because their date is excluded. POS may use
|
|
105
110
|
* these to surface an unavailable, manually selectable candidate. */
|
|
106
111
|
excludedWindows?: AvailabilityResourceWindow[];
|
|
107
112
|
}
|
|
113
|
+
export interface AvailabilityBusinessPeriod extends AvailabilityAbsoluteRange {
|
|
114
|
+
businessDate: string;
|
|
115
|
+
}
|
|
108
116
|
export interface AvailabilityResource {
|
|
109
117
|
id: AvailabilityId;
|
|
110
118
|
formId?: AvailabilityId;
|
|
@@ -212,6 +220,17 @@ export interface ProductTimeRange extends AvailabilityDisplayRange {
|
|
|
212
220
|
bookingEndAt: string;
|
|
213
221
|
scheduleRefs: AvailabilityScheduleRef[];
|
|
214
222
|
primaryScheduleRef?: AvailabilityScheduleRef;
|
|
223
|
+
/** Business-day label. date/startAt keep the actual calendar date. */
|
|
224
|
+
businessDate?: string;
|
|
225
|
+
}
|
|
226
|
+
export interface UnavailableCurrentTime {
|
|
227
|
+
productId: AvailabilityId;
|
|
228
|
+
startAt: string;
|
|
229
|
+
date: string;
|
|
230
|
+
businessDate: string;
|
|
231
|
+
startTime: string;
|
|
232
|
+
status: 'unavailable';
|
|
233
|
+
reasonCodes: string[];
|
|
215
234
|
}
|
|
216
235
|
export interface ProductTimeRangeGroup {
|
|
217
236
|
productId: AvailabilityId;
|
|
@@ -221,6 +240,8 @@ export interface ProductTimeRangeGroup {
|
|
|
221
240
|
/** 单日查询对应营业窗口的最终结束时刻,不受资源排班是否为空影响。 */
|
|
222
241
|
operatingEndAt?: string;
|
|
223
242
|
ranges: ProductTimeRange[];
|
|
243
|
+
/** A display-only current item when flexible duration has no valid end. Never submit it. */
|
|
244
|
+
unavailableCurrentTime?: UnavailableCurrentTime;
|
|
224
245
|
}
|
|
225
246
|
export interface AvailabilityAssignment extends AvailabilityAbsoluteRange {
|
|
226
247
|
productId: AvailabilityId;
|
|
@@ -242,6 +263,10 @@ export interface GetProductTimeRangesRequest {
|
|
|
242
263
|
partySize?: number;
|
|
243
264
|
durationMinutesByProductId?: Record<string, number>;
|
|
244
265
|
durationStartTimesByProductId?: Record<string, string[]>;
|
|
266
|
+
/** Restore only the exact remembered start; recompute the end from current product/windows. */
|
|
267
|
+
rememberedStartAtByProductId?: Record<string, string>;
|
|
268
|
+
/** Edit only: preserve the full actual start plus an explicit durationMinutesByProductId. */
|
|
269
|
+
editingStartAtByProductId?: Record<string, string>;
|
|
245
270
|
/**
|
|
246
271
|
* POS 手工输入时间的兼容开关:只为明确请求的 start time 补候选,
|
|
247
272
|
* 不改变常规候选列表仍按营业时间生成的规则。
|
|
@@ -250,6 +275,10 @@ export interface GetProductTimeRangesRequest {
|
|
|
250
275
|
}
|
|
251
276
|
export interface GetProductTimeRangesRuntimeOptions {
|
|
252
277
|
evaluatedAt?: string;
|
|
278
|
+
/** Enabled by the POS/Kiosk facade only; other consumers retain their existing grid. */
|
|
279
|
+
useDurationTimeGrid?: boolean;
|
|
280
|
+
/** Automatic remembered selection must not use POS opening-minute cutoff exceptions. */
|
|
281
|
+
enforceSaleCutoff?: boolean;
|
|
253
282
|
/**
|
|
254
283
|
* Internal POS capability: fixed-duration candidates start from the stable
|
|
255
284
|
* context anchor's current minute instead of rounding up to ten minutes, and
|
|
@@ -328,7 +328,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
328
328
|
date: string;
|
|
329
329
|
status: string;
|
|
330
330
|
week: string;
|
|
331
|
-
weekNum: 0 | 2 | 1 |
|
|
331
|
+
weekNum: 0 | 2 | 1 | 6 | 4 | 5 | 3;
|
|
332
332
|
}[]>;
|
|
333
333
|
submitTimeSlot(timeSlots: TimeSliceItem): void;
|
|
334
334
|
private getScheduleDataByIds;
|
|
@@ -146,7 +146,7 @@ function isMultiDayLike(cacheItem) {
|
|
|
146
146
|
* `_extend.resource / capacity / startDate / endDate` 等后续 booking mapper 需要的字段。
|
|
147
147
|
*/
|
|
148
148
|
function prepareBookingCacheItem(cacheItem, ctx) {
|
|
149
|
-
if (isNormalBookingProduct(cacheItem) || hasPreparedBookingExtend(cacheItem)) {
|
|
149
|
+
if (ctx?.rememberedTime === undefined && (isNormalBookingProduct(cacheItem) || hasPreparedBookingExtend(cacheItem))) {
|
|
150
150
|
return cacheItem;
|
|
151
151
|
}
|
|
152
152
|
return (0, _BookingContext.getProductExtend)({
|
|
@@ -99,6 +99,9 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
|
|
|
99
99
|
protected getSubModuleHooks(moduleName: string): any;
|
|
100
100
|
private createAvailabilityFacadeError;
|
|
101
101
|
private getAvailabilityScopeDescriptor;
|
|
102
|
+
private usesDurationTimeGrid;
|
|
103
|
+
private getCurrentAvailabilityBusinessDate;
|
|
104
|
+
private getAvailabilityBusinessDateRange;
|
|
102
105
|
private normalizeAvailabilityIdList;
|
|
103
106
|
private normalizeAvailabilityStringList;
|
|
104
107
|
private normalizeAvailabilityPrepareDescriptor;
|