@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
- package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
- package/dist/generated/entities/customer_interaction/index.js +2 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +9 -8
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -0
- package/src/modules/communication_channels/i18n/en.json +4 -0
- package/src/modules/communication_channels/i18n/es.json +4 -0
- package/src/modules/communication_channels/i18n/ko.json +4 -0
- package/src/modules/communication_channels/i18n/pl.json +4 -0
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- 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
|
|
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
|
|
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
|
|
100
|
-
const
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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() +
|
|
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
|
|
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 =
|
|
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,
|
|
191
|
-
} else if (!overrideDays.has(
|
|
192
|
-
overrideDays.set(dayKey,
|
|
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 =
|
|
204
|
-
.filter((window) =>
|
|
205
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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,
|