@open-mercato/core 0.6.4-develop.4106.1.12c205a613 → 0.6.4-develop.4113.1.5e87922616

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.
@@ -0,0 +1,8 @@
1
+ function resolveRuleSetSelectValue(ruleSets, selectedRulesetId) {
2
+ if (!selectedRulesetId) return void 0;
3
+ return ruleSets.some((ruleSet) => ruleSet.id === selectedRulesetId) ? selectedRulesetId : void 0;
4
+ }
5
+ export {
6
+ resolveRuleSetSelectValue
7
+ };
8
+ //# sourceMappingURL=availabilityRulesEditorState.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/planner/components/availabilityRulesEditorState.ts"],
4
+ "sourcesContent": ["export type AvailabilityRuleSetOption = {\n id: string\n}\n\nexport function resolveRuleSetSelectValue(\n ruleSets: AvailabilityRuleSetOption[],\n selectedRulesetId: string | null | undefined,\n): string | undefined {\n if (!selectedRulesetId) return undefined\n return ruleSets.some((ruleSet) => ruleSet.id === selectedRulesetId) ? selectedRulesetId : undefined\n}\n"],
5
+ "mappings": "AAIO,SAAS,0BACd,UACA,mBACoB;AACpB,MAAI,CAAC,kBAAmB,QAAO;AAC/B,SAAO,SAAS,KAAK,CAAC,YAAY,QAAQ,OAAO,iBAAiB,IAAI,oBAAoB;AAC5F;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.4-develop.4106.1.12c205a613",
3
+ "version": "0.6.4-develop.4113.1.5e87922616",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -243,16 +243,16 @@
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.4-develop.4106.1.12c205a613",
247
- "@open-mercato/shared": "0.6.4-develop.4106.1.12c205a613",
248
- "@open-mercato/ui": "0.6.4-develop.4106.1.12c205a613",
246
+ "@open-mercato/ai-assistant": "0.6.4-develop.4113.1.5e87922616",
247
+ "@open-mercato/shared": "0.6.4-develop.4113.1.5e87922616",
248
+ "@open-mercato/ui": "0.6.4-develop.4113.1.5e87922616",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.4-develop.4106.1.12c205a613",
254
- "@open-mercato/shared": "0.6.4-develop.4106.1.12c205a613",
255
- "@open-mercato/ui": "0.6.4-develop.4106.1.12c205a613",
253
+ "@open-mercato/ai-assistant": "0.6.4-develop.4113.1.5e87922616",
254
+ "@open-mercato/shared": "0.6.4-develop.4113.1.5e87922616",
255
+ "@open-mercato/ui": "0.6.4-develop.4113.1.5e87922616",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
@@ -13,6 +13,13 @@ export interface FetchOptions {
13
13
  forceUpdate?: boolean
14
14
  }
15
15
 
16
+ const exchangeRateKey = (
17
+ fromCurrencyCode: string,
18
+ toCurrencyCode: string,
19
+ date: Date,
20
+ source: string
21
+ ): string => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`
22
+
16
23
  export class RateFetchingService {
17
24
  private providers: Map<string, RateProvider>
18
25
 
@@ -103,25 +110,47 @@ export class RateFetchingService {
103
110
  rates: RateProviderResult[],
104
111
  scope: { tenantId: string; organizationId: string }
105
112
  ): Promise<number> {
113
+ if (rates.length === 0) return 0
114
+
106
115
  let stored = 0
107
116
 
108
117
  await this.em.transactional(async (em) => {
118
+ // Prefetch every existing rate that could match this batch in a single query,
119
+ // then index by composite key so the per-rate loop never hits the database.
120
+ const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)))
121
+ const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)))
122
+ const sources = Array.from(new Set(rates.map((rate) => rate.source)))
123
+ const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(
124
+ (time) => new Date(time)
125
+ )
126
+
127
+ const existingRates = await em.find(ExchangeRate, {
128
+ organizationId: scope.organizationId,
129
+ tenantId: scope.tenantId,
130
+ fromCurrencyCode: { $in: fromCurrencyCodes },
131
+ toCurrencyCode: { $in: toCurrencyCodes },
132
+ date: { $in: dates },
133
+ source: { $in: sources },
134
+ })
135
+
136
+ const existingByKey = new Map<string, ExchangeRate>()
137
+ for (const existing of existingRates) {
138
+ existingByKey.set(
139
+ exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),
140
+ existing
141
+ )
142
+ }
143
+
144
+ const now = new Date()
109
145
  for (const rate of rates) {
110
- // Check if rate already exists
111
- const existing = await em.findOne(ExchangeRate, {
112
- organizationId: scope.organizationId,
113
- tenantId: scope.tenantId,
114
- fromCurrencyCode: rate.fromCurrencyCode,
115
- toCurrencyCode: rate.toCurrencyCode,
116
- date: rate.date,
117
- source: rate.source,
118
- })
146
+ const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source)
147
+ const existing = existingByKey.get(key)
119
148
 
120
149
  if (existing) {
121
150
  // Update existing rate
122
151
  existing.rate = rate.rate
123
152
  existing.type = rate.type ?? null
124
- existing.updatedAt = new Date()
153
+ existing.updatedAt = now
125
154
  em.persist(existing)
126
155
  } else {
127
156
  // Create new rate
@@ -135,15 +164,17 @@ export class RateFetchingService {
135
164
  source: rate.source,
136
165
  type: rate.type ?? null,
137
166
  isActive: true,
138
- createdAt: new Date(),
139
- updatedAt: new Date(),
167
+ createdAt: now,
168
+ updatedAt: now,
140
169
  })
141
170
  em.persist(newRate)
171
+ // Track so duplicate keys within the same batch update in memory instead of double-inserting.
172
+ existingByKey.set(key, newRate)
142
173
  }
143
174
 
144
175
  stored++
145
176
  }
146
-
177
+
147
178
  // Flush all changes at once
148
179
  await em.flush()
149
180
  })
@@ -65,6 +65,20 @@ export async function POST(req: Request) {
65
65
 
66
66
  await em.begin()
67
67
  try {
68
+ // Prefetch every existing definition for this entity in a single query, then index
69
+ // by key so the per-definition loop resolves create/update without round trips.
70
+ const defByKey = new Map<string, any>()
71
+ const keys = definitions.map((d) => d.key)
72
+ if (keys.length > 0) {
73
+ const existingDefs = await em.find(CustomFieldDef, {
74
+ entityId,
75
+ key: { $in: keys },
76
+ organizationId: auth.orgId ?? null,
77
+ tenantId: auth.tenantId ?? null,
78
+ })
79
+ for (const existing of existingDefs) defByKey.set(existing.key, existing)
80
+ }
81
+
68
82
  for (const [idx, d] of definitions.entries()) {
69
83
  const where: any = {
70
84
  entityId,
@@ -72,8 +86,11 @@ export async function POST(req: Request) {
72
86
  organizationId: auth.orgId ?? null,
73
87
  tenantId: auth.tenantId ?? null,
74
88
  }
75
- let def = await em.findOne(CustomFieldDef, where)
76
- if (!def) def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })
89
+ let def = defByKey.get(d.key)
90
+ if (!def) {
91
+ def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })
92
+ defByKey.set(d.key, def)
93
+ }
77
94
  def.kind = d.kind
78
95
 
79
96
  const inCfg = (d as any).configJson ?? {}
@@ -73,15 +73,28 @@ export async function ensureCustomFieldDefinitions(
73
73
  let updated = 0
74
74
  let unchanged = 0
75
75
 
76
+ // Prefetch every existing definition the batch could touch in a single query,
77
+ // then index by composite key so the nested loop never issues per-field lookups.
78
+ const entityIds = Array.from(new Set(sets.map((set) => set.entity)))
79
+ const fieldKeys = Array.from(new Set(sets.flatMap((set) => set.fields.map((field) => field.key))))
80
+ const existingByKey = new Map<string, CustomFieldDef>()
81
+ if (entityIds.length > 0 && fieldKeys.length > 0) {
82
+ const existingDefs = await em.find(CustomFieldDef, {
83
+ entityId: { $in: entityIds },
84
+ organizationId: scope.organizationId,
85
+ tenantId: scope.tenantId,
86
+ key: { $in: fieldKeys },
87
+ })
88
+ for (const def of existingDefs) {
89
+ existingByKey.set(`${def.entityId}|${def.key}`, def)
90
+ }
91
+ }
92
+
93
+ let dirty = false
94
+
76
95
  for (const set of sets) {
77
96
  for (const field of set.fields) {
78
- const where = {
79
- entityId: set.entity,
80
- organizationId: scope.organizationId,
81
- tenantId: scope.tenantId,
82
- key: field.key,
83
- }
84
- const existing = await em.findOne(CustomFieldDef, where)
97
+ const existing = existingByKey.get(`${set.entity}|${field.key}`) ?? null
85
98
  const configJson: Record<string, unknown> = {}
86
99
 
87
100
  for (const key of CONFIG_PASSTHROUGH_KEYS) {
@@ -91,19 +104,21 @@ export async function ensureCustomFieldDefinitions(
91
104
 
92
105
  if (!existing) {
93
106
  if (!scope.dryRun) {
94
- await em.persist(
95
- em.create(CustomFieldDef, {
96
- entityId: set.entity,
97
- organizationId: scope.organizationId,
98
- tenantId: scope.tenantId,
99
- key: field.key,
100
- kind: field.kind,
101
- configJson,
102
- isActive: true,
103
- createdAt: new Date(),
104
- updatedAt: new Date(),
105
- })
106
- ).flush()
107
+ const createdDef = em.create(CustomFieldDef, {
108
+ entityId: set.entity,
109
+ organizationId: scope.organizationId,
110
+ tenantId: scope.tenantId,
111
+ key: field.key,
112
+ kind: field.kind,
113
+ configJson,
114
+ isActive: true,
115
+ createdAt: new Date(),
116
+ updatedAt: new Date(),
117
+ })
118
+ em.persist(createdDef)
119
+ // Track so duplicate (entity, key) pairs within the batch update in memory instead of double-inserting.
120
+ existingByKey.set(`${set.entity}|${field.key}`, createdDef)
121
+ dirty = true
107
122
  }
108
123
  created++
109
124
  continue
@@ -127,11 +142,17 @@ export async function ensureCustomFieldDefinitions(
127
142
  existing.isActive = true
128
143
  existing.updatedAt = new Date()
129
144
  if (existing.deletedAt) existing.deletedAt = null
130
- await em.flush()
145
+ em.persist(existing)
146
+ dirty = true
131
147
  }
132
148
  updated++
133
149
  }
134
150
  }
135
151
 
152
+ if (dirty) {
153
+ // Single flush for the whole batch instead of one round trip per field.
154
+ await em.flush()
155
+ }
156
+
136
157
  return { created, updated, unchanged }
137
158
  }
@@ -339,15 +339,23 @@ export async function saveRolePerspectives(
339
339
 
340
340
  const results: ResolvedRolePerspective[] = []
341
341
 
342
- for (const roleId of input.roleIds) {
343
- let record = await em.findOne(RolePerspective, {
344
- roleId,
342
+ // Prefetch every matching role perspective in a single query, then index by role id
343
+ // so the loop resolves create/update without a lookup per role.
344
+ const recordByRole = new Map<string, RolePerspective>()
345
+ if (input.roleIds.length) {
346
+ const existingRecords = await em.find(RolePerspective, {
347
+ roleId: { $in: input.roleIds },
345
348
  tableId,
346
349
  tenantId,
347
350
  organizationId,
348
351
  name: input.name,
349
352
  deletedAt: null,
350
353
  })
354
+ for (const existing of existingRecords) recordByRole.set(existing.roleId, existing)
355
+ }
356
+
357
+ for (const roleId of input.roleIds) {
358
+ let record = recordByRole.get(roleId) ?? null
351
359
  if (!record) {
352
360
  record = em.create(RolePerspective, {
353
361
  roleId,
@@ -361,6 +369,7 @@ export async function saveRolePerspectives(
361
369
  updatedAt: now,
362
370
  })
363
371
  em.persist(record)
372
+ recordByRole.set(roleId, record)
364
373
  } else {
365
374
  record.settingsJson = input.settings
366
375
  record.updatedAt = now
@@ -30,6 +30,7 @@ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
30
30
  import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
31
31
  import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
32
32
  import { Calendar, Clock, List, PencilLine, Plus, Trash2 } from 'lucide-react'
33
+ import { resolveRuleSetSelectValue } from './availabilityRulesEditorState'
33
34
 
34
35
  type AvailabilityRepeat = 'once' | 'daily' | 'weekly'
35
36
  type AvailabilitySubjectType = 'member' | 'resource' | 'ruleset'
@@ -398,6 +399,7 @@ export function AvailabilityRulesEditor({
398
399
  const [createRuleSetOpen, setCreateRuleSetOpen] = React.useState(false)
399
400
  const [isWeeklyAutoSaving, setIsWeeklyAutoSaving] = React.useState(false)
400
401
  const [customOverridesEnabled, setCustomOverridesEnabled] = React.useState(false)
402
+ const [selectedRulesetId, setSelectedRulesetId] = React.useState<string | null>(rulesetId ?? null)
401
403
  const autoSaveTimerRef = React.useRef<number | null>(null)
402
404
  const lastSavedWeeklyKeyRef = React.useRef<string | null>(null)
403
405
  const weeklySaveStateRef = React.useRef({ inFlight: false, queued: false })
@@ -409,7 +411,12 @@ export function AvailabilityRulesEditor({
409
411
  >(null)
410
412
  const timezoneOptions = React.useMemo(() => getTimezoneOptions(), [])
411
413
 
412
- const usingRuleSet = Boolean(rulesetId) && availabilityRules.length === 0 && !customOverridesEnabled
414
+ React.useEffect(() => {
415
+ setSelectedRulesetId(rulesetId ?? null)
416
+ }, [rulesetId])
417
+
418
+ const effectiveRulesetId = selectedRulesetId
419
+ const usingRuleSet = Boolean(effectiveRulesetId) && availabilityRules.length === 0 && !customOverridesEnabled
413
420
  const activeRules = usingRuleSet ? rulesetRules : availabilityRules
414
421
  const scheduleRules = React.useMemo(() => {
415
422
  const dateBlockers = new Set<string>()
@@ -547,7 +554,7 @@ export function AvailabilityRulesEditor({
547
554
  }, [labelPrefix, subjectId, subjectType, t])
548
555
 
549
556
  const refreshRuleSetRules = React.useCallback(async () => {
550
- if (!rulesetId) {
557
+ if (!effectiveRulesetId) {
551
558
  setRulesetRules([])
552
559
  return
553
560
  }
@@ -556,7 +563,7 @@ export function AvailabilityRulesEditor({
556
563
  page: '1',
557
564
  pageSize: '100',
558
565
  subjectType: 'ruleset',
559
- subjectIds: rulesetId,
566
+ subjectIds: effectiveRulesetId,
560
567
  })
561
568
  const call = await apiCall<{ items?: AvailabilityRule[] }>(`/api/planner/availability?${params.toString()}`)
562
569
  const items = Array.isArray(call.result?.items) ? call.result.items : []
@@ -564,7 +571,7 @@ export function AvailabilityRulesEditor({
564
571
  } catch {
565
572
  setRulesetRules([])
566
573
  }
567
- }, [rulesetId])
574
+ }, [effectiveRulesetId])
568
575
 
569
576
  const refreshRuleSets = React.useCallback(async () => {
570
577
  if (!onRulesetChange) return
@@ -653,10 +660,10 @@ export function AvailabilityRulesEditor({
653
660
 
654
661
  React.useEffect(() => {
655
662
  if (timezoneDirty) return
656
- if (!usingRuleSet || !rulesetId) return
657
- const ruleset = ruleSets.find((entry) => entry.id === rulesetId)
663
+ if (!usingRuleSet || !effectiveRulesetId) return
664
+ const ruleset = ruleSets.find((entry) => entry.id === effectiveRulesetId)
658
665
  if (ruleset?.timezone) setTimezone(ruleset.timezone)
659
- }, [rulesetId, ruleSets, timezoneDirty, usingRuleSet])
666
+ }, [effectiveRulesetId, ruleSets, timezoneDirty, usingRuleSet])
660
667
 
661
668
  React.useEffect(() => {
662
669
  if (availabilityRules.length > 0) {
@@ -665,10 +672,10 @@ export function AvailabilityRulesEditor({
665
672
  }, [availabilityRules.length])
666
673
 
667
674
  React.useEffect(() => {
668
- if (!rulesetId) {
675
+ if (!effectiveRulesetId) {
669
676
  setCustomOverridesEnabled(false)
670
677
  }
671
- }, [rulesetId])
678
+ }, [effectiveRulesetId])
672
679
 
673
680
  const refreshBookedEvents = React.useCallback(async () => {
674
681
  if (!loadBookedEvents) return
@@ -764,7 +771,7 @@ export function AvailabilityRulesEditor({
764
771
  const saveWeeklyHours = React.useCallback(async (options?: { silentSuccess?: boolean; skipRefresh?: boolean }) => {
765
772
  if (isReadOnly) return
766
773
  const subjectForRules: AvailabilitySubjectType = usingRuleSet ? 'ruleset' : subjectType
767
- const subjectIdForRules = usingRuleSet ? (rulesetId ?? '') : subjectId
774
+ const subjectIdForRules = usingRuleSet ? (effectiveRulesetId ?? '') : subjectId
768
775
  if (!subjectIdForRules) return
769
776
  if (weeklyHasErrors) return
770
777
 
@@ -802,7 +809,7 @@ export function AvailabilityRulesEditor({
802
809
  listLabels.saveWeeklySuccess,
803
810
  refreshAvailability,
804
811
  refreshRuleSetRules,
805
- rulesetId,
812
+ effectiveRulesetId,
806
813
  subjectId,
807
814
  subjectType,
808
815
  timezone,
@@ -856,7 +863,7 @@ export function AvailabilityRulesEditor({
856
863
  if (isReadOnly) return
857
864
  if (timezoneSaveInFlightRef.current) return
858
865
  const trimmedTimezone = nextTimezone.trim() || 'UTC'
859
- const rulesetTimezoneId = subjectType === 'ruleset' ? subjectId : rulesetId
866
+ const rulesetTimezoneId = subjectType === 'ruleset' ? subjectId : effectiveRulesetId
860
867
  const rulesToUpdate = activeRules.filter((rule) => rule.timezone !== trimmedTimezone)
861
868
  if (!rulesToUpdate.length && !rulesetTimezoneId) {
862
869
  setTimezoneDirty(false)
@@ -895,7 +902,7 @@ export function AvailabilityRulesEditor({
895
902
  listLabels.timezoneSaveError,
896
903
  refreshAvailability,
897
904
  refreshRuleSetRules,
898
- rulesetId,
905
+ effectiveRulesetId,
899
906
  subjectId,
900
907
  subjectType,
901
908
  isReadOnly,
@@ -927,7 +934,7 @@ export function AvailabilityRulesEditor({
927
934
 
928
935
  const handleCustomize = React.useCallback(async () => {
929
936
  if (isReadOnly) return
930
- if (!rulesetId) return
937
+ if (!effectiveRulesetId) return
931
938
  try {
932
939
  const creations = rulesetRules.map((rule) => createCrud('planner/availability', {
933
940
  subjectType,
@@ -945,11 +952,11 @@ export function AvailabilityRulesEditor({
945
952
  const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
946
953
  flash(message, 'error')
947
954
  }
948
- }, [listLabels.saveWeeklyError, refreshAvailability, rulesetId, rulesetRules, subjectId, subjectType, isReadOnly])
955
+ }, [effectiveRulesetId, listLabels.saveWeeklyError, refreshAvailability, rulesetRules, subjectId, subjectType, isReadOnly])
949
956
 
950
957
  const handleResetToRuleSet = React.useCallback(async () => {
951
958
  if (isReadOnly) return
952
- if (!rulesetId) return
959
+ if (!effectiveRulesetId) return
953
960
  try {
954
961
  await Promise.all(
955
962
  availabilityRules.map((rule) => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError })),
@@ -960,12 +967,12 @@ export function AvailabilityRulesEditor({
960
967
  const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
961
968
  flash(message, 'error')
962
969
  }
963
- }, [availabilityRules, listLabels.saveWeeklyError, refreshAvailability, rulesetId, isReadOnly])
970
+ }, [availabilityRules, effectiveRulesetId, listLabels.saveWeeklyError, refreshAvailability, isReadOnly])
964
971
 
965
972
  const handleRuleSetChange = React.useCallback(async (nextId: string | null) => {
966
973
  if (isReadOnly) return
967
974
  if (!onRulesetChange) return
968
- if (availabilityRules.length > 0 && nextId !== rulesetId) {
975
+ if (availabilityRules.length > 0 && nextId !== effectiveRulesetId) {
969
976
  const confirmed = await confirm({
970
977
  title: listLabels.ruleSetConfirm,
971
978
  variant: 'default',
@@ -975,17 +982,23 @@ export function AvailabilityRulesEditor({
975
982
  availabilityRules.map((rule) => deleteCrud('planner/availability', rule.id, { errorMessage: listLabels.saveWeeklyError })),
976
983
  )
977
984
  }
985
+ setSelectedRulesetId(nextId)
978
986
  setCustomOverridesEnabled(false)
979
- await onRulesetChange(nextId)
980
- await refreshAvailability()
987
+ try {
988
+ await onRulesetChange(nextId)
989
+ await refreshAvailability()
990
+ } catch (error) {
991
+ setSelectedRulesetId(effectiveRulesetId)
992
+ throw error
993
+ }
981
994
  }, [
982
995
  availabilityRules,
983
996
  confirm,
997
+ effectiveRulesetId,
984
998
  listLabels.ruleSetConfirm,
985
999
  listLabels.saveWeeklyError,
986
1000
  onRulesetChange,
987
1001
  refreshAvailability,
988
- rulesetId,
989
1002
  isReadOnly,
990
1003
  ])
991
1004
 
@@ -1058,6 +1071,7 @@ export function AvailabilityRulesEditor({
1058
1071
  }
1059
1072
  await refreshRuleSets()
1060
1073
  if (onRulesetChange) {
1074
+ setSelectedRulesetId(id)
1061
1075
  await onRulesetChange(id)
1062
1076
  await refreshAvailability()
1063
1077
  }
@@ -1155,7 +1169,7 @@ export function AvailabilityRulesEditor({
1155
1169
  if (isReadOnly) return
1156
1170
  if (editorUnavailable && !canManageUnavailability) return
1157
1171
  const subjectForRules: AvailabilitySubjectType = usingRuleSet ? 'ruleset' : subjectType
1158
- const subjectIdForRules = usingRuleSet ? (rulesetId ?? '') : subjectId
1172
+ const subjectIdForRules = usingRuleSet ? (effectiveRulesetId ?? '') : subjectId
1159
1173
  if (!subjectIdForRules) return
1160
1174
  if (!editorUnavailable && editorWindowErrors.some(Boolean)) return
1161
1175
  const validWindows = editorWindows
@@ -1235,7 +1249,7 @@ export function AvailabilityRulesEditor({
1235
1249
  refreshAvailability,
1236
1250
  refreshRuleSetRules,
1237
1251
  reasonEntriesById,
1238
- rulesetId,
1252
+ effectiveRulesetId,
1239
1253
  subjectId,
1240
1254
  subjectType,
1241
1255
  timezone,
@@ -1316,7 +1330,7 @@ export function AvailabilityRulesEditor({
1316
1330
  <span className="text-xs text-muted-foreground">{listLabels.ruleSetLoading}</span>
1317
1331
  ) : (
1318
1332
  <Select
1319
- value={rulesetId || undefined}
1333
+ value={resolveRuleSetSelectValue(ruleSets, effectiveRulesetId)}
1320
1334
  onValueChange={(value) => {
1321
1335
  void handleRuleSetChange(value ? value : null)
1322
1336
  }}
@@ -1338,12 +1352,12 @@ export function AvailabilityRulesEditor({
1338
1352
  <Plus className="size-4 mr-2" aria-hidden />
1339
1353
  {listLabels.ruleSetCreateLabel}
1340
1354
  </Button>
1341
- {rulesetId && usingRuleSet ? (
1355
+ {effectiveRulesetId && usingRuleSet ? (
1342
1356
  <Button type="button" variant="outline" size="sm" onClick={handleCustomize} disabled={isReadOnly}>
1343
1357
  {listLabels.ruleSetCustomize}
1344
1358
  </Button>
1345
1359
  ) : null}
1346
- {rulesetId && !usingRuleSet ? (
1360
+ {effectiveRulesetId && !usingRuleSet ? (
1347
1361
  <Button type="button" variant="ghost" size="sm" onClick={handleResetToRuleSet} disabled={isReadOnly}>
1348
1362
  {listLabels.ruleSetReset}
1349
1363
  </Button>
@@ -0,0 +1,11 @@
1
+ export type AvailabilityRuleSetOption = {
2
+ id: string
3
+ }
4
+
5
+ export function resolveRuleSetSelectValue(
6
+ ruleSets: AvailabilityRuleSetOption[],
7
+ selectedRulesetId: string | null | undefined,
8
+ ): string | undefined {
9
+ if (!selectedRulesetId) return undefined
10
+ return ruleSets.some((ruleSet) => ruleSet.id === selectedRulesetId) ? selectedRulesetId : undefined
11
+ }