@pisell/pisellos 2.1.128 → 2.1.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  2. package/dist/modules/Order/index.d.ts +4 -0
  3. package/dist/modules/Order/index.js +18 -1
  4. package/dist/modules/Order/types.d.ts +9 -1
  5. package/dist/modules/Order/utils.d.ts +7 -0
  6. package/dist/modules/Order/utils.js +27 -11
  7. package/dist/solution/ScanOrder/index.d.ts +27 -3
  8. package/dist/solution/ScanOrder/index.js +865 -481
  9. package/dist/solution/ScanOrder/types.d.ts +34 -24
  10. package/dist/solution/ScanOrder/types.js +5 -1
  11. package/dist/solution/ScanOrder/utils.d.ts +13 -1
  12. package/dist/solution/ScanOrder/utils.js +45 -6
  13. package/dist/solution/VenueBooking/index.d.ts +28 -5
  14. package/dist/solution/VenueBooking/index.js +463 -227
  15. package/dist/solution/VenueBooking/types.d.ts +23 -0
  16. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -1
  17. package/dist/solution/VenueBooking/utils/dateSummary.js +1 -1
  18. package/dist/solution/VenueBooking/utils/resource.d.ts +11 -1
  19. package/dist/solution/VenueBooking/utils/resource.js +57 -21
  20. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +5 -0
  21. package/dist/solution/VenueBooking/utils/slotMerge.js +33 -12
  22. package/dist/solution/VenueBooking/utils/timeSlot.d.ts +1 -1
  23. package/dist/solution/VenueBooking/utils/timeSlot.js +259 -62
  24. package/lib/model/strategy/adapter/promotion/index.js +49 -0
  25. package/lib/modules/Order/index.d.ts +4 -0
  26. package/lib/modules/Order/index.js +14 -1
  27. package/lib/modules/Order/types.d.ts +9 -1
  28. package/lib/modules/Order/utils.d.ts +7 -0
  29. package/lib/modules/Order/utils.js +22 -12
  30. package/lib/solution/ScanOrder/index.d.ts +27 -3
  31. package/lib/solution/ScanOrder/index.js +409 -114
  32. package/lib/solution/ScanOrder/types.d.ts +34 -24
  33. package/lib/solution/ScanOrder/utils.d.ts +13 -1
  34. package/lib/solution/ScanOrder/utils.js +37 -0
  35. package/lib/solution/VenueBooking/index.d.ts +28 -5
  36. package/lib/solution/VenueBooking/index.js +203 -58
  37. package/lib/solution/VenueBooking/types.d.ts +23 -0
  38. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -1
  39. package/lib/solution/VenueBooking/utils/dateSummary.js +1 -1
  40. package/lib/solution/VenueBooking/utils/resource.d.ts +11 -1
  41. package/lib/solution/VenueBooking/utils/resource.js +15 -4
  42. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +5 -0
  43. package/lib/solution/VenueBooking/utils/slotMerge.js +29 -12
  44. package/lib/solution/VenueBooking/utils/timeSlot.d.ts +1 -1
  45. package/lib/solution/VenueBooking/utils/timeSlot.js +182 -43
  46. package/package.json +1 -1
@@ -629,12 +629,14 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
629
629
  }
630
630
  this.store.rawResourceData = rawData;
631
631
  for (const item of rawData) {
632
- const mapping = this.resourceProductMap.get(item.resourceId);
633
- if (mapping && item.main_field) {
634
- mapping.resourceName = item.main_field;
635
- }
636
- if (mapping && item.resource_form_id) {
637
- mapping.formId = item.resource_form_id;
632
+ const mappings = this.resourceProductMap.get(item.resourceId);
633
+ if (!mappings)
634
+ continue;
635
+ for (const mapping of mappings) {
636
+ if (item.main_field)
637
+ mapping.resourceName = item.main_field;
638
+ if (item.resource_form_id)
639
+ mapping.formId = item.resource_form_id;
638
640
  }
639
641
  }
640
642
  this.logMethodSuccess("fetchResourceAvailability", {
@@ -721,7 +723,11 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
721
723
  const resolvedSlotConfig = this.syncOperatingHoursToSlotConfig(date);
722
724
  let quotationPriceMap;
723
725
  if (this.store.quotation) {
724
- const productIds = [...new Set([...this.resourceProductMap.values()].map((m) => m.productId))];
726
+ const productIds = [
727
+ ...new Set(
728
+ [...this.resourceProductMap.values()].flat().map((m) => m.productId)
729
+ )
730
+ ];
725
731
  const timeLabels = (0, import_timeSlot.generateTimeLabels)(resolvedSlotConfig);
726
732
  const timePoints = timeLabels.map((label) => `${date} ${label}`);
727
733
  quotationPriceMap = this.store.quotation.buildProductPriceMap({ productIds, timePoints });
@@ -738,19 +744,31 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
738
744
  /**
739
745
  * 切换单个时段的选中状态(选中/取消)。
740
746
  * 内部自动处理连续时段的合并与拆分,订单是唯一真相源。
747
+ *
748
+ * slot.productId 指定当前操作针对的是该 resourceId 下的哪一个商品。
749
+ * 同一资源下不同 productId 之间互相隔离,不会相互合并。
741
750
  */
742
751
  async toggleSlot(slot) {
743
752
  this.logMethodStart("toggleSlot", {
744
753
  resourceId: slot.resourceId,
754
+ productId: slot.productId,
745
755
  startTime: slot.startTime
746
756
  });
747
757
  try {
748
- const mapping = this.resourceProductMap.get(slot.resourceId);
749
- if (!mapping)
758
+ const mappings = this.resourceProductMap.get(slot.resourceId);
759
+ if (!mappings || !mappings.length) {
750
760
  throw new Error(`未找到资源 ${slot.resourceId} 的商品映射`);
761
+ }
762
+ const mapping = mappings.find((m) => m.productId === slot.productId);
763
+ if (!mapping) {
764
+ throw new Error(`资源 ${slot.resourceId} 未关联商品 ${slot.productId}`);
765
+ }
751
766
  if (!this.store.order)
752
767
  throw new Error("order 模块未初始化");
753
- const currentSlots = this.getSelectedSlotsForResource(slot.resourceId);
768
+ const currentSlots = this.getSelectedSlotsForResourceProduct(
769
+ slot.resourceId,
770
+ slot.productId
771
+ );
754
772
  const existIndex = currentSlots.findIndex((s) => s.startTime === slot.startTime);
755
773
  let nextSlots;
756
774
  if (existIndex !== -1) {
@@ -758,13 +776,18 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
758
776
  } else {
759
777
  nextSlots = [...currentSlots, slot];
760
778
  }
761
- await this.reconcileOrderForResource(slot.resourceId, nextSlots);
779
+ await this.reconcileOrderForResourceProduct(
780
+ slot.resourceId,
781
+ slot.productId,
782
+ nextSlots
783
+ );
762
784
  const products = this.store.order.getOrderProducts();
763
785
  await this.refreshItemRuleQuantityLimits();
764
786
  await this.refreshCartValidationPassed();
765
787
  this.logMethodSuccess("toggleSlot", {
766
788
  action: existIndex !== -1 ? "remove" : "add",
767
789
  resourceId: slot.resourceId,
790
+ productId: slot.productId,
768
791
  slotCount: nextSlots.length
769
792
  });
770
793
  return products;
@@ -775,33 +798,106 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
775
798
  }
776
799
  /**
777
800
  * 获取某资源当前选中的所有独立时段(从订单中解析)。
801
+ * 不传 productId 时返回该资源下所有商品的选中时段;传了则精确匹配。
778
802
  */
779
- getSelectedSlotsForResource(resourceId) {
803
+ getSelectedSlotsForResource(resourceId, productId) {
780
804
  var _a;
781
805
  const tempOrder = (_a = this.store.order) == null ? void 0 : _a.getTempOrder();
782
806
  if (!tempOrder)
783
807
  return [];
784
- const venueProducts = tempOrder.products.filter(
785
- (p) => {
786
- var _a2, _b;
787
- return ((_a2 = p.metadata) == null ? void 0 : _a2.venue_booking) && String((_b = p.metadata) == null ? void 0 : _b.resource_id) === String(resourceId);
788
- }
789
- );
808
+ const venueProducts = tempOrder.products.filter((p) => {
809
+ var _a2, _b;
810
+ if (!((_a2 = p.metadata) == null ? void 0 : _a2.venue_booking))
811
+ return false;
812
+ if (String((_b = p.metadata) == null ? void 0 : _b.resource_id) !== String(resourceId))
813
+ return false;
814
+ if (productId != null && Number(p.product_id) !== Number(productId))
815
+ return false;
816
+ return true;
817
+ });
790
818
  const slotDuration = this.store.slotConfig.slotDurationMinutes;
791
819
  const slots = [];
792
820
  for (const product of venueProducts) {
793
- slots.push(...(0, import_slotMerge.expandMergedSlotToIndividual)(product, slotDuration));
821
+ slots.push(
822
+ ...(0, import_slotMerge.expandMergedSlotToIndividual)(product, slotDuration).map((s) => ({
823
+ ...s,
824
+ productId: Number(product.product_id)
825
+ }))
826
+ );
794
827
  }
795
828
  return slots.sort((a, b) => a.startTime.localeCompare(b.startTime));
796
829
  }
830
+ /** getSelectedSlotsForResource 的 (resourceId, productId) 精确版,内部使用。 */
831
+ getSelectedSlotsForResourceProduct(resourceId, productId) {
832
+ return this.getSelectedSlotsForResource(resourceId, productId);
833
+ }
797
834
  /**
798
835
  * 判断某个时段是否已选中。
836
+ * 不传 productId 时:只要该资源下任一商品在 startTime 被选中即返回 true;传了则精确匹配。
799
837
  */
800
- isSlotSelected(resourceId, startTime) {
801
- return this.getSelectedSlotsForResource(resourceId).some((s) => s.startTime === startTime);
838
+ isSlotSelected(resourceId, startTime, productId) {
839
+ return this.getSelectedSlotsForResource(resourceId, productId).some((s) => s.startTime === startTime);
840
+ }
841
+ /**
842
+ * 判断指定 (resourceId, productId, startTime) 格子是否应因其它已选项而被禁用。
843
+ * 规则:
844
+ * 1) 同一 resourceId 下若已选了另一个 productId 的同 startTime → 禁用
845
+ * 2) 当前 resource 是组合资源:若其任一 child resource 在 startTime 被选中 → 禁用
846
+ * 3) 当前 resource 是某些组合资源的 child:若该组合资源在 startTime 被选中 → 禁用
847
+ * 4) 两个组合资源的 child 集合有交集,且对方在该 startTime 被选中 → 禁用
848
+ */
849
+ isSlotDisabledBySelection(params) {
850
+ const { resourceId, productId, startTime } = params;
851
+ const allSelected = this.getAllSelectedSlots();
852
+ const getSelectedAt = (resId) => {
853
+ const list = allSelected.get(resId) || [];
854
+ return list.filter((s) => s.startTime === startTime);
855
+ };
856
+ const sameResourceSelections = getSelectedAt(resourceId);
857
+ if (sameResourceSelections.some((s) => Number(s.productId) !== Number(productId))) {
858
+ return true;
859
+ }
860
+ const getCombinedChildIds = (resId) => {
861
+ const raw = this.store.rawResourceData.find(
862
+ (r) => String(r.resourceId) === String(resId)
863
+ );
864
+ const combined = raw == null ? void 0 : raw.combined_resource;
865
+ if (combined && combined.status === 1 && Array.isArray(combined.resource_ids)) {
866
+ return combined.resource_ids;
867
+ }
868
+ return [];
869
+ };
870
+ const currentChildIds = getCombinedChildIds(resourceId);
871
+ const currentIsCombined = currentChildIds.length > 0;
872
+ if (currentIsCombined) {
873
+ for (const childId of currentChildIds) {
874
+ if (getSelectedAt(childId).length > 0)
875
+ return true;
876
+ }
877
+ }
878
+ for (const [selectedResId, slots] of allSelected) {
879
+ if (!slots.some((s) => s.startTime === startTime))
880
+ continue;
881
+ if (String(selectedResId) === String(resourceId))
882
+ continue;
883
+ const selectedChildIds = getCombinedChildIds(selectedResId);
884
+ if (selectedChildIds.length === 0)
885
+ continue;
886
+ if (selectedChildIds.some((id) => String(id) === String(resourceId))) {
887
+ return true;
888
+ }
889
+ if (currentIsCombined) {
890
+ const intersect = selectedChildIds.some(
891
+ (id) => currentChildIds.some((cid) => String(cid) === String(id))
892
+ );
893
+ if (intersect)
894
+ return true;
895
+ }
896
+ }
897
+ return false;
802
898
  }
803
899
  /**
804
- * 获取所有已选时段(按资源分组)。
900
+ * 获取所有已选时段(按资源分组)。每个 slot 上带 productId,便于 UI 做 per-product 判定。
805
901
  */
806
902
  getAllSelectedSlots() {
807
903
  var _a, _b;
@@ -818,8 +914,14 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
818
914
  const resourceId = (_b = product.metadata) == null ? void 0 : _b.resource_id;
819
915
  if (resourceId == null)
820
916
  continue;
917
+ const productId = Number(product.product_id);
821
918
  const existing = result.get(resourceId) || [];
822
- existing.push(...(0, import_slotMerge.expandMergedSlotToIndividual)(product, slotDuration));
919
+ existing.push(
920
+ ...(0, import_slotMerge.expandMergedSlotToIndividual)(product, slotDuration).map((s) => ({
921
+ ...s,
922
+ productId
923
+ }))
924
+ );
823
925
  result.set(resourceId, existing);
824
926
  }
825
927
  for (const [key, slots] of result) {
@@ -828,24 +930,33 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
828
930
  return result;
829
931
  }
830
932
  /**
831
- * 对指定资源的订单商品进行 reconcile:
933
+ * 对指定 (resourceId, productId) 的订单商品进行 reconcile:
832
934
  * 清除旧商品 → 合并连续时段 → 重新写入。
935
+ * 同一场地下不同商品互不干扰,各自单独 reconcile。
833
936
  */
834
- async reconcileOrderForResource(resourceId, slots) {
835
- const mapping = this.resourceProductMap.get(resourceId);
836
- if (!mapping || !this.store.order)
937
+ async reconcileOrderForResourceProduct(resourceId, productId, slots) {
938
+ const mappings = this.resourceProductMap.get(resourceId);
939
+ if (!mappings || !mappings.length || !this.store.order)
940
+ return;
941
+ const mapping = mappings.find((m) => m.productId === productId);
942
+ if (!mapping)
837
943
  return;
838
944
  const tempOrder = this.store.order.ensureTempOrder();
945
+ const matchesCurrent = (meta, pid) => !!(meta == null ? void 0 : meta.venue_booking) && String(meta == null ? void 0 : meta.resource_id) === String(resourceId) && Number(pid) === Number(productId);
839
946
  tempOrder.products = tempOrder.products.filter(
840
- (p) => {
841
- var _a, _b;
842
- return !(((_a = p.metadata) == null ? void 0 : _a.venue_booking) && String((_b = p.metadata) == null ? void 0 : _b.resource_id) === String(resourceId));
843
- }
947
+ (p) => !matchesCurrent(p.metadata, p.product_id)
844
948
  );
845
949
  tempOrder.bookings = (tempOrder.bookings || []).filter(
846
950
  (b) => {
847
- var _a, _b;
848
- return !(((_a = b.metadata) == null ? void 0 : _a.venue_booking) && String((_b = b.metadata) == null ? void 0 : _b.resource_id) === String(resourceId));
951
+ var _a, _b, _c, _d;
952
+ if (!((_a = b.metadata) == null ? void 0 : _a.venue_booking))
953
+ return true;
954
+ if (String((_b = b.metadata) == null ? void 0 : _b.resource_id) !== String(resourceId))
955
+ return true;
956
+ const bookingProductId = ((_c = b.metadata) == null ? void 0 : _c.product_id) ?? ((_d = b == null ? void 0 : b.product) == null ? void 0 : _d.product_id);
957
+ if (bookingProductId == null)
958
+ return false;
959
+ return Number(bookingProductId) !== Number(productId);
849
960
  }
850
961
  );
851
962
  if (!slots.length) {
@@ -858,6 +969,7 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
858
969
  const rawResource = this.store.rawResourceData.find(
859
970
  (r) => String(r.resourceId) === String(resourceId)
860
971
  );
972
+ const childRawResources = this.getCombinedChildRawResources(rawResource);
861
973
  const venueProduct = this.getVenueProducts().find((p) => p.id === mapping.productId);
862
974
  tempOrder.bookings = tempOrder.bookings || [];
863
975
  for (let i = 0; i < merged.length; i++) {
@@ -868,6 +980,28 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
868
980
  const endMoment = (0, import_dayjs.default)(group.endTime, "YYYY-MM-DD HH:mm");
869
981
  const duration = endMoment.diff(startMoment, "minute");
870
982
  const customDepositData = cloneCustomDepositData(venueProduct == null ? void 0 : venueProduct.custom_deposit_data);
983
+ const resourceEntry = {
984
+ relation_type: "form",
985
+ like_status: "common",
986
+ id: resourceId,
987
+ main_field: mapping.resourceName,
988
+ form_id: (rawResource == null ? void 0 : rawResource.form_id) ?? mapping.formId,
989
+ relation_id: resourceId,
990
+ capacity: 1,
991
+ metadata: {}
992
+ };
993
+ if (childRawResources && childRawResources.length) {
994
+ resourceEntry.children = childRawResources.map((child) => ({
995
+ relation_type: "form",
996
+ like_status: "common",
997
+ id: child.resourceId,
998
+ main_field: child.main_field || "",
999
+ form_id: child.form_id ?? child.formId,
1000
+ relation_id: child.resourceId,
1001
+ capacity: child.capacity ?? 1,
1002
+ metadata: {}
1003
+ }));
1004
+ }
871
1005
  tempOrder.products.push(
872
1006
  (0, import_utils.normalizeOrderProduct)({
873
1007
  product_id: mapping.productId,
@@ -914,16 +1048,7 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
914
1048
  sub_type: "minutes",
915
1049
  duration,
916
1050
  like_status: "common",
917
- resources: [{
918
- relation_type: "form",
919
- like_status: "common",
920
- id: resourceId,
921
- main_field: mapping.resourceName,
922
- form_id: (rawResource == null ? void 0 : rawResource.form_id) ?? mapping.formId,
923
- relation_id: resourceId,
924
- capacity: 1,
925
- metadata: {}
926
- }],
1051
+ resources: [resourceEntry],
927
1052
  schedule_id: 0,
928
1053
  select_date: startMoment.format("YYYY-MM-DD"),
929
1054
  start_date: startMoment.format("YYYY-MM-DD"),
@@ -944,7 +1069,8 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
944
1069
  mapping,
945
1070
  rawResource,
946
1071
  bookingUuid,
947
- productUid: identityKey
1072
+ productUid: identityKey,
1073
+ childResources: childRawResources
948
1074
  });
949
1075
  tempOrder.bookings.push(booking);
950
1076
  }
@@ -952,6 +1078,24 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
952
1078
  await this.store.order.recalculateSummary({ createIfMissing: true });
953
1079
  this.store.order.persistTempOrder();
954
1080
  }
1081
+ /** 给定一个父 rawResource,返回其 combined_resource.resource_ids 对应的子 rawResource 列表。 */
1082
+ getCombinedChildRawResources(rawResource) {
1083
+ if (!rawResource)
1084
+ return void 0;
1085
+ const combined = rawResource.combined_resource;
1086
+ if (!combined || combined.status !== 1 || !Array.isArray(combined.resource_ids) || !combined.resource_ids.length) {
1087
+ return void 0;
1088
+ }
1089
+ const children = [];
1090
+ for (const id of combined.resource_ids) {
1091
+ const child = this.store.rawResourceData.find(
1092
+ (r) => String(r.resourceId) === String(id)
1093
+ );
1094
+ if (child)
1095
+ children.push(child);
1096
+ }
1097
+ return children.length ? children : void 0;
1098
+ }
955
1099
  // ─── 配置 ───
956
1100
  setSlotConfig(config) {
957
1101
  this.baseSlotConfig = {
@@ -1034,7 +1178,10 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
1034
1178
  const now = (0, import_dayjs.default)().format("YYYY-MM-DD HH:mm:ss");
1035
1179
  for (const product of tempOrder.products) {
1036
1180
  if ((_b = product.metadata) == null ? void 0 : _b.venue_booking) {
1037
- const mapping = this.resourceProductMap.get(product.metadata.resource_id);
1181
+ const mappings = this.resourceProductMap.get(product.metadata.resource_id);
1182
+ if (!mappings || !mappings.length)
1183
+ continue;
1184
+ const mapping = mappings.find((m) => Number(m.productId) === Number(product.product_id)) || mappings[0];
1038
1185
  if (!mapping)
1039
1186
  continue;
1040
1187
  const slots = (0, import_slotMerge.expandMergedSlotToIndividual)(
@@ -1043,6 +1190,7 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
1043
1190
  );
1044
1191
  const updatedSlots = slots.map((slot) => ({
1045
1192
  ...slot,
1193
+ productId: mapping.productId,
1046
1194
  price: this.store.quotation.getPriceForProduct({
1047
1195
  productId: mapping.productId,
1048
1196
  datetime: slot.startTime
@@ -1152,24 +1300,21 @@ var _VenueBookingImpl = class extends import_BaseModule.BaseModule {
1152
1300
  nextDiscountList.filter((d) => d.isSelected).map((d) => d.id)
1153
1301
  );
1154
1302
  for (const product of tempOrder.products) {
1155
- if (!((_c = product.discount_list) == null ? void 0 : _c.length))
1303
+ if ((_c = product._origin) == null ? void 0 : _c.isManualDiscount)
1156
1304
  continue;
1157
- const before = product.discount_list.length;
1158
- product.discount_list = product.discount_list.filter((pd) => {
1305
+ product.discount_list = (product.discount_list || []).filter((pd) => {
1159
1306
  var _a2;
1160
1307
  const rid = ((_a2 = pd.discount) == null ? void 0 : _a2.resource_id) ?? pd.id;
1161
1308
  return rid != null && selectedResourceIds.has(rid);
1162
1309
  });
1163
- if (product.discount_list.length < before) {
1164
- const totalPerUnitDiscount = product.discount_list.reduce(
1165
- (sum, pd) => sum + (pd.amount || 0),
1166
- 0
1167
- );
1168
- const newSellingPrice = new import_decimal.default(product.original_price || product.selling_price || 0).minus(totalPerUnitDiscount).toDecimalPlaces(2).toString();
1169
- product.selling_price = newSellingPrice;
1170
- if (product.metadata) {
1171
- product.metadata.main_product_selling_price = newSellingPrice;
1172
- }
1310
+ const totalPerUnitDiscount = (product.discount_list || []).reduce(
1311
+ (sum, pd) => sum + (pd.amount || 0),
1312
+ 0
1313
+ );
1314
+ const newSellingPrice = new import_decimal.default(product.original_price || 0).minus(totalPerUnitDiscount).toDecimalPlaces(2).toString();
1315
+ product.selling_price = newSellingPrice;
1316
+ if (product.metadata) {
1317
+ product.metadata.main_product_selling_price = newSellingPrice;
1173
1318
  }
1174
1319
  }
1175
1320
  import_Order.OrderModule.populateSavedAmounts(tempOrder.products, nextDiscountList);
@@ -36,14 +36,31 @@ export interface VenueTimeSlot {
36
36
  resourceFormId: number | string;
37
37
  capacity: number | null;
38
38
  remainingCapacity: number | null;
39
+ /** per-product sub-row slot 上带的 productId;外层合并 slot 不写 */
40
+ productId?: number;
41
+ }
42
+ export interface VenueResourceSubRow {
43
+ productId: number;
44
+ productTitle: string;
45
+ price: string;
46
+ slots: VenueTimeSlot[];
47
+ }
48
+ export interface VenueResourceCombinedInfo {
49
+ resourceIds: Array<number | string>;
39
50
  }
40
51
  export interface VenueResourceRow {
41
52
  resourceId: number | string;
42
53
  resourceFormId: number | string;
43
54
  resourceName: string;
55
+ /** 单商品时取唯一 mapping.productId;多商品时取第一个 mapping(兼容字段) */
44
56
  productId: number;
45
57
  productTitle: string;
46
58
  slots: VenueTimeSlot[];
59
+ /** 多商品共享同一场地时的子行;单商品不输出 */
60
+ products?: VenueResourceSubRow[];
61
+ hasMultipleProducts?: boolean;
62
+ /** 当前资源是 combined_resource.status===1 时输出 */
63
+ combinedResource?: VenueResourceCombinedInfo;
47
64
  }
48
65
  export interface VenueTimeSlotGrid {
49
66
  date: string;
@@ -57,6 +74,10 @@ export interface VenueDateSummaryItem {
57
74
  totalSlots: number;
58
75
  availableSlots: number;
59
76
  }
77
+ export interface VenueResourceCombinedRaw {
78
+ status: number;
79
+ resource_ids: Array<number | string>;
80
+ }
60
81
  export interface VenueResourceRawData {
61
82
  resourceId: number | string;
62
83
  formId: number | string;
@@ -69,6 +90,7 @@ export interface VenueResourceRawData {
69
90
  end_at: string;
70
91
  [key: string]: any;
71
92
  }>;
93
+ combined_resource?: VenueResourceCombinedRaw;
72
94
  [key: string]: any;
73
95
  }
74
96
  export interface ResourceProductMapping {
@@ -81,6 +103,7 @@ export interface ResourceProductMapping {
81
103
  }
82
104
  export interface VenueSlotSelection {
83
105
  resourceId: number | string;
106
+ productId: number;
84
107
  startTime: string;
85
108
  endTime: string;
86
109
  price: string;
@@ -5,7 +5,7 @@ export declare function buildDateRangeSummary(params: {
5
5
  endDate: string;
6
6
  config: VenueBookingSlotConfig;
7
7
  rawResources: VenueResourceRawData[];
8
- resourceProductMap: Map<number | string, ResourceProductMapping>;
8
+ resourceProductMap: Map<number | string, ResourceProductMapping[]>;
9
9
  quotationModule?: QuotationModule;
10
10
  resolveConfig?: (date: string) => VenueBookingSlotConfig;
11
11
  }): VenueDateSummaryItem[];
@@ -45,7 +45,7 @@ function buildDateRangeSummary(params) {
45
45
  resolveConfig
46
46
  } = params;
47
47
  const result = [];
48
- const productIds = quotationModule ? [...new Set([...resourceProductMap.values()].map((m) => m.productId))] : [];
48
+ const productIds = quotationModule ? [...new Set([...resourceProductMap.values()].flat().map((m) => m.productId))] : [];
49
49
  let cursor = (0, import_dayjs.default)(startDate);
50
50
  const end = (0, import_dayjs.default)(endDate);
51
51
  while (!cursor.isAfter(end)) {
@@ -1,4 +1,14 @@
1
1
  import type { ProductData } from '../../../modules/Product/types';
2
2
  import type { ResourceProductMapping } from '../types';
3
+ /**
4
+ * 从商品列表里抽取所有 resource_id:
5
+ * - 商品绑定的 default_resource / optional_resource
6
+ * - 以及这些资源上声明的 combined_resource.resource_ids(若可从 product_resource 读取)
7
+ * 用于一次性拉到子资源数据,判定组合资源的可用性。
8
+ */
3
9
  export declare function extractResourceIds(products: ProductData[]): number[];
4
- export declare function buildResourceProductMap(products: ProductData[]): Map<number | string, ResourceProductMapping>;
10
+ /**
11
+ * 每个 resourceId 可以被多个商品共享(同一场地关联多种运动商品)。
12
+ * Map 的 value 改为 ResourceProductMapping[],按商品出现顺序保留。
13
+ */
14
+ export declare function buildResourceProductMap(products: ProductData[]): Map<number | string, ResourceProductMapping[]>;
@@ -39,6 +39,13 @@ function extractResourceIds(products) {
39
39
  if (typeof id === "number" && id > 0)
40
40
  ids.add(id);
41
41
  }
42
+ const combined = resource.combined_resource;
43
+ if (combined && combined.status === 1 && Array.isArray(combined.resource_ids)) {
44
+ for (const id of combined.resource_ids) {
45
+ if (typeof id === "number" && id > 0)
46
+ ids.add(id);
47
+ }
48
+ }
42
49
  }
43
50
  }
44
51
  return Array.from(ids);
@@ -58,16 +65,20 @@ function buildResourceProductMap(products) {
58
65
  ...resource.optional_resource || []
59
66
  ];
60
67
  for (const resId of allResourceIds) {
61
- if (map.has(resId))
62
- continue;
63
- map.set(resId, {
68
+ const existing = map.get(resId);
69
+ const mapping = {
64
70
  productId: product.id,
65
71
  productTitle: product.title,
66
72
  resourceName: resource.title,
67
73
  formId: resource.id,
68
74
  price: product.price || "0.00",
69
75
  resourceType: resource.type || "single"
70
- });
76
+ };
77
+ if (!existing) {
78
+ map.set(resId, [mapping]);
79
+ } else if (!existing.some((m) => m.productId === mapping.productId)) {
80
+ existing.push(mapping);
81
+ }
71
82
  }
72
83
  }
73
84
  }
@@ -19,6 +19,11 @@ export interface BuildVenueBookingParams {
19
19
  rawResource: VenueResourceRawData | undefined;
20
20
  bookingUuid: string;
21
21
  productUid: string;
22
+ /**
23
+ * 若当前 resource 是组合资源(combined_resource.status === 1),传入其子资源的原始数据,
24
+ * 将作为 resources[0].children 写入 booking,结构与父 resource 保持一致。
25
+ */
26
+ childResources?: VenueResourceRawData[];
22
27
  }
23
28
  export declare function buildVenueBookingEntry(params: BuildVenueBookingParams): Record<string, any>;
24
29
  /**
@@ -107,10 +107,32 @@ function buildPriceBreakdown(params) {
107
107
  return entries;
108
108
  }
109
109
  function buildVenueBookingEntry(params) {
110
- const { group, resourceId, mapping, rawResource, bookingUuid, productUid } = params;
110
+ const { group, resourceId, mapping, rawResource, bookingUuid, productUid, childResources } = params;
111
111
  const startMoment = (0, import_dayjs.default)(group.startTime, "YYYY-MM-DD HH:mm");
112
112
  const endMoment = (0, import_dayjs.default)(group.endTime, "YYYY-MM-DD HH:mm");
113
113
  const duration = endMoment.diff(startMoment, "minute");
114
+ const resourceEntry = {
115
+ relation_type: "form",
116
+ like_status: "common",
117
+ id: resourceId,
118
+ main_field: mapping.resourceName,
119
+ form_id: (rawResource == null ? void 0 : rawResource.form_id) ?? mapping.formId,
120
+ relation_id: resourceId,
121
+ capacity: 1,
122
+ metadata: {}
123
+ };
124
+ if (childResources && childResources.length) {
125
+ resourceEntry.children = childResources.map((child) => ({
126
+ relation_type: "form",
127
+ like_status: "common",
128
+ id: child.resourceId,
129
+ main_field: child.main_field || "",
130
+ form_id: child.form_id ?? child.formId,
131
+ relation_id: child.resourceId,
132
+ capacity: child.capacity ?? 1,
133
+ metadata: {}
134
+ }));
135
+ }
114
136
  return {
115
137
  schedule_event_id: null,
116
138
  appointment_status: "new",
@@ -126,23 +148,15 @@ function buildVenueBookingEntry(params) {
126
148
  is_all: false,
127
149
  schedule_id: 0,
128
150
  product_uid: productUid,
129
- resources: [{
130
- relation_type: "form",
131
- like_status: "common",
132
- id: resourceId,
133
- main_field: mapping.resourceName,
134
- form_id: (rawResource == null ? void 0 : rawResource.form_id) ?? mapping.formId,
135
- relation_id: resourceId,
136
- capacity: 1,
137
- metadata: {}
138
- }],
151
+ resources: [resourceEntry],
139
152
  relation_products: [],
140
153
  relation_forms: [],
141
154
  holder: null,
142
155
  metadata: {
143
156
  unique_identification_number: bookingUuid,
144
157
  venue_booking: true,
145
- resource_id: resourceId
158
+ resource_id: resourceId,
159
+ product_id: mapping.productId
146
160
  }
147
161
  };
148
162
  }
@@ -155,6 +169,7 @@ function expandMergedSlotToIndividual(product, slotDurationMinutes) {
155
169
  const endTime = meta.end_time;
156
170
  if (!startTime || !endTime)
157
171
  return [];
172
+ const productId = Number(product.product_id);
158
173
  const breakdown = meta.price_breakdown;
159
174
  const result = [];
160
175
  let cursor = (0, import_dayjs.default)(startTime, "YYYY-MM-DD HH:mm");
@@ -175,6 +190,7 @@ function expandMergedSlotToIndividual(product, slotDurationMinutes) {
175
190
  const price = priceMap.get(hm) ?? 0;
176
191
  result.push({
177
192
  resourceId,
193
+ productId,
178
194
  startTime: cursor.format("YYYY-MM-DD HH:mm"),
179
195
  endTime: slotEnd.format("YYYY-MM-DD HH:mm"),
180
196
  price: new import_decimal.default(price).toFixed(2)
@@ -189,6 +205,7 @@ function expandMergedSlotToIndividual(product, slotDurationMinutes) {
189
205
  const slotEnd = cursor.add(slotDurationMinutes, "minute");
190
206
  result.push({
191
207
  resourceId,
208
+ productId,
192
209
  startTime: cursor.format("YYYY-MM-DD HH:mm"),
193
210
  endTime: slotEnd.format("YYYY-MM-DD HH:mm"),
194
211
  price: perSlotPrice
@@ -26,7 +26,7 @@ export declare function buildTimeSlotGrid(params: {
26
26
  date: string;
27
27
  config: VenueBookingSlotConfig;
28
28
  rawResources: VenueResourceRawData[];
29
- resourceProductMap: Map<number | string, ResourceProductMapping>;
29
+ resourceProductMap: Map<number | string, ResourceProductMapping[]>;
30
30
  quotationPriceMap?: Map<string, string | null>;
31
31
  }): VenueTimeSlotGrid;
32
32
  export {};