@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/currencies/services/rateFetchingService.js +30 -11
- package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +16 -2
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/lib/field-definitions.js +35 -21
- package/dist/modules/entities/lib/field-definitions.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +9 -3
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +39 -26
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/planner/components/availabilityRulesEditorState.js +8 -0
- package/dist/modules/planner/components/availabilityRulesEditorState.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/currencies/services/rateFetchingService.ts +44 -13
- package/src/modules/entities/api/definitions.batch.ts +19 -2
- package/src/modules/entities/lib/field-definitions.ts +42 -21
- package/src/modules/perspectives/services/perspectiveService.ts +12 -3
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +40 -26
- package/src/modules/planner/components/availabilityRulesEditorState.ts +11 -0
|
@@ -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.
|
|
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.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
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.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
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
|
-
|
|
111
|
-
const existing =
|
|
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 =
|
|
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:
|
|
139
|
-
updatedAt:
|
|
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 =
|
|
76
|
-
if (!def)
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
).
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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 (!
|
|
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:
|
|
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
|
-
}, [
|
|
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 || !
|
|
657
|
-
const ruleset = ruleSets.find((entry) => entry.id ===
|
|
663
|
+
if (!usingRuleSet || !effectiveRulesetId) return
|
|
664
|
+
const ruleset = ruleSets.find((entry) => entry.id === effectiveRulesetId)
|
|
658
665
|
if (ruleset?.timezone) setTimezone(ruleset.timezone)
|
|
659
|
-
}, [
|
|
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 (!
|
|
675
|
+
if (!effectiveRulesetId) {
|
|
669
676
|
setCustomOverridesEnabled(false)
|
|
670
677
|
}
|
|
671
|
-
}, [
|
|
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 ? (
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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 (!
|
|
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,
|
|
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 (!
|
|
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,
|
|
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 !==
|
|
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
|
-
|
|
980
|
-
|
|
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 ? (
|
|
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
|
-
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
+
}
|