@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
@@ -3,6 +3,7 @@ type AvailabilityKind = 'availability' | 'unavailability'
3
3
  export type AvailabilityRuleLike = {
4
4
  id?: string
5
5
  rrule: string
6
+ timezone?: string
6
7
  exdates?: string[]
7
8
  kind?: AvailabilityKind
8
9
  note?: string | null
@@ -28,6 +29,45 @@ type ParsedRule = {
28
29
  }
29
30
 
30
31
  const DAY_MS = 24 * 60 * 60 * 1000
32
+ const timezoneFormatters = new Map<string, Intl.DateTimeFormat>()
33
+
34
+ function zonedParts(instant: Date, timezone: string): { year: number; month: number; day: number; hour: number; minute: number; second: number } {
35
+ let formatter = timezoneFormatters.get(timezone)
36
+ if (!formatter) {
37
+ formatter = new Intl.DateTimeFormat('en-US', {
38
+ timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit',
39
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
40
+ })
41
+ if (timezoneFormatters.size >= 64) timezoneFormatters.delete(timezoneFormatters.keys().next().value!)
42
+ timezoneFormatters.set(timezone, formatter)
43
+ }
44
+ const parts = formatter.formatToParts(instant)
45
+ const number = (kind: string) => Number(parts.find((part) => part.type === kind)?.value)
46
+ return { year: number('year'), month: number('month'), day: number('day'), hour: number('hour'), minute: number('minute'), second: number('second') }
47
+ }
48
+
49
+ function zonedWallToInstant(wall: Date, timezone: string): Date | null {
50
+ const target = wall.getTime()
51
+ let candidate = target
52
+ for (let attempt = 0; attempt < 4; attempt += 1) {
53
+ const parts = zonedParts(new Date(candidate), timezone)
54
+ const displayed = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second)
55
+ const next = candidate + target - displayed
56
+ if (next === candidate) return new Date(candidate)
57
+ candidate = next
58
+ }
59
+ return null
60
+ }
61
+
62
+ function zonedStartOfDay(dayKey: string, timezone: string): Date | null {
63
+ return zonedWallToInstant(new Date(`${dayKey}T00:00:00Z`), timezone)
64
+ }
65
+
66
+ function dayKeyForRule(instant: Date, timezone?: string): string {
67
+ if (!timezone || timezone === 'UTC' || timezone === 'Etc/UTC' || timezone === 'Etc/GMT') return toDayKey(instant)
68
+ const parts = zonedParts(instant, timezone)
69
+ return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`
70
+ }
31
71
 
32
72
  function parseRrule(rule: string): ParsedRule | null {
33
73
  const dtStartMatch = rule.match(/DTSTART[:=](\d{8}T\d{6}Z?)/)
@@ -77,10 +117,10 @@ function buildExdateSets(exdates?: string[]) {
77
117
  return { dateOnly, dateTime }
78
118
  }
79
119
 
80
- function shouldExcludeOccurrence(startAt: Date, exdates?: string[]): boolean {
120
+ function shouldExcludeOccurrence(startAt: Date, exdates?: string[], timezone?: string): boolean {
81
121
  const { dateOnly, dateTime } = buildExdateSets(exdates)
82
122
  if (dateTime.has(startAt.getTime())) return true
83
- const dayKey = startAt.toISOString().slice(0, 10)
123
+ const dayKey = dayKeyForRule(startAt, timezone)
84
124
  return dateOnly.has(dayKey)
85
125
  }
86
126
 
@@ -92,15 +132,47 @@ function startOfDay(value: Date): Date {
92
132
  return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()))
93
133
  }
94
134
 
95
- function expandRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: AvailabilityRange): AvailabilityWindow[] {
135
+ function expandZonedRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: AvailabilityRange, weeklyScheduleTemplate: boolean): AvailabilityWindow[] {
136
+ const timezone = rule.timezone!
137
+ const anchor = zonedParts(parsed.startAt, timezone)
138
+ const anchorWall = Date.UTC(anchor.year, anchor.month - 1, anchor.day, anchor.hour, anchor.minute, anchor.second)
139
+ const step = parsed.freq === 'WEEKLY' ? 7 * DAY_MS : DAY_MS
140
+ const duration = parsed.durationMinutes * 60000
141
+ const windows: AvailabilityWindow[] = []
142
+ const rangeAnchor = zonedParts(range.start, timezone)
143
+ const rangeStartWall = Date.UTC(rangeAnchor.year, rangeAnchor.month - 1, rangeAnchor.day, rangeAnchor.hour, rangeAnchor.minute, rangeAnchor.second)
144
+ const rangeOffset = Math.floor((rangeStartWall - duration - DAY_MS - anchorWall) / step)
145
+ const skipped = weeklyScheduleTemplate && parsed.freq === 'WEEKLY' && parsed.count === undefined ? rangeOffset : Math.max(0, rangeOffset)
146
+ let wall = new Date(anchorWall + skipped * step)
147
+ let remaining = (parsed.count ?? Number.POSITIVE_INFINITY) - skipped
148
+ while (wall.getTime() <= range.end.getTime() + DAY_MS && remaining > 0) {
149
+ const start = zonedWallToInstant(wall, timezone)
150
+ if (start && !shouldExcludeOccurrence(start, rule.exdates, timezone)) {
151
+ const end = new Date(start.getTime() + duration)
152
+ if (end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id })
153
+ }
154
+ wall = new Date(wall.getTime() + step)
155
+ remaining -= 1
156
+ }
157
+ return windows
158
+ }
159
+
160
+ function expandRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: AvailabilityRange, weeklyScheduleTemplate: boolean): AvailabilityWindow[] {
96
161
  const { startAt, durationMinutes, freq, count, repeat } = parsed
97
162
  if (repeat === 'once') {
98
- if (shouldExcludeOccurrence(startAt, rule.exdates)) return []
99
- const start = startOfDay(startAt)
100
- const end = new Date(start.getTime() + DAY_MS)
163
+ if (shouldExcludeOccurrence(startAt, rule.exdates, rule.timezone)) return []
164
+ const dayKey = dayKeyForRule(startAt, rule.timezone)
165
+ const start = rule.timezone ? zonedStartOfDay(dayKey, rule.timezone) : startOfDay(startAt)
166
+ if (!start) return []
167
+ const nextDay = new Date(new Date(`${dayKey}T00:00:00Z`).getTime() + DAY_MS).toISOString().slice(0, 10)
168
+ const end = rule.timezone ? zonedStartOfDay(nextDay, rule.timezone) : new Date(start.getTime() + DAY_MS)
169
+ if (!end) return []
101
170
  if (end <= range.start || start >= range.end) return []
102
171
  return [{ start, end, ruleId: rule.id }]
103
172
  }
173
+ if (rule.timezone && !['UTC', 'Etc/UTC', 'Etc/GMT'].includes(rule.timezone)) {
174
+ return expandZonedRule(rule, parsed, range, weeklyScheduleTemplate)
175
+ }
104
176
  const durationMs = durationMinutes * 60000
105
177
  const windows: AvailabilityWindow[] = []
106
178
  const addWindow = (start: Date) => {
@@ -110,39 +182,24 @@ function expandRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range: Avail
110
182
  windows.push({ start, end, ruleId: rule.id })
111
183
  }
112
184
 
113
- if (freq === 'DAILY') {
114
- let cursor = new Date(startAt)
115
- let remaining = count ?? Number.POSITIVE_INFINITY
116
- while (cursor < range.end && remaining > 0) {
117
- addWindow(new Date(cursor))
118
- cursor = new Date(cursor.getTime() + DAY_MS)
119
- remaining -= 1
120
- }
121
- return windows
122
- }
123
-
124
- let cursor = new Date(startAt)
125
- let remaining = count ?? Number.POSITIVE_INFINITY
126
- if (cursor < range.start) {
127
- const diffDays = Math.floor((range.start.getTime() - cursor.getTime()) / DAY_MS)
128
- const weeksToAdd = Math.floor(diffDays / 7)
129
- if (weeksToAdd > 0) {
130
- cursor = new Date(cursor.getTime() + weeksToAdd * 7 * DAY_MS)
131
- }
132
- }
185
+ const step = freq === 'WEEKLY' ? 7 * DAY_MS : DAY_MS
186
+ const rangeOffset = Math.floor((range.start.getTime() - durationMs - startAt.getTime()) / step)
187
+ const skipped = weeklyScheduleTemplate && freq === 'WEEKLY' && count === undefined ? rangeOffset : Math.max(0, rangeOffset)
188
+ let cursor = new Date(startAt.getTime() + skipped * step)
189
+ let remaining = (count ?? Number.POSITIVE_INFINITY) - skipped
133
190
  while (cursor < range.end && remaining > 0) {
134
191
  addWindow(new Date(cursor))
135
- cursor = new Date(cursor.getTime() + 7 * DAY_MS)
192
+ cursor = new Date(cursor.getTime() + step)
136
193
  remaining -= 1
137
194
  }
138
195
  return windows
139
196
  }
140
197
 
141
- function expandRules(rules: AvailabilityRuleLike[], range: AvailabilityRange): AvailabilityWindow[] {
198
+ function expandRules(rules: AvailabilityRuleLike[], range: AvailabilityRange, weeklyScheduleTemplate: boolean): AvailabilityWindow[] {
142
199
  const expanded = rules.flatMap((rule) => {
143
200
  const parsed = parseRrule(rule.rrule)
144
201
  if (!parsed) return []
145
- return expandRule(rule, parsed, range)
202
+ return expandRule(rule, parsed, range, weeklyScheduleTemplate)
146
203
  })
147
204
  return expanded.sort((a, b) => a.start.getTime() - b.start.getTime())
148
205
  }
@@ -171,8 +228,11 @@ function subtractWindow(window: AvailabilityWindow, blockers: AvailabilityWindow
171
228
  export function getMergedAvailabilityWindows(params: {
172
229
  rules: AvailabilityRuleLike[]
173
230
  range: AvailabilityRange
231
+ respectTimezone?: boolean
232
+ weeklyScheduleTemplate?: boolean
174
233
  }): AvailabilityWindow[] {
175
- const parsedRules = params.rules
234
+ const rules = params.respectTimezone ? params.rules : params.rules.map((rule) => ({ ...rule, timezone: undefined }))
235
+ const parsedRules = rules
176
236
  .map((rule) => {
177
237
  const parsed = parseRrule(rule.rrule)
178
238
  if (!parsed) return null
@@ -180,16 +240,17 @@ export function getMergedAvailabilityWindows(params: {
180
240
  })
181
241
  .filter((entry): entry is { rule: AvailabilityRuleLike; parsed: ParsedRule } => entry !== null)
182
242
 
183
- const overrideDays = new Map<string, AvailabilityKind>()
243
+ const overrideDays = new Map<string, { kind: AvailabilityKind; dayKey: string; timezone?: string }>()
184
244
  parsedRules
185
245
  .filter(({ parsed }) => parsed.repeat === 'once')
186
246
  .forEach(({ rule, parsed }) => {
187
- const dayKey = toDayKey(parsed.startAt)
247
+ const dayKey = dayKeyForRule(parsed.startAt, rule.timezone)
248
+ const overrideKey = `${rule.timezone ?? 'UTC'}|${dayKey}`
188
249
  const kind = rule.kind === 'unavailability' ? 'unavailability' : 'availability'
189
250
  if (kind === 'unavailability') {
190
- overrideDays.set(dayKey, 'unavailability')
191
- } else if (!overrideDays.has(dayKey)) {
192
- overrideDays.set(dayKey, 'availability')
251
+ overrideDays.set(overrideKey, { kind, dayKey, timezone: rule.timezone })
252
+ } else if (!overrideDays.has(overrideKey)) {
253
+ overrideDays.set(overrideKey, { kind, dayKey, timezone: rule.timezone })
193
254
  }
194
255
  })
195
256
 
@@ -200,20 +261,24 @@ export function getMergedAvailabilityWindows(params: {
200
261
  .filter(({ parsed, rule }) => parsed.repeat !== 'once' && rule.kind === 'unavailability')
201
262
  .map(({ rule }) => rule)
202
263
 
203
- const availabilityWindows = expandRules(availabilityRules, params.range)
204
- .filter((window) => !overrideDays.has(toDayKey(window.start)))
205
- const unavailabilityWindows = expandRules(unavailabilityRules, params.range)
264
+ const availabilityWindows = availabilityRules.flatMap((rule) =>
265
+ expandRules([rule], params.range, params.weeklyScheduleTemplate === true).filter((window) =>
266
+ !overrideDays.has(`${rule.timezone ?? 'UTC'}|${dayKeyForRule(window.start, rule.timezone)}`)))
267
+ const unavailabilityWindows = expandRules(unavailabilityRules, params.range, params.weeklyScheduleTemplate === true)
206
268
 
207
269
  const merged = unavailabilityWindows.length === 0
208
270
  ? availabilityWindows
209
271
  : availabilityWindows.flatMap((window) => subtractWindow(window, unavailabilityWindows))
210
272
 
211
273
  const overrideWindows: AvailabilityWindow[] = []
212
- overrideDays.forEach((kind, dayKey) => {
274
+ overrideDays.forEach(({ kind, dayKey, timezone }) => {
213
275
  if (kind !== 'availability') return
214
- const start = new Date(`${dayKey}T00:00:00Z`)
276
+ const start = timezone ? zonedStartOfDay(dayKey, timezone) : new Date(`${dayKey}T00:00:00Z`)
277
+ if (!start) return
215
278
  if (Number.isNaN(start.getTime())) return
216
- const end = new Date(start.getTime() + DAY_MS)
279
+ const nextDay = new Date(new Date(`${dayKey}T00:00:00Z`).getTime() + DAY_MS).toISOString().slice(0, 10)
280
+ const end = timezone ? zonedStartOfDay(nextDay, timezone) : new Date(start.getTime() + DAY_MS)
281
+ if (!end) return
217
282
  if (end <= params.range.start || start >= params.range.end) return
218
283
  overrideWindows.push({ start, end })
219
284
  })
@@ -7,6 +7,8 @@ export interface PlannerAvailabilityService {
7
7
  getMergedAvailabilityWindows(params: {
8
8
  rules: AvailabilityRuleLike[]
9
9
  range: AvailabilityRange
10
+ respectTimezone?: boolean
11
+ weeklyScheduleTemplate?: boolean
10
12
  }): AvailabilityWindow[]
11
13
  }
12
14
 
@@ -14,6 +16,8 @@ export class DefaultPlannerAvailabilityService implements PlannerAvailabilitySer
14
16
  getMergedAvailabilityWindows(params: {
15
17
  rules: AvailabilityRuleLike[]
16
18
  range: AvailabilityRange
19
+ respectTimezone?: boolean
20
+ weeklyScheduleTemplate?: boolean
17
21
  }): AvailabilityWindow[] {
18
22
  return getMergedAvailabilityWindows(params)
19
23
  }
@@ -48,7 +48,11 @@ import {
48
48
  import { E } from "#generated/entities.ids.generated";
49
49
  import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
50
50
  import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
51
- import { formatMoney, normalizeNumber } from "./lineItemUtils";
51
+ import {
52
+ formatMoney,
53
+ normalizeNumber,
54
+ resolveCatalogPriceAmount,
55
+ } from "./lineItemUtils";
52
56
  import type { SalesLineRecord } from "./lineItemTypes";
53
57
  import { prepareShippedLineUpdatePayload } from "./lineItemShipmentLock";
54
58
  import {
@@ -540,6 +544,7 @@ export function LineItemDialog({
540
544
  const [, setLineStatusLoading] = React.useState(false);
541
545
  const productOptionsRef = React.useRef<Map<string, ProductOption>>(new Map());
542
546
  const variantOptionsRef = React.useRef<Map<string, VariantOption>>(new Map());
547
+ const selectedProductIdRef = React.useRef<string | null>(null);
543
548
  const taxRatesRef = React.useRef<TaxRateOption[]>([]);
544
549
  const defaultTaxRateRef = React.useRef<TaxRateOption | null>(null);
545
550
  const dialogContentRef = React.useRef<HTMLDivElement | null>(null);
@@ -645,6 +650,7 @@ export function LineItemDialog({
645
650
  }
646
651
  setInitialValues(base);
647
652
  setLineMode(base.lineMode);
653
+ selectedProductIdRef.current = null;
648
654
  setProductOption(null);
649
655
  setVariantOption(null);
650
656
  setPriceOptions([]);
@@ -1108,7 +1114,15 @@ export function LineItemDialog({
1108
1114
  scopeTags: tags,
1109
1115
  } as PriceOption;
1110
1116
  })
1111
- .filter((entry): entry is PriceOption => Boolean(entry));
1117
+ .filter((entry): entry is PriceOption => Boolean(entry))
1118
+ .filter(
1119
+ (entry) =>
1120
+ resolveCatalogPriceAmount(
1121
+ entry.amountNet,
1122
+ entry.amountGross,
1123
+ entry.displayMode === "excluding-tax" ? "net" : "gross",
1124
+ ) !== null,
1125
+ );
1112
1126
  setPriceOptions(mapped);
1113
1127
  return mapped;
1114
1128
  } catch (err) {
@@ -1198,9 +1212,11 @@ export function LineItemDialog({
1198
1212
  const mode =
1199
1213
  selected.displayMode === "excluding-tax" ? "net" : "gross";
1200
1214
  const amountPerBaseUnit =
1201
- mode === "net"
1202
- ? (selected.amountNet ?? selected.amountGross ?? 0)
1203
- : (selected.amountGross ?? selected.amountNet ?? 0);
1215
+ resolveCatalogPriceAmount(
1216
+ selected.amountNet,
1217
+ selected.amountGross,
1218
+ mode,
1219
+ ) ?? 0;
1204
1220
  const factor = resolveUnitPriceFactor(options?.quantityUnit ?? null);
1205
1221
  const amount = Number.isFinite(amountPerBaseUnit * factor)
1206
1222
  ? amountPerBaseUnit * factor
@@ -1223,6 +1239,125 @@ export function LineItemDialog({
1223
1239
  [currencyCode, findTaxRateIdByValue, resolveTaxSelection, resolveUnitPriceFactor],
1224
1240
  );
1225
1241
 
1242
+ const commitVariantSelection = React.useCallback(
1243
+ ({
1244
+ variantId,
1245
+ productId,
1246
+ product,
1247
+ values,
1248
+ setFormValue,
1249
+ }: {
1250
+ variantId: string | null;
1251
+ productId: string | null;
1252
+ product: ProductOption | null;
1253
+ values: Record<string, unknown> | undefined;
1254
+ setFormValue: ((id: string, value: unknown) => void) | undefined;
1255
+ }) => {
1256
+ const selectedOption = variantId
1257
+ ? (variantOptionsRef.current.get(variantId) ?? null)
1258
+ : null;
1259
+ setVariantOption(selectedOption);
1260
+ setFormValue?.("variantId", variantId);
1261
+ const existingName = typeof values?.name === "string" ? values.name : "";
1262
+ if (!existingName.trim()) {
1263
+ setFormValue?.(
1264
+ "name",
1265
+ selectedOption?.title ?? product?.title ?? existingName,
1266
+ );
1267
+ }
1268
+ const taxSource = hasTaxMetadata(selectedOption)
1269
+ ? selectedOption
1270
+ : hasTaxMetadata(product)
1271
+ ? product
1272
+ : null;
1273
+ if (taxSource) {
1274
+ const taxSelection = resolveTaxSelection(taxSource);
1275
+ setFormValue?.("taxRate", taxSelection.taxRate ?? null);
1276
+ setFormValue?.("taxRateId", taxSelection.taxRateId ?? null);
1277
+ }
1278
+ const prevSnapshot =
1279
+ typeof values?.catalogSnapshot === "object" && values.catalogSnapshot
1280
+ ? (values.catalogSnapshot as Record<string, unknown>)
1281
+ : null;
1282
+ if (variantId) {
1283
+ setFormValue?.("catalogSnapshot", {
1284
+ ...(prevSnapshot ?? {}),
1285
+ variant: {
1286
+ id: variantId,
1287
+ title: selectedOption?.title ?? null,
1288
+ sku: selectedOption?.sku ?? null,
1289
+ thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
1290
+ },
1291
+ });
1292
+ } else if (prevSnapshot) {
1293
+ const snapshot = { ...prevSnapshot };
1294
+ if ("variant" in snapshot)
1295
+ delete (snapshot as CatalogSnapshotRecord).variant;
1296
+ setFormValue?.(
1297
+ "catalogSnapshot",
1298
+ Object.keys(snapshot).length ? snapshot : null,
1299
+ );
1300
+ } else {
1301
+ setFormValue?.("catalogSnapshot", null);
1302
+ }
1303
+ if (productId) {
1304
+ const currentQuantity =
1305
+ typeof values?.quantity === "string" ? values.quantity : 1;
1306
+ const currentQuantityUnit =
1307
+ typeof values?.quantityUnit === "string" ? values.quantityUnit : null;
1308
+ void loadPrices(
1309
+ productId,
1310
+ variantId,
1311
+ currentQuantity,
1312
+ currentQuantityUnit,
1313
+ );
1314
+ }
1315
+ },
1316
+ [hasTaxMetadata, loadPrices, resolveTaxSelection],
1317
+ );
1318
+
1319
+ const autoSelectSoleVariant = React.useCallback(
1320
+ async ({
1321
+ productId,
1322
+ product,
1323
+ values,
1324
+ setFormValue,
1325
+ }: {
1326
+ productId: string;
1327
+ product: ProductOption | null;
1328
+ values: Record<string, unknown>;
1329
+ setFormValue: ((id: string, value: unknown) => void) | undefined;
1330
+ }) => {
1331
+ let variants: LookupSelectItem[] = [];
1332
+ try {
1333
+ variants = await loadVariantOptions(
1334
+ productId,
1335
+ product?.thumbnailUrl ?? null,
1336
+ );
1337
+ } catch (err) {
1338
+ logger.error('sales.document.items.autoSelectVariant', { err });
1339
+ }
1340
+ if (selectedProductIdRef.current !== productId) return;
1341
+ if (variants.length === 1) {
1342
+ commitVariantSelection({
1343
+ variantId: variants[0].id,
1344
+ productId,
1345
+ product,
1346
+ values,
1347
+ setFormValue,
1348
+ });
1349
+ return;
1350
+ }
1351
+ void loadPrices(
1352
+ productId,
1353
+ null,
1354
+ typeof values.quantity === "string" ? values.quantity : 1,
1355
+ typeof values.quantityUnit === "string" ? values.quantityUnit : null,
1356
+ );
1357
+ },
1358
+ [commitVariantSelection, loadPrices, loadVariantOptions],
1359
+ );
1360
+
1226
1361
  const loadLineStatuses = React.useCallback(async (): Promise<
1227
1362
  StatusOption[]
1228
1363
  > => {
@@ -1671,6 +1806,7 @@ export function LineItemDialog({
1671
1806
  const switchMode = (next: "catalog" | "custom") => {
1672
1807
  if (next === mode) return;
1673
1808
  setValue(next);
1809
+ selectedProductIdRef.current = null;
1674
1810
  setLineMode(next);
1675
1811
  if (next === "custom") {
1676
1812
  setProductOption(null);
@@ -1795,18 +1931,17 @@ export function LineItemDialog({
1795
1931
  selectedOption?.defaultUnit ??
1796
1932
  null;
1797
1933
  setFormValue?.("quantityUnit", defaultQuantityUnit);
1798
- if (
1934
+ const defaultQuantity =
1799
1935
  typeof selectedOption?.defaultSalesUnitQuantity ===
1800
1936
  "number" &&
1801
1937
  Number.isFinite(
1802
1938
  selectedOption.defaultSalesUnitQuantity,
1803
1939
  ) &&
1804
1940
  selectedOption.defaultSalesUnitQuantity > 0
1805
- ) {
1806
- setFormValue?.(
1807
- "quantity",
1808
- String(selectedOption.defaultSalesUnitQuantity),
1809
- );
1941
+ ? String(selectedOption.defaultSalesUnitQuantity)
1942
+ : null;
1943
+ if (defaultQuantity) {
1944
+ setFormValue?.("quantity", defaultQuantity);
1810
1945
  }
1811
1946
  const taxSelection = selectedOption
1812
1947
  ? resolveTaxSelection(selectedOption)
@@ -1818,33 +1953,38 @@ export function LineItemDialog({
1818
1953
  if (!existingName.trim() && selectedOption?.title) {
1819
1954
  setFormValue?.("name", selectedOption.title);
1820
1955
  }
1821
- setFormValue?.(
1822
- "catalogSnapshot",
1823
- next
1824
- ? {
1825
- product: {
1826
- id: next,
1827
- title: selectedOption?.title ?? null,
1828
- sku: selectedOption?.sku ?? null,
1829
- thumbnailUrl:
1830
- selectedOption?.thumbnailUrl ?? null,
1831
- defaultUnit: selectedOption?.defaultUnit ?? null,
1832
- defaultSalesUnit:
1833
- selectedOption?.defaultSalesUnit ?? null,
1834
- },
1835
- }
1836
- : null,
1837
- );
1956
+ const productSnapshot = next
1957
+ ? {
1958
+ product: {
1959
+ id: next,
1960
+ title: selectedOption?.title ?? null,
1961
+ sku: selectedOption?.sku ?? null,
1962
+ thumbnailUrl:
1963
+ selectedOption?.thumbnailUrl ?? null,
1964
+ defaultUnit: selectedOption?.defaultUnit ?? null,
1965
+ defaultSalesUnit:
1966
+ selectedOption?.defaultSalesUnit ?? null,
1967
+ },
1968
+ }
1969
+ : null;
1970
+ setFormValue?.("catalogSnapshot", productSnapshot);
1971
+ selectedProductIdRef.current = next ?? null;
1838
1972
  if (next) {
1839
1973
  void loadProductUnits(next, selectedOption);
1840
- void loadPrices(
1841
- next,
1842
- null,
1843
- typeof values?.quantity === "string"
1844
- ? values.quantity
1845
- : 1,
1846
- defaultQuantityUnit,
1847
- );
1974
+ const valuesAfterProductChange = {
1975
+ ...values,
1976
+ name:
1977
+ existingName.trim() || selectedOption?.title || "",
1978
+ catalogSnapshot: productSnapshot,
1979
+ quantity: defaultQuantity ?? values?.quantity,
1980
+ quantityUnit: defaultQuantityUnit,
1981
+ };
1982
+ void autoSelectSoleVariant({
1983
+ productId: next,
1984
+ product: selectedOption,
1985
+ values: valuesAfterProductChange,
1986
+ setFormValue,
1987
+ });
1848
1988
  }
1849
1989
  }}
1850
1990
  fetchItems={loadProductOptions}
@@ -1921,76 +2061,14 @@ export function LineItemDialog({
1921
2061
  key={productId ?? "no-product"}
1922
2062
  value={typeof value === "string" ? value : null}
1923
2063
  onChange={(next) => {
1924
- const selectedOption = next
1925
- ? (variantOptionsRef.current.get(next) ?? null)
1926
- : null;
1927
- setVariantOption(selectedOption);
1928
2064
  setValue(next ?? null);
1929
- const existingName =
1930
- typeof values?.name === "string" ? values.name : "";
1931
- if (!existingName.trim()) {
1932
- setFormValue?.(
1933
- "name",
1934
- selectedOption?.title ??
1935
- productOption?.title ??
1936
- existingName,
1937
- );
1938
- }
1939
- const taxSource = hasTaxMetadata(selectedOption)
1940
- ? selectedOption
1941
- : hasTaxMetadata(productOption)
1942
- ? productOption
1943
- : null;
1944
- if (taxSource) {
1945
- const taxSelection = resolveTaxSelection(taxSource);
1946
- setFormValue?.("taxRate", taxSelection.taxRate ?? null);
1947
- setFormValue?.(
1948
- "taxRateId",
1949
- taxSelection.taxRateId ?? null,
1950
- );
1951
- }
1952
- const prevSnapshot =
1953
- typeof values?.catalogSnapshot === "object" &&
1954
- values.catalogSnapshot
1955
- ? (values.catalogSnapshot as Record<string, unknown>)
1956
- : null;
1957
- if (next) {
1958
- setFormValue?.("catalogSnapshot", {
1959
- ...(prevSnapshot ?? {}),
1960
- variant: {
1961
- id: next,
1962
- title: selectedOption?.title ?? null,
1963
- sku: selectedOption?.sku ?? null,
1964
- thumbnailUrl: selectedOption?.thumbnailUrl ?? null,
1965
- },
1966
- });
1967
- } else if (prevSnapshot) {
1968
- const snapshot = { ...prevSnapshot };
1969
- if ("variant" in snapshot)
1970
- delete (snapshot as CatalogSnapshotRecord).variant;
1971
- setFormValue?.(
1972
- "catalogSnapshot",
1973
- Object.keys(snapshot).length ? snapshot : null,
1974
- );
1975
- } else {
1976
- setFormValue?.("catalogSnapshot", null);
1977
- }
1978
- if (productId) {
1979
- const currentQuantity =
1980
- typeof values?.quantity === "string"
1981
- ? values.quantity
1982
- : 1;
1983
- const currentQuantityUnit =
1984
- typeof values?.quantityUnit === "string"
1985
- ? values.quantityUnit
1986
- : null;
1987
- void loadPrices(
1988
- productId,
1989
- next,
1990
- currentQuantity,
1991
- currentQuantityUnit,
1992
- );
1993
- }
2065
+ commitVariantSelection({
2066
+ variantId: next ?? null,
2067
+ productId,
2068
+ product: productOption,
2069
+ values,
2070
+ setFormValue,
2071
+ });
1994
2072
  }}
1995
2073
  fetchItems={async (query) => {
1996
2074
  if (!productId) return [];
@@ -2355,9 +2433,10 @@ export function LineItemDialog({
2355
2433
  event: React.ChangeEvent<HTMLSelectElement>,
2356
2434
  ) => {
2357
2435
  const nextId = event.target.value || null;
2358
- const option = nextId ? (taxRateMap.get(nextId) ?? null) : null;
2436
+ if (!nextId) return;
2437
+ const option = taxRateMap.get(nextId) ?? null;
2359
2438
  setValue(nextId);
2360
- const rate = normalizeNumber(option?.rate);
2439
+ const rate = normalizeNumber(option?.rate, Number.NaN);
2361
2440
  setFormValue?.("taxRate", Number.isFinite(rate) ? rate : null);
2362
2441
  };
2363
2442
  return (
@@ -2672,6 +2751,8 @@ export function LineItemDialog({
2672
2751
  ];
2673
2752
  }, [
2674
2753
  applyPriceSelection,
2754
+ autoSelectSoleVariant,
2755
+ commitVariantSelection,
2675
2756
  convertUnitPriceForUnitChange,
2676
2757
  currencyCode,
2677
2758
  findTaxRateIdByValue,
@@ -2716,6 +2797,7 @@ export function LineItemDialog({
2716
2797
  return;
2717
2798
  }
2718
2799
  let cancelled = false;
2800
+ selectedProductIdRef.current = null;
2719
2801
  setDeletedCatalogReference(false);
2720
2802
  setEditingId(initialLine.id);
2721
2803
  const nextForm = defaultForm(initialLine.currencyCode ?? currencyCode);
@@ -2778,10 +2860,11 @@ export function LineItemDialog({
2778
2860
  ? metaRec.taxRateId
2779
2861
  : null;
2780
2862
  const fallbackTaxRateId = findTaxRateIdByValue(nextForm.taxRate);
2781
- nextForm.taxRateId =
2782
- metaTaxRateId ??
2783
- fallbackTaxRateId ??
2784
- (defaultTaxRateRef.current ? defaultTaxRateRef.current.id : null);
2863
+ const defaultTaxRateId =
2864
+ !Number.isFinite(nextForm.taxRate) && defaultTaxRateRef.current
2865
+ ? defaultTaxRateRef.current.id
2866
+ : null;
2867
+ nextForm.taxRateId = metaTaxRateId ?? fallbackTaxRateId ?? defaultTaxRateId;
2785
2868
  if (!Number.isFinite(nextForm.taxRate) && nextForm.taxRateId) {
2786
2869
  const matched = taxRatesRef.current.find(
2787
2870
  (rate) => rate.id === nextForm.taxRateId,