@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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 (243) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -1,4 +1,45 @@
1
1
  const DAY_MS = 24 * 60 * 60 * 1e3;
2
+ const timezoneFormatters = /* @__PURE__ */ new Map();
3
+ function zonedParts(instant, timezone) {
4
+ let formatter = timezoneFormatters.get(timezone);
5
+ if (!formatter) {
6
+ formatter = new Intl.DateTimeFormat("en-US", {
7
+ timeZone: timezone,
8
+ year: "numeric",
9
+ month: "2-digit",
10
+ day: "2-digit",
11
+ hour: "2-digit",
12
+ minute: "2-digit",
13
+ second: "2-digit",
14
+ hourCycle: "h23"
15
+ });
16
+ if (timezoneFormatters.size >= 64) timezoneFormatters.delete(timezoneFormatters.keys().next().value);
17
+ timezoneFormatters.set(timezone, formatter);
18
+ }
19
+ const parts = formatter.formatToParts(instant);
20
+ const number = (kind) => Number(parts.find((part) => part.type === kind)?.value);
21
+ return { year: number("year"), month: number("month"), day: number("day"), hour: number("hour"), minute: number("minute"), second: number("second") };
22
+ }
23
+ function zonedWallToInstant(wall, timezone) {
24
+ const target = wall.getTime();
25
+ let candidate = target;
26
+ for (let attempt = 0; attempt < 4; attempt += 1) {
27
+ const parts = zonedParts(new Date(candidate), timezone);
28
+ const displayed = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
29
+ const next = candidate + target - displayed;
30
+ if (next === candidate) return new Date(candidate);
31
+ candidate = next;
32
+ }
33
+ return null;
34
+ }
35
+ function zonedStartOfDay(dayKey, timezone) {
36
+ return zonedWallToInstant(/* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`), timezone);
37
+ }
38
+ function dayKeyForRule(instant, timezone) {
39
+ if (!timezone || timezone === "UTC" || timezone === "Etc/UTC" || timezone === "Etc/GMT") return toDayKey(instant);
40
+ const parts = zonedParts(instant, timezone);
41
+ return `${parts.year}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
42
+ }
2
43
  function parseRrule(rule) {
3
44
  const dtStartMatch = rule.match(/DTSTART[:=](\d{8}T\d{6}Z?)/);
4
45
  const durationMatch = rule.match(/DURATION:PT(?:(\d+)H)?(?:(\d+)M)?/);
@@ -39,10 +80,10 @@ function buildExdateSets(exdates) {
39
80
  });
40
81
  return { dateOnly, dateTime };
41
82
  }
42
- function shouldExcludeOccurrence(startAt, exdates) {
83
+ function shouldExcludeOccurrence(startAt, exdates, timezone) {
43
84
  const { dateOnly, dateTime } = buildExdateSets(exdates);
44
85
  if (dateTime.has(startAt.getTime())) return true;
45
- const dayKey = startAt.toISOString().slice(0, 10);
86
+ const dayKey = dayKeyForRule(startAt, timezone);
46
87
  return dateOnly.has(dayKey);
47
88
  }
48
89
  function toDayKey(value) {
@@ -51,15 +92,46 @@ function toDayKey(value) {
51
92
  function startOfDay(value) {
52
93
  return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
53
94
  }
54
- function expandRule(rule, parsed, range) {
95
+ function expandZonedRule(rule, parsed, range, weeklyScheduleTemplate) {
96
+ const timezone = rule.timezone;
97
+ const anchor = zonedParts(parsed.startAt, timezone);
98
+ const anchorWall = Date.UTC(anchor.year, anchor.month - 1, anchor.day, anchor.hour, anchor.minute, anchor.second);
99
+ const step = parsed.freq === "WEEKLY" ? 7 * DAY_MS : DAY_MS;
100
+ const duration = parsed.durationMinutes * 6e4;
101
+ const windows = [];
102
+ const rangeAnchor = zonedParts(range.start, timezone);
103
+ const rangeStartWall = Date.UTC(rangeAnchor.year, rangeAnchor.month - 1, rangeAnchor.day, rangeAnchor.hour, rangeAnchor.minute, rangeAnchor.second);
104
+ const rangeOffset = Math.floor((rangeStartWall - duration - DAY_MS - anchorWall) / step);
105
+ const skipped = weeklyScheduleTemplate && parsed.freq === "WEEKLY" && parsed.count === void 0 ? rangeOffset : Math.max(0, rangeOffset);
106
+ let wall = new Date(anchorWall + skipped * step);
107
+ let remaining = (parsed.count ?? Number.POSITIVE_INFINITY) - skipped;
108
+ while (wall.getTime() <= range.end.getTime() + DAY_MS && remaining > 0) {
109
+ const start = zonedWallToInstant(wall, timezone);
110
+ if (start && !shouldExcludeOccurrence(start, rule.exdates, timezone)) {
111
+ const end = new Date(start.getTime() + duration);
112
+ if (end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id });
113
+ }
114
+ wall = new Date(wall.getTime() + step);
115
+ remaining -= 1;
116
+ }
117
+ return windows;
118
+ }
119
+ function expandRule(rule, parsed, range, weeklyScheduleTemplate) {
55
120
  const { startAt, durationMinutes, freq, count, repeat } = parsed;
56
121
  if (repeat === "once") {
57
- if (shouldExcludeOccurrence(startAt, rule.exdates)) return [];
58
- const start = startOfDay(startAt);
59
- const end = new Date(start.getTime() + DAY_MS);
122
+ if (shouldExcludeOccurrence(startAt, rule.exdates, rule.timezone)) return [];
123
+ const dayKey = dayKeyForRule(startAt, rule.timezone);
124
+ const start = rule.timezone ? zonedStartOfDay(dayKey, rule.timezone) : startOfDay(startAt);
125
+ if (!start) return [];
126
+ const nextDay = new Date((/* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`)).getTime() + DAY_MS).toISOString().slice(0, 10);
127
+ const end = rule.timezone ? zonedStartOfDay(nextDay, rule.timezone) : new Date(start.getTime() + DAY_MS);
128
+ if (!end) return [];
60
129
  if (end <= range.start || start >= range.end) return [];
61
130
  return [{ start, end, ruleId: rule.id }];
62
131
  }
132
+ if (rule.timezone && !["UTC", "Etc/UTC", "Etc/GMT"].includes(rule.timezone)) {
133
+ return expandZonedRule(rule, parsed, range, weeklyScheduleTemplate);
134
+ }
63
135
  const durationMs = durationMinutes * 6e4;
64
136
  const windows = [];
65
137
  const addWindow = (start) => {
@@ -68,37 +140,23 @@ function expandRule(rule, parsed, range) {
68
140
  if (end <= range.start || start >= range.end) return;
69
141
  windows.push({ start, end, ruleId: rule.id });
70
142
  };
71
- if (freq === "DAILY") {
72
- let cursor2 = new Date(startAt);
73
- let remaining2 = count ?? Number.POSITIVE_INFINITY;
74
- while (cursor2 < range.end && remaining2 > 0) {
75
- addWindow(new Date(cursor2));
76
- cursor2 = new Date(cursor2.getTime() + DAY_MS);
77
- remaining2 -= 1;
78
- }
79
- return windows;
80
- }
81
- let cursor = new Date(startAt);
82
- let remaining = count ?? Number.POSITIVE_INFINITY;
83
- if (cursor < range.start) {
84
- const diffDays = Math.floor((range.start.getTime() - cursor.getTime()) / DAY_MS);
85
- const weeksToAdd = Math.floor(diffDays / 7);
86
- if (weeksToAdd > 0) {
87
- cursor = new Date(cursor.getTime() + weeksToAdd * 7 * DAY_MS);
88
- }
89
- }
143
+ const step = freq === "WEEKLY" ? 7 * DAY_MS : DAY_MS;
144
+ const rangeOffset = Math.floor((range.start.getTime() - durationMs - startAt.getTime()) / step);
145
+ const skipped = weeklyScheduleTemplate && freq === "WEEKLY" && count === void 0 ? rangeOffset : Math.max(0, rangeOffset);
146
+ let cursor = new Date(startAt.getTime() + skipped * step);
147
+ let remaining = (count ?? Number.POSITIVE_INFINITY) - skipped;
90
148
  while (cursor < range.end && remaining > 0) {
91
149
  addWindow(new Date(cursor));
92
- cursor = new Date(cursor.getTime() + 7 * DAY_MS);
150
+ cursor = new Date(cursor.getTime() + step);
93
151
  remaining -= 1;
94
152
  }
95
153
  return windows;
96
154
  }
97
- function expandRules(rules, range) {
155
+ function expandRules(rules, range, weeklyScheduleTemplate) {
98
156
  const expanded = rules.flatMap((rule) => {
99
157
  const parsed = parseRrule(rule.rrule);
100
158
  if (!parsed) return [];
101
- return expandRule(rule, parsed, range);
159
+ return expandRule(rule, parsed, range, weeklyScheduleTemplate);
102
160
  });
103
161
  return expanded.sort((a, b) => a.start.getTime() - b.start.getTime());
104
162
  }
@@ -123,32 +181,37 @@ function subtractWindow(window, blockers) {
123
181
  return segments;
124
182
  }
125
183
  function getMergedAvailabilityWindows(params) {
126
- const parsedRules = params.rules.map((rule) => {
184
+ const rules = params.respectTimezone ? params.rules : params.rules.map((rule) => ({ ...rule, timezone: void 0 }));
185
+ const parsedRules = rules.map((rule) => {
127
186
  const parsed = parseRrule(rule.rrule);
128
187
  if (!parsed) return null;
129
188
  return { rule, parsed };
130
189
  }).filter((entry) => entry !== null);
131
190
  const overrideDays = /* @__PURE__ */ new Map();
132
191
  parsedRules.filter(({ parsed }) => parsed.repeat === "once").forEach(({ rule, parsed }) => {
133
- const dayKey = toDayKey(parsed.startAt);
192
+ const dayKey = dayKeyForRule(parsed.startAt, rule.timezone);
193
+ const overrideKey = `${rule.timezone ?? "UTC"}|${dayKey}`;
134
194
  const kind = rule.kind === "unavailability" ? "unavailability" : "availability";
135
195
  if (kind === "unavailability") {
136
- overrideDays.set(dayKey, "unavailability");
137
- } else if (!overrideDays.has(dayKey)) {
138
- overrideDays.set(dayKey, "availability");
196
+ overrideDays.set(overrideKey, { kind, dayKey, timezone: rule.timezone });
197
+ } else if (!overrideDays.has(overrideKey)) {
198
+ overrideDays.set(overrideKey, { kind, dayKey, timezone: rule.timezone });
139
199
  }
140
200
  });
141
201
  const availabilityRules = parsedRules.filter(({ parsed, rule }) => parsed.repeat !== "once" && rule.kind !== "unavailability").map(({ rule }) => rule);
142
202
  const unavailabilityRules = parsedRules.filter(({ parsed, rule }) => parsed.repeat !== "once" && rule.kind === "unavailability").map(({ rule }) => rule);
143
- const availabilityWindows = expandRules(availabilityRules, params.range).filter((window) => !overrideDays.has(toDayKey(window.start)));
144
- const unavailabilityWindows = expandRules(unavailabilityRules, params.range);
203
+ const availabilityWindows = availabilityRules.flatMap((rule) => expandRules([rule], params.range, params.weeklyScheduleTemplate === true).filter((window) => !overrideDays.has(`${rule.timezone ?? "UTC"}|${dayKeyForRule(window.start, rule.timezone)}`)));
204
+ const unavailabilityWindows = expandRules(unavailabilityRules, params.range, params.weeklyScheduleTemplate === true);
145
205
  const merged = unavailabilityWindows.length === 0 ? availabilityWindows : availabilityWindows.flatMap((window) => subtractWindow(window, unavailabilityWindows));
146
206
  const overrideWindows = [];
147
- overrideDays.forEach((kind, dayKey) => {
207
+ overrideDays.forEach(({ kind, dayKey, timezone }) => {
148
208
  if (kind !== "availability") return;
149
- const start = /* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`);
209
+ const start = timezone ? zonedStartOfDay(dayKey, timezone) : /* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`);
210
+ if (!start) return;
150
211
  if (Number.isNaN(start.getTime())) return;
151
- const end = new Date(start.getTime() + DAY_MS);
212
+ const nextDay = new Date((/* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`)).getTime() + DAY_MS).toISOString().slice(0, 10);
213
+ const end = timezone ? zonedStartOfDay(nextDay, timezone) : new Date(start.getTime() + DAY_MS);
214
+ if (!end) return;
152
215
  if (end <= params.range.start || start >= params.range.end) return;
153
216
  overrideWindows.push({ start, end });
154
217
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/lib/availabilityMerge.ts"],
4
- "sourcesContent": ["type AvailabilityKind = 'availability' | 'unavailability'\n\nexport type AvailabilityRuleLike = {\n id?: string\n rrule: string\n exdates?: string[]\n kind?: AvailabilityKind\n note?: string | null\n}\n\nexport type AvailabilityRange = {\n start: Date\n end: Date\n}\n\nexport type AvailabilityWindow = {\n start: Date\n end: Date\n ruleId?: string\n}\n\ntype ParsedRule = {\n startAt: Date\n durationMinutes: number\n freq: 'DAILY' | 'WEEKLY'\n repeat: 'once' | 'daily' | 'weekly'\n count?: number\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction parseRrule(rule: string): ParsedRule | null {\n const dtStartMatch = rule.match(/DTSTART[:=](\\d{8}T\\d{6}Z?)/)\n const durationMatch = rule.match(/DURATION:PT(?:(\\d+)H)?(?:(\\d+)M)?/)\n const freqMatch = rule.match(/FREQ=([A-Z]+)/)\n if (!dtStartMatch?.[1] || !durationMatch || !freqMatch?.[1]) return null\n const raw = dtStartMatch[1].replace(/Z$/, '')\n const parts = raw.match(/(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})/)\n if (!parts) return null\n const [, year, month, day, hour, minute, second] = parts\n const iso = `${year}-${month}-${day}T${hour}:${minute}:${second}Z`\n const startAt = new Date(iso)\n if (Number.isNaN(startAt.getTime())) return null\n\n const hours = durationMatch[1] ? Number(durationMatch[1]) : 0\n const minutes = durationMatch[2] ? Number(durationMatch[2]) : 0\n const durationMinutes = Math.max(1, hours * 60 + minutes)\n const freq = freqMatch[1]\n if (freq !== 'DAILY' && freq !== 'WEEKLY') return null\n\n const countMatch = rule.match(/COUNT=(\\d+)/)\n const count = countMatch?.[1] ? Number(countMatch[1]) : undefined\n const repeat = freq === 'WEEKLY'\n ? 'weekly'\n : freq === 'DAILY' && count === 1\n ? 'once'\n : 'daily'\n return { startAt, durationMinutes, freq, count, repeat }\n}\n\nfunction buildExdateSets(exdates?: string[]) {\n const dateOnly = new Set<string>()\n const dateTime = new Set<number>()\n ;(exdates ?? []).forEach((value) => {\n if (typeof value !== 'string') return\n const trimmed = value.trim()\n if (!trimmed) return\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) {\n dateOnly.add(trimmed)\n return\n }\n const parsed = new Date(trimmed)\n if (!Number.isNaN(parsed.getTime())) {\n dateTime.add(parsed.getTime())\n }\n })\n return { dateOnly, dateTime }\n}\n\nfunction shouldExcludeOccurrence(startAt: Date, exdates?: string[]): boolean {\n const { dateOnly, dateTime } = buildExdateSets(exdates)\n if (dateTime.has(startAt.getTime())) return true\n const dayKey = startAt.toISOString().slice(0, 10)\n return dateOnly.has(dayKey)\n}\n\nfunction toDayKey(value: Date): string {\n return value.toISOString().slice(0, 10)\n}\n\nfunction startOfDay(value: Date): Date {\n return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()))\n}\n\nfunction expandRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: AvailabilityRange): AvailabilityWindow[] {\n const { startAt, durationMinutes, freq, count, repeat } = parsed\n if (repeat === 'once') {\n if (shouldExcludeOccurrence(startAt, rule.exdates)) return []\n const start = startOfDay(startAt)\n const end = new Date(start.getTime() + DAY_MS)\n if (end <= range.start || start >= range.end) return []\n return [{ start, end, ruleId: rule.id }]\n }\n const durationMs = durationMinutes * 60000\n const windows: AvailabilityWindow[] = []\n const addWindow = (start: Date) => {\n if (shouldExcludeOccurrence(start, rule.exdates)) return\n const end = new Date(start.getTime() + durationMs)\n if (end <= range.start || start >= range.end) return\n windows.push({ start, end, ruleId: rule.id })\n }\n\n if (freq === 'DAILY') {\n let cursor = new Date(startAt)\n let remaining = count ?? Number.POSITIVE_INFINITY\n while (cursor < range.end && remaining > 0) {\n addWindow(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n remaining -= 1\n }\n return windows\n }\n\n let cursor = new Date(startAt)\n let remaining = count ?? Number.POSITIVE_INFINITY\n if (cursor < range.start) {\n const diffDays = Math.floor((range.start.getTime() - cursor.getTime()) / DAY_MS)\n const weeksToAdd = Math.floor(diffDays / 7)\n if (weeksToAdd > 0) {\n cursor = new Date(cursor.getTime() + weeksToAdd * 7 * DAY_MS)\n }\n }\n while (cursor < range.end && remaining > 0) {\n addWindow(new Date(cursor))\n cursor = new Date(cursor.getTime() + 7 * DAY_MS)\n remaining -= 1\n }\n return windows\n}\n\nfunction expandRules(rules: AvailabilityRuleLike[], range: AvailabilityRange): AvailabilityWindow[] {\n const expanded = rules.flatMap((rule) => {\n const parsed = parseRrule(rule.rrule)\n if (!parsed) return []\n return expandRule(rule, parsed, range)\n })\n return expanded.sort((a, b) => a.start.getTime() - b.start.getTime())\n}\n\nfunction subtractWindow(window: AvailabilityWindow, blockers: AvailabilityWindow[]): AvailabilityWindow[] {\n let segments = [window]\n blockers.forEach((blocker) => {\n const next: AvailabilityWindow[] = []\n segments.forEach((segment) => {\n if (blocker.end <= segment.start || blocker.start >= segment.end) {\n next.push(segment)\n return\n }\n if (blocker.start > segment.start) {\n next.push({ ...segment, end: new Date(blocker.start) })\n }\n if (blocker.end < segment.end) {\n next.push({ ...segment, start: new Date(blocker.end) })\n }\n })\n segments = next\n })\n return segments\n}\n\nexport function getMergedAvailabilityWindows(params: {\n rules: AvailabilityRuleLike[]\n range: AvailabilityRange\n}): AvailabilityWindow[] {\n const parsedRules = params.rules\n .map((rule) => {\n const parsed = parseRrule(rule.rrule)\n if (!parsed) return null\n return { rule, parsed }\n })\n .filter((entry): entry is { rule: AvailabilityRuleLike; parsed: ParsedRule } => entry !== null)\n\n const overrideDays = new Map<string, AvailabilityKind>()\n parsedRules\n .filter(({ parsed }) => parsed.repeat === 'once')\n .forEach(({ rule, parsed }) => {\n const dayKey = toDayKey(parsed.startAt)\n const kind = rule.kind === 'unavailability' ? 'unavailability' : 'availability'\n if (kind === 'unavailability') {\n overrideDays.set(dayKey, 'unavailability')\n } else if (!overrideDays.has(dayKey)) {\n overrideDays.set(dayKey, 'availability')\n }\n })\n\n const availabilityRules = parsedRules\n .filter(({ parsed, rule }) => parsed.repeat !== 'once' && rule.kind !== 'unavailability')\n .map(({ rule }) => rule)\n const unavailabilityRules = parsedRules\n .filter(({ parsed, rule }) => parsed.repeat !== 'once' && rule.kind === 'unavailability')\n .map(({ rule }) => rule)\n\n const availabilityWindows = expandRules(availabilityRules, params.range)\n .filter((window) => !overrideDays.has(toDayKey(window.start)))\n const unavailabilityWindows = expandRules(unavailabilityRules, params.range)\n\n const merged = unavailabilityWindows.length === 0\n ? availabilityWindows\n : availabilityWindows.flatMap((window) => subtractWindow(window, unavailabilityWindows))\n\n const overrideWindows: AvailabilityWindow[] = []\n overrideDays.forEach((kind, dayKey) => {\n if (kind !== 'availability') return\n const start = new Date(`${dayKey}T00:00:00Z`)\n if (Number.isNaN(start.getTime())) return\n const end = new Date(start.getTime() + DAY_MS)\n if (end <= params.range.start || start >= params.range.end) return\n overrideWindows.push({ start, end })\n })\n\n return [...merged, ...overrideWindows].sort((a, b) => a.start.getTime() - b.start.getTime())\n}\n"],
5
- "mappings": "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",
6
- "names": ["cursor", "remaining"]
4
+ "sourcesContent": ["type AvailabilityKind = 'availability' | 'unavailability'\n\nexport type AvailabilityRuleLike = {\n id?: string\n rrule: string\n timezone?: string\n exdates?: string[]\n kind?: AvailabilityKind\n note?: string | null\n}\n\nexport type AvailabilityRange = {\n start: Date\n end: Date\n}\n\nexport type AvailabilityWindow = {\n start: Date\n end: Date\n ruleId?: string\n}\n\ntype ParsedRule = {\n startAt: Date\n durationMinutes: number\n freq: 'DAILY' | 'WEEKLY'\n repeat: 'once' | 'daily' | 'weekly'\n count?: number\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\nconst timezoneFormatters = new Map<string, Intl.DateTimeFormat>()\n\nfunction zonedParts(instant: Date, timezone: string): { year: number; month: number; day: number; hour: number; minute: number; second: number } {\n let formatter = timezoneFormatters.get(timezone)\n if (!formatter) {\n formatter = new Intl.DateTimeFormat('en-US', {\n timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit',\n hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',\n })\n if (timezoneFormatters.size >= 64) timezoneFormatters.delete(timezoneFormatters.keys().next().value!)\n timezoneFormatters.set(timezone, formatter)\n }\n const parts = formatter.formatToParts(instant)\n const number = (kind: string) => Number(parts.find((part) => part.type === kind)?.value)\n return { year: number('year'), month: number('month'), day: number('day'), hour: number('hour'), minute: number('minute'), second: number('second') }\n}\n\nfunction zonedWallToInstant(wall: Date, timezone: string): Date | null {\n const target = wall.getTime()\n let candidate = target\n for (let attempt = 0; attempt < 4; attempt += 1) {\n const parts = zonedParts(new Date(candidate), timezone)\n const displayed = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second)\n const next = candidate + target - displayed\n if (next === candidate) return new Date(candidate)\n candidate = next\n }\n return null\n}\n\nfunction zonedStartOfDay(dayKey: string, timezone: string): Date | null {\n return zonedWallToInstant(new Date(`${dayKey}T00:00:00Z`), timezone)\n}\n\nfunction dayKeyForRule(instant: Date, timezone?: string): string {\n if (!timezone || timezone === 'UTC' || timezone === 'Etc/UTC' || timezone === 'Etc/GMT') return toDayKey(instant)\n const parts = zonedParts(instant, timezone)\n return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`\n}\n\nfunction parseRrule(rule: string): ParsedRule | null {\n const dtStartMatch = rule.match(/DTSTART[:=](\\d{8}T\\d{6}Z?)/)\n const durationMatch = rule.match(/DURATION:PT(?:(\\d+)H)?(?:(\\d+)M)?/)\n const freqMatch = rule.match(/FREQ=([A-Z]+)/)\n if (!dtStartMatch?.[1] || !durationMatch || !freqMatch?.[1]) return null\n const raw = dtStartMatch[1].replace(/Z$/, '')\n const parts = raw.match(/(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})/)\n if (!parts) return null\n const [, year, month, day, hour, minute, second] = parts\n const iso = `${year}-${month}-${day}T${hour}:${minute}:${second}Z`\n const startAt = new Date(iso)\n if (Number.isNaN(startAt.getTime())) return null\n\n const hours = durationMatch[1] ? Number(durationMatch[1]) : 0\n const minutes = durationMatch[2] ? Number(durationMatch[2]) : 0\n const durationMinutes = Math.max(1, hours * 60 + minutes)\n const freq = freqMatch[1]\n if (freq !== 'DAILY' && freq !== 'WEEKLY') return null\n\n const countMatch = rule.match(/COUNT=(\\d+)/)\n const count = countMatch?.[1] ? Number(countMatch[1]) : undefined\n const repeat = freq === 'WEEKLY'\n ? 'weekly'\n : freq === 'DAILY' && count === 1\n ? 'once'\n : 'daily'\n return { startAt, durationMinutes, freq, count, repeat }\n}\n\nfunction buildExdateSets(exdates?: string[]) {\n const dateOnly = new Set<string>()\n const dateTime = new Set<number>()\n ;(exdates ?? []).forEach((value) => {\n if (typeof value !== 'string') return\n const trimmed = value.trim()\n if (!trimmed) return\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) {\n dateOnly.add(trimmed)\n return\n }\n const parsed = new Date(trimmed)\n if (!Number.isNaN(parsed.getTime())) {\n dateTime.add(parsed.getTime())\n }\n })\n return { dateOnly, dateTime }\n}\n\nfunction shouldExcludeOccurrence(startAt: Date, exdates?: string[], timezone?: string): boolean {\n const { dateOnly, dateTime } = buildExdateSets(exdates)\n if (dateTime.has(startAt.getTime())) return true\n const dayKey = dayKeyForRule(startAt, timezone)\n return dateOnly.has(dayKey)\n}\n\nfunction toDayKey(value: Date): string {\n return value.toISOString().slice(0, 10)\n}\n\nfunction startOfDay(value: Date): Date {\n return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()))\n}\n\nfunction expandZonedRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: AvailabilityRange, weeklyScheduleTemplate: boolean): AvailabilityWindow[] {\n const timezone = rule.timezone!\n const anchor = zonedParts(parsed.startAt, timezone)\n const anchorWall = Date.UTC(anchor.year, anchor.month - 1, anchor.day, anchor.hour, anchor.minute, anchor.second)\n const step = parsed.freq === 'WEEKLY' ? 7 * DAY_MS : DAY_MS\n const duration = parsed.durationMinutes * 60000\n const windows: AvailabilityWindow[] = []\n const rangeAnchor = zonedParts(range.start, timezone)\n const rangeStartWall = Date.UTC(rangeAnchor.year, rangeAnchor.month - 1, rangeAnchor.day, rangeAnchor.hour, rangeAnchor.minute, rangeAnchor.second)\n const rangeOffset = Math.floor((rangeStartWall - duration - DAY_MS - anchorWall) / step)\n const skipped = weeklyScheduleTemplate && parsed.freq === 'WEEKLY' && parsed.count === undefined ? rangeOffset : Math.max(0, rangeOffset)\n let wall = new Date(anchorWall + skipped * step)\n let remaining = (parsed.count ?? Number.POSITIVE_INFINITY) - skipped\n while (wall.getTime() <= range.end.getTime() + DAY_MS && remaining > 0) {\n const start = zonedWallToInstant(wall, timezone)\n if (start && !shouldExcludeOccurrence(start, rule.exdates, timezone)) {\n const end = new Date(start.getTime() + duration)\n if (end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id })\n }\n wall = new Date(wall.getTime() + step)\n remaining -= 1\n }\n return windows\n}\n\nfunction expandRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: AvailabilityRange, weeklyScheduleTemplate: boolean): AvailabilityWindow[] {\n const { startAt, durationMinutes, freq, count, repeat } = parsed\n if (repeat === 'once') {\n if (shouldExcludeOccurrence(startAt, rule.exdates, rule.timezone)) return []\n const dayKey = dayKeyForRule(startAt, rule.timezone)\n const start = rule.timezone ? zonedStartOfDay(dayKey, rule.timezone) : startOfDay(startAt)\n if (!start) return []\n const nextDay = new Date(new Date(`${dayKey}T00:00:00Z`).getTime() + DAY_MS).toISOString().slice(0, 10)\n const end = rule.timezone ? zonedStartOfDay(nextDay, rule.timezone) : new Date(start.getTime() + DAY_MS)\n if (!end) return []\n if (end <= range.start || start >= range.end) return []\n return [{ start, end, ruleId: rule.id }]\n }\n if (rule.timezone && !['UTC', 'Etc/UTC', 'Etc/GMT'].includes(rule.timezone)) {\n return expandZonedRule(rule, parsed, range, weeklyScheduleTemplate)\n }\n const durationMs = durationMinutes * 60000\n const windows: AvailabilityWindow[] = []\n const addWindow = (start: Date) => {\n if (shouldExcludeOccurrence(start, rule.exdates)) return\n const end = new Date(start.getTime() + durationMs)\n if (end <= range.start || start >= range.end) return\n windows.push({ start, end, ruleId: rule.id })\n }\n\n const step = freq === 'WEEKLY' ? 7 * DAY_MS : DAY_MS\n const rangeOffset = Math.floor((range.start.getTime() - durationMs - startAt.getTime()) / step)\n const skipped = weeklyScheduleTemplate && freq === 'WEEKLY' && count === undefined ? rangeOffset : Math.max(0, rangeOffset)\n let cursor = new Date(startAt.getTime() + skipped * step)\n let remaining = (count ?? Number.POSITIVE_INFINITY) - skipped\n while (cursor < range.end && remaining > 0) {\n addWindow(new Date(cursor))\n cursor = new Date(cursor.getTime() + step)\n remaining -= 1\n }\n return windows\n}\n\nfunction expandRules(rules: AvailabilityRuleLike[], range: AvailabilityRange, weeklyScheduleTemplate: boolean): AvailabilityWindow[] {\n const expanded = rules.flatMap((rule) => {\n const parsed = parseRrule(rule.rrule)\n if (!parsed) return []\n return expandRule(rule, parsed, range, weeklyScheduleTemplate)\n })\n return expanded.sort((a, b) => a.start.getTime() - b.start.getTime())\n}\n\nfunction subtractWindow(window: AvailabilityWindow, blockers: AvailabilityWindow[]): AvailabilityWindow[] {\n let segments = [window]\n blockers.forEach((blocker) => {\n const next: AvailabilityWindow[] = []\n segments.forEach((segment) => {\n if (blocker.end <= segment.start || blocker.start >= segment.end) {\n next.push(segment)\n return\n }\n if (blocker.start > segment.start) {\n next.push({ ...segment, end: new Date(blocker.start) })\n }\n if (blocker.end < segment.end) {\n next.push({ ...segment, start: new Date(blocker.end) })\n }\n })\n segments = next\n })\n return segments\n}\n\nexport function getMergedAvailabilityWindows(params: {\n rules: AvailabilityRuleLike[]\n range: AvailabilityRange\n respectTimezone?: boolean\n weeklyScheduleTemplate?: boolean\n}): AvailabilityWindow[] {\n const rules = params.respectTimezone ? params.rules : params.rules.map((rule) => ({ ...rule, timezone: undefined }))\n const parsedRules = rules\n .map((rule) => {\n const parsed = parseRrule(rule.rrule)\n if (!parsed) return null\n return { rule, parsed }\n })\n .filter((entry): entry is { rule: AvailabilityRuleLike; parsed: ParsedRule } => entry !== null)\n\n const overrideDays = new Map<string, { kind: AvailabilityKind; dayKey: string; timezone?: string }>()\n parsedRules\n .filter(({ parsed }) => parsed.repeat === 'once')\n .forEach(({ rule, parsed }) => {\n const dayKey = dayKeyForRule(parsed.startAt, rule.timezone)\n const overrideKey = `${rule.timezone ?? 'UTC'}|${dayKey}`\n const kind = rule.kind === 'unavailability' ? 'unavailability' : 'availability'\n if (kind === 'unavailability') {\n overrideDays.set(overrideKey, { kind, dayKey, timezone: rule.timezone })\n } else if (!overrideDays.has(overrideKey)) {\n overrideDays.set(overrideKey, { kind, dayKey, timezone: rule.timezone })\n }\n })\n\n const availabilityRules = parsedRules\n .filter(({ parsed, rule }) => parsed.repeat !== 'once' && rule.kind !== 'unavailability')\n .map(({ rule }) => rule)\n const unavailabilityRules = parsedRules\n .filter(({ parsed, rule }) => parsed.repeat !== 'once' && rule.kind === 'unavailability')\n .map(({ rule }) => rule)\n\n const availabilityWindows = availabilityRules.flatMap((rule) =>\n expandRules([rule], params.range, params.weeklyScheduleTemplate === true).filter((window) =>\n !overrideDays.has(`${rule.timezone ?? 'UTC'}|${dayKeyForRule(window.start, rule.timezone)}`)))\n const unavailabilityWindows = expandRules(unavailabilityRules, params.range, params.weeklyScheduleTemplate === true)\n\n const merged = unavailabilityWindows.length === 0\n ? availabilityWindows\n : availabilityWindows.flatMap((window) => subtractWindow(window, unavailabilityWindows))\n\n const overrideWindows: AvailabilityWindow[] = []\n overrideDays.forEach(({ kind, dayKey, timezone }) => {\n if (kind !== 'availability') return\n const start = timezone ? zonedStartOfDay(dayKey, timezone) : new Date(`${dayKey}T00:00:00Z`)\n if (!start) return\n if (Number.isNaN(start.getTime())) return\n const nextDay = new Date(new Date(`${dayKey}T00:00:00Z`).getTime() + DAY_MS).toISOString().slice(0, 10)\n const end = timezone ? zonedStartOfDay(nextDay, timezone) : new Date(start.getTime() + DAY_MS)\n if (!end) return\n if (end <= params.range.start || start >= params.range.end) return\n overrideWindows.push({ start, end })\n })\n\n return [...merged, ...overrideWindows].sort((a, b) => a.start.getTime() - b.start.getTime())\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/services/plannerAvailabilityService.ts"],
4
- "sourcesContent": ["import type { AvailabilityRange, AvailabilityRuleLike, AvailabilityWindow } from '../lib/availabilityMerge'\nimport { getMergedAvailabilityWindows } from '../lib/availabilityMerge'\n\nexport type { AvailabilityRange, AvailabilityRuleLike, AvailabilityWindow }\n\nexport interface PlannerAvailabilityService {\n getMergedAvailabilityWindows(params: {\n rules: AvailabilityRuleLike[]\n range: AvailabilityRange\n }): AvailabilityWindow[]\n}\n\nexport class DefaultPlannerAvailabilityService implements PlannerAvailabilityService {\n getMergedAvailabilityWindows(params: {\n rules: AvailabilityRuleLike[]\n range: AvailabilityRange\n }): AvailabilityWindow[] {\n return getMergedAvailabilityWindows(params)\n }\n}\n"],
5
- "mappings": "AACA,SAAS,oCAAoC;AAWtC,MAAM,kCAAwE;AAAA,EACnF,6BAA6B,QAGJ;AACvB,WAAO,6BAA6B,MAAM;AAAA,EAC5C;AACF;",
4
+ "sourcesContent": ["import type { AvailabilityRange, AvailabilityRuleLike, AvailabilityWindow } from '../lib/availabilityMerge'\nimport { getMergedAvailabilityWindows } from '../lib/availabilityMerge'\n\nexport type { AvailabilityRange, AvailabilityRuleLike, AvailabilityWindow }\n\nexport interface PlannerAvailabilityService {\n getMergedAvailabilityWindows(params: {\n rules: AvailabilityRuleLike[]\n range: AvailabilityRange\n respectTimezone?: boolean\n weeklyScheduleTemplate?: boolean\n }): AvailabilityWindow[]\n}\n\nexport class DefaultPlannerAvailabilityService implements PlannerAvailabilityService {\n getMergedAvailabilityWindows(params: {\n rules: AvailabilityRuleLike[]\n range: AvailabilityRange\n respectTimezone?: boolean\n weeklyScheduleTemplate?: boolean\n }): AvailabilityWindow[] {\n return getMergedAvailabilityWindows(params)\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,oCAAoC;AAatC,MAAM,kCAAwE;AAAA,EACnF,6BAA6B,QAKJ;AACvB,WAAO,6BAA6B,MAAM;AAAA,EAC5C;AACF;",
6
6
  "names": []
7
7
  }
@@ -43,7 +43,11 @@ import {
43
43
  import { E } from "../../../../generated/entities.ids.generated.js";
44
44
  import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
45
45
  import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
46
- import { formatMoney, normalizeNumber } from "./lineItemUtils.js";
46
+ import {
47
+ formatMoney,
48
+ normalizeNumber,
49
+ resolveCatalogPriceAmount
50
+ } from "./lineItemUtils.js";
47
51
  import { prepareShippedLineUpdatePayload } from "./lineItemShipmentLock.js";
48
52
  import {
49
53
  normalizeCustomFieldSubmitValue,
@@ -256,6 +260,7 @@ function LineItemDialog({
256
260
  const [, setLineStatusLoading] = React.useState(false);
257
261
  const productOptionsRef = React.useRef(/* @__PURE__ */ new Map());
258
262
  const variantOptionsRef = React.useRef(/* @__PURE__ */ new Map());
263
+ const selectedProductIdRef = React.useRef(null);
259
264
  const taxRatesRef = React.useRef([]);
260
265
  const defaultTaxRateRef = React.useRef(null);
261
266
  const dialogContentRef = React.useRef(null);
@@ -320,6 +325,7 @@ function LineItemDialog({
320
325
  }
321
326
  setInitialValues(base);
322
327
  setLineMode(base.lineMode);
328
+ selectedProductIdRef.current = null;
323
329
  setProductOption(null);
324
330
  setVariantOption(null);
325
331
  setPriceOptions([]);
@@ -618,7 +624,13 @@ function LineItemDialog({
618
624
  scopeReason: reason,
619
625
  scopeTags: tags
620
626
  };
621
- }).filter((entry) => Boolean(entry));
627
+ }).filter((entry) => Boolean(entry)).filter(
628
+ (entry) => resolveCatalogPriceAmount(
629
+ entry.amountNet,
630
+ entry.amountGross,
631
+ entry.displayMode === "excluding-tax" ? "net" : "gross"
632
+ ) !== null
633
+ );
622
634
  setPriceOptions(mapped);
623
635
  return mapped;
624
636
  } catch (err) {
@@ -682,7 +694,11 @@ function LineItemDialog({
682
694
  if (!setFormValue) return;
683
695
  if (selected) {
684
696
  const mode = selected.displayMode === "excluding-tax" ? "net" : "gross";
685
- const amountPerBaseUnit = mode === "net" ? selected.amountNet ?? selected.amountGross ?? 0 : selected.amountGross ?? selected.amountNet ?? 0;
697
+ const amountPerBaseUnit = resolveCatalogPriceAmount(
698
+ selected.amountNet,
699
+ selected.amountGross,
700
+ mode
701
+ ) ?? 0;
686
702
  const factor = resolveUnitPriceFactor(options?.quantityUnit ?? null);
687
703
  const amount = Number.isFinite(amountPerBaseUnit * factor) ? amountPerBaseUnit * factor : amountPerBaseUnit;
688
704
  setFormValue("priceId", selected.id);
@@ -702,6 +718,101 @@ function LineItemDialog({
702
718
  },
703
719
  [currencyCode, findTaxRateIdByValue, resolveTaxSelection, resolveUnitPriceFactor]
704
720
  );
721
+ const commitVariantSelection = React.useCallback(
722
+ ({
723
+ variantId,
724
+ productId,
725
+ product,
726
+ values,
727
+ setFormValue
728
+ }) => {
729
+ const selectedOption = variantId ? variantOptionsRef.current.get(variantId) ?? null : null;
730
+ setVariantOption(selectedOption);
731
+ setFormValue?.("variantId", variantId);
732
+ const existingName = typeof values?.name === "string" ? values.name : "";
733
+ if (!existingName.trim()) {
734
+ setFormValue?.(
735
+ "name",
736
+ selectedOption?.title ?? product?.title ?? existingName
737
+ );
738
+ }
739
+ const taxSource = hasTaxMetadata(selectedOption) ? selectedOption : hasTaxMetadata(product) ? product : null;
740
+ if (taxSource) {
741
+ const taxSelection = resolveTaxSelection(taxSource);
742
+ setFormValue?.("taxRate", taxSelection.taxRate ?? null);
743
+ setFormValue?.("taxRateId", taxSelection.taxRateId ?? null);
744
+ }
745
+ const prevSnapshot = typeof values?.catalogSnapshot === "object" && values.catalogSnapshot ? values.catalogSnapshot : null;
746
+ if (variantId) {
747
+ setFormValue?.("catalogSnapshot", {
748
+ ...prevSnapshot ?? {},
749
+ variant: {
750
+ id: variantId,
751
+ title: selectedOption?.title ?? null,
752
+ sku: selectedOption?.sku ?? null,
753
+ thumbnailUrl: selectedOption?.thumbnailUrl ?? null
754
+ }
755
+ });
756
+ } else if (prevSnapshot) {
757
+ const snapshot = { ...prevSnapshot };
758
+ if ("variant" in snapshot)
759
+ delete snapshot.variant;
760
+ setFormValue?.(
761
+ "catalogSnapshot",
762
+ Object.keys(snapshot).length ? snapshot : null
763
+ );
764
+ } else {
765
+ setFormValue?.("catalogSnapshot", null);
766
+ }
767
+ if (productId) {
768
+ const currentQuantity = typeof values?.quantity === "string" ? values.quantity : 1;
769
+ const currentQuantityUnit = typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
770
+ void loadPrices(
771
+ productId,
772
+ variantId,
773
+ currentQuantity,
774
+ currentQuantityUnit
775
+ );
776
+ }
777
+ },
778
+ [hasTaxMetadata, loadPrices, resolveTaxSelection]
779
+ );
780
+ const autoSelectSoleVariant = React.useCallback(
781
+ async ({
782
+ productId,
783
+ product,
784
+ values,
785
+ setFormValue
786
+ }) => {
787
+ let variants = [];
788
+ try {
789
+ variants = await loadVariantOptions(
790
+ productId,
791
+ product?.thumbnailUrl ?? null
792
+ );
793
+ } catch (err) {
794
+ logger.error("sales.document.items.autoSelectVariant", { err });
795
+ }
796
+ if (selectedProductIdRef.current !== productId) return;
797
+ if (variants.length === 1) {
798
+ commitVariantSelection({
799
+ variantId: variants[0].id,
800
+ productId,
801
+ product,
802
+ values,
803
+ setFormValue
804
+ });
805
+ return;
806
+ }
807
+ void loadPrices(
808
+ productId,
809
+ null,
810
+ typeof values.quantity === "string" ? values.quantity : 1,
811
+ typeof values.quantityUnit === "string" ? values.quantityUnit : null
812
+ );
813
+ },
814
+ [commitVariantSelection, loadPrices, loadVariantOptions]
815
+ );
705
816
  const loadLineStatuses = React.useCallback(async () => {
706
817
  setLineStatusLoading(true);
707
818
  try {
@@ -1032,6 +1143,7 @@ function LineItemDialog({
1032
1143
  const switchMode = (next) => {
1033
1144
  if (next === mode) return;
1034
1145
  setValue(next);
1146
+ selectedProductIdRef.current = null;
1035
1147
  setLineMode(next);
1036
1148
  if (next === "custom") {
1037
1149
  setProductOption(null);
@@ -1143,13 +1255,11 @@ function LineItemDialog({
1143
1255
  setFormValue?.("priceMode", "gross");
1144
1256
  const defaultQuantityUnit = selectedOption?.defaultSalesUnit ?? selectedOption?.defaultUnit ?? null;
1145
1257
  setFormValue?.("quantityUnit", defaultQuantityUnit);
1146
- if (typeof selectedOption?.defaultSalesUnitQuantity === "number" && Number.isFinite(
1258
+ const defaultQuantity = typeof selectedOption?.defaultSalesUnitQuantity === "number" && Number.isFinite(
1147
1259
  selectedOption.defaultSalesUnitQuantity
1148
- ) && selectedOption.defaultSalesUnitQuantity > 0) {
1149
- setFormValue?.(
1150
- "quantity",
1151
- String(selectedOption.defaultSalesUnitQuantity)
1152
- );
1260
+ ) && selectedOption.defaultSalesUnitQuantity > 0 ? String(selectedOption.defaultSalesUnitQuantity) : null;
1261
+ if (defaultQuantity) {
1262
+ setFormValue?.("quantity", defaultQuantity);
1153
1263
  }
1154
1264
  const taxSelection = selectedOption ? resolveTaxSelection(selectedOption) : { taxRate: null, taxRateId: null };
1155
1265
  setFormValue?.("taxRate", taxSelection.taxRate ?? null);
@@ -1158,27 +1268,33 @@ function LineItemDialog({
1158
1268
  if (!existingName.trim() && selectedOption?.title) {
1159
1269
  setFormValue?.("name", selectedOption.title);
1160
1270
  }
1161
- setFormValue?.(
1162
- "catalogSnapshot",
1163
- next ? {
1164
- product: {
1165
- id: next,
1166
- title: selectedOption?.title ?? null,
1167
- sku: selectedOption?.sku ?? null,
1168
- thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
1169
- defaultUnit: selectedOption?.defaultUnit ?? null,
1170
- defaultSalesUnit: selectedOption?.defaultSalesUnit ?? null
1171
- }
1172
- } : null
1173
- );
1271
+ const productSnapshot = next ? {
1272
+ product: {
1273
+ id: next,
1274
+ title: selectedOption?.title ?? null,
1275
+ sku: selectedOption?.sku ?? null,
1276
+ thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
1277
+ defaultUnit: selectedOption?.defaultUnit ?? null,
1278
+ defaultSalesUnit: selectedOption?.defaultSalesUnit ?? null
1279
+ }
1280
+ } : null;
1281
+ setFormValue?.("catalogSnapshot", productSnapshot);
1282
+ selectedProductIdRef.current = next ?? null;
1174
1283
  if (next) {
1175
1284
  void loadProductUnits(next, selectedOption);
1176
- void loadPrices(
1177
- next,
1178
- null,
1179
- typeof values?.quantity === "string" ? values.quantity : 1,
1180
- defaultQuantityUnit
1181
- );
1285
+ const valuesAfterProductChange = {
1286
+ ...values,
1287
+ name: existingName.trim() || selectedOption?.title || "",
1288
+ catalogSnapshot: productSnapshot,
1289
+ quantity: defaultQuantity ?? values?.quantity,
1290
+ quantityUnit: defaultQuantityUnit
1291
+ };
1292
+ void autoSelectSoleVariant({
1293
+ productId: next,
1294
+ product: selectedOption,
1295
+ values: valuesAfterProductChange,
1296
+ setFormValue
1297
+ });
1182
1298
  }
1183
1299
  },
1184
1300
  fetchItems: loadProductOptions,
@@ -1245,57 +1361,14 @@ function LineItemDialog({
1245
1361
  {
1246
1362
  value: typeof value === "string" ? value : null,
1247
1363
  onChange: (next) => {
1248
- const selectedOption = next ? variantOptionsRef.current.get(next) ?? null : null;
1249
- setVariantOption(selectedOption);
1250
1364
  setValue(next ?? null);
1251
- const existingName = typeof values?.name === "string" ? values.name : "";
1252
- if (!existingName.trim()) {
1253
- setFormValue?.(
1254
- "name",
1255
- selectedOption?.title ?? productOption?.title ?? existingName
1256
- );
1257
- }
1258
- const taxSource = hasTaxMetadata(selectedOption) ? selectedOption : hasTaxMetadata(productOption) ? productOption : null;
1259
- if (taxSource) {
1260
- const taxSelection = resolveTaxSelection(taxSource);
1261
- setFormValue?.("taxRate", taxSelection.taxRate ?? null);
1262
- setFormValue?.(
1263
- "taxRateId",
1264
- taxSelection.taxRateId ?? null
1265
- );
1266
- }
1267
- const prevSnapshot = typeof values?.catalogSnapshot === "object" && values.catalogSnapshot ? values.catalogSnapshot : null;
1268
- if (next) {
1269
- setFormValue?.("catalogSnapshot", {
1270
- ...prevSnapshot ?? {},
1271
- variant: {
1272
- id: next,
1273
- title: selectedOption?.title ?? null,
1274
- sku: selectedOption?.sku ?? null,
1275
- thumbnailUrl: selectedOption?.thumbnailUrl ?? null
1276
- }
1277
- });
1278
- } else if (prevSnapshot) {
1279
- const snapshot = { ...prevSnapshot };
1280
- if ("variant" in snapshot)
1281
- delete snapshot.variant;
1282
- setFormValue?.(
1283
- "catalogSnapshot",
1284
- Object.keys(snapshot).length ? snapshot : null
1285
- );
1286
- } else {
1287
- setFormValue?.("catalogSnapshot", null);
1288
- }
1289
- if (productId) {
1290
- const currentQuantity = typeof values?.quantity === "string" ? values.quantity : 1;
1291
- const currentQuantityUnit = typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
1292
- void loadPrices(
1293
- productId,
1294
- next,
1295
- currentQuantity,
1296
- currentQuantityUnit
1297
- );
1298
- }
1365
+ commitVariantSelection({
1366
+ variantId: next ?? null,
1367
+ productId,
1368
+ product: productOption,
1369
+ values,
1370
+ setFormValue
1371
+ });
1299
1372
  },
1300
1373
  fetchItems: async (query) => {
1301
1374
  if (!productId) return [];
@@ -1556,9 +1629,10 @@ function LineItemDialog({
1556
1629
  const selectedTaxRate = resolvedValue ? taxRateMap.get(resolvedValue) ?? null : null;
1557
1630
  const handleChange = (event) => {
1558
1631
  const nextId = event.target.value || null;
1559
- const option = nextId ? taxRateMap.get(nextId) ?? null : null;
1632
+ if (!nextId) return;
1633
+ const option = taxRateMap.get(nextId) ?? null;
1560
1634
  setValue(nextId);
1561
- const rate = normalizeNumber(option?.rate);
1635
+ const rate = normalizeNumber(option?.rate, Number.NaN);
1562
1636
  setFormValue?.("taxRate", Number.isFinite(rate) ? rate : null);
1563
1637
  };
1564
1638
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -1829,6 +1903,8 @@ function LineItemDialog({
1829
1903
  ];
1830
1904
  }, [
1831
1905
  applyPriceSelection,
1906
+ autoSelectSoleVariant,
1907
+ commitVariantSelection,
1832
1908
  convertUnitPriceForUnitChange,
1833
1909
  currencyCode,
1834
1910
  findTaxRateIdByValue,
@@ -1871,6 +1947,7 @@ function LineItemDialog({
1871
1947
  return;
1872
1948
  }
1873
1949
  let cancelled = false;
1950
+ selectedProductIdRef.current = null;
1874
1951
  setDeletedCatalogReference(false);
1875
1952
  setEditingId(initialLine.id);
1876
1953
  const nextForm = defaultForm(initialLine.currencyCode ?? currencyCode);
@@ -1896,7 +1973,8 @@ function LineItemDialog({
1896
1973
  nextForm.lineMode = metaLineMode ?? (initialLine.productId || initialLine.productVariantId ? "catalog" : "custom");
1897
1974
  const metaTaxRateId = typeof metaRec?.taxRateId === "string" ? metaRec.taxRateId : null;
1898
1975
  const fallbackTaxRateId = findTaxRateIdByValue(nextForm.taxRate);
1899
- nextForm.taxRateId = metaTaxRateId ?? fallbackTaxRateId ?? (defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null);
1976
+ const defaultTaxRateId = !Number.isFinite(nextForm.taxRate) && defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null;
1977
+ nextForm.taxRateId = metaTaxRateId ?? fallbackTaxRateId ?? defaultTaxRateId;
1900
1978
  if (!Number.isFinite(nextForm.taxRate) && nextForm.taxRateId) {
1901
1979
  const matched = taxRatesRef.current.find(
1902
1980
  (rate) => rate.id === nextForm.taxRateId