@open-mercato/core 0.6.6-develop.6296.1.5e8bdfee17 → 0.6.6-develop.6300.1.08ff40c897
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/AGENTS.md +8 -0
- package/dist/generated/entities/module_config/index.js +4 -0
- package/dist/generated/entities/module_config/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/auth/lib/setup-app.js +1 -1
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/configs/data/entities.js +15 -1
- package/dist/modules/configs/data/entities.js.map +2 -2
- package/dist/modules/configs/lib/module-config-service.js +52 -19
- package/dist/modules/configs/lib/module-config-service.js.map +3 -3
- package/dist/modules/configs/migrations/Migration20260617150000.js +21 -0
- package/dist/modules/configs/migrations/Migration20260617150000.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -22
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +8 -7
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +6 -5
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/generated/entities/module_config/index.ts +2 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/setup-app.ts +1 -1
- package/src/modules/configs/data/entities.ts +17 -1
- package/src/modules/configs/lib/module-config-service.ts +83 -24
- package/src/modules/configs/migrations/.snapshot-open-mercato.json +41 -4
- package/src/modules/configs/migrations/.snapshot-openmercato.json +41 -4
- package/src/modules/configs/migrations/Migration20260617150000.ts +21 -0
- package/src/modules/customers/api/activities/route.ts +6 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +29 -22
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +8 -7
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +6 -5
- package/dist/modules/customers/components/detail/CompanyDashboardTab.js +0 -133
- package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +0 -7
- package/src/modules/customers/components/detail/CompanyDashboardTab.tsx +0 -161
|
@@ -4,7 +4,7 @@ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
4
4
|
import { ModuleConfig } from '../data/entities'
|
|
5
5
|
import { moduleConfigKeySchema } from '../data/validators'
|
|
6
6
|
|
|
7
|
-
const CACHE_VERSION = '
|
|
7
|
+
const CACHE_VERSION = 'v2'
|
|
8
8
|
const CACHE_TTL_MS = 60_000
|
|
9
9
|
|
|
10
10
|
type CachePayload = {
|
|
@@ -12,7 +12,10 @@ type CachePayload = {
|
|
|
12
12
|
record?: ModuleConfigRecord | null
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const scopeKeyPart = (tenantId?: string | null) => (tenantId ?? 'global')
|
|
16
|
+
|
|
17
|
+
const cacheKey = (moduleId: string, name: string, tenantId?: string | null) =>
|
|
18
|
+
`module-config:${CACHE_VERSION}:${moduleId}:${name}:${scopeKeyPart(tenantId)}`
|
|
16
19
|
const moduleTag = (moduleId: string) => `module-config:module:${moduleId}`
|
|
17
20
|
|
|
18
21
|
const resolveEm = (container: AppContainer): EntityManager | null => {
|
|
@@ -31,10 +34,13 @@ const resolveCache = (container: AppContainer): CacheStrategy | null => {
|
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
const toRecord = (entity: ModuleConfig): ModuleConfigRecord => ({
|
|
37
|
+
const toRecord = (entity: ModuleConfig, source: ModuleConfigSource): ModuleConfigRecord => ({
|
|
35
38
|
moduleId: entity.moduleId,
|
|
36
39
|
name: entity.name,
|
|
37
40
|
value: entity.valueJson ?? null,
|
|
41
|
+
tenantId: entity.tenantId ?? null,
|
|
42
|
+
organizationId: entity.organizationId ?? null,
|
|
43
|
+
source,
|
|
38
44
|
createdAt: entity.createdAt.toISOString(),
|
|
39
45
|
updatedAt: entity.updatedAt.toISOString(),
|
|
40
46
|
})
|
|
@@ -73,10 +79,20 @@ const deleteCacheByModule = async (cache: CacheStrategy | null, moduleId: string
|
|
|
73
79
|
|
|
74
80
|
const normalizeKey = (moduleId: string, name: string) => moduleConfigKeySchema.parse({ moduleId, name })
|
|
75
81
|
|
|
82
|
+
export type ModuleConfigSource = 'tenant' | 'instance'
|
|
83
|
+
|
|
84
|
+
export type ConfigScope = {
|
|
85
|
+
tenantId?: string | null
|
|
86
|
+
organizationId?: string | null
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
export type ModuleConfigRecord = {
|
|
77
90
|
moduleId: string
|
|
78
91
|
name: string
|
|
79
92
|
value: unknown
|
|
93
|
+
tenantId: string | null
|
|
94
|
+
organizationId: string | null
|
|
95
|
+
source: ModuleConfigSource
|
|
80
96
|
createdAt: string
|
|
81
97
|
updatedAt: string
|
|
82
98
|
}
|
|
@@ -88,18 +104,23 @@ export type ModuleConfigDefault = {
|
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
export type ModuleConfigService = {
|
|
91
|
-
getRecord(moduleId: string, name: string): Promise<ModuleConfigRecord | null>
|
|
92
|
-
getValue<T = unknown>(
|
|
93
|
-
|
|
107
|
+
getRecord(moduleId: string, name: string, scope?: ConfigScope): Promise<ModuleConfigRecord | null>
|
|
108
|
+
getValue<T = unknown>(
|
|
109
|
+
moduleId: string,
|
|
110
|
+
name: string,
|
|
111
|
+
options?: { defaultValue?: T | null; scope?: ConfigScope },
|
|
112
|
+
): Promise<T | null>
|
|
113
|
+
setValue(moduleId: string, name: string, value: unknown, scope?: ConfigScope): Promise<ModuleConfigRecord | null>
|
|
94
114
|
restoreDefaults(defaults: ModuleConfigDefault[], options?: { force?: boolean }): Promise<void>
|
|
95
|
-
invalidate(moduleId: string, name?: string): Promise<void>
|
|
115
|
+
invalidate(moduleId: string, name?: string, scope?: ConfigScope): Promise<void>
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
export function createModuleConfigService(container: AppContainer): ModuleConfigService {
|
|
99
|
-
const getRecord = async (rawModuleId: string, rawName: string): Promise<ModuleConfigRecord | null> => {
|
|
119
|
+
const getRecord = async (rawModuleId: string, rawName: string, scope?: ConfigScope): Promise<ModuleConfigRecord | null> => {
|
|
100
120
|
const { moduleId, name } = normalizeKey(rawModuleId, rawName)
|
|
121
|
+
const tenantId = scope?.tenantId ?? null
|
|
101
122
|
const cache = resolveCache(container)
|
|
102
|
-
const key = cacheKey(moduleId, name)
|
|
123
|
+
const key = cacheKey(moduleId, name, tenantId)
|
|
103
124
|
const cached = await readCache(cache, key)
|
|
104
125
|
if (cached) {
|
|
105
126
|
if (!cached.found) return null
|
|
@@ -109,47 +130,84 @@ export function createModuleConfigService(container: AppContainer): ModuleConfig
|
|
|
109
130
|
const em = resolveEm(container)
|
|
110
131
|
if (!em) return null
|
|
111
132
|
const repo = em.getRepository(ModuleConfig)
|
|
112
|
-
|
|
113
|
-
if (
|
|
133
|
+
|
|
134
|
+
if (tenantId) {
|
|
135
|
+
const scoped = await repo.findOne({ moduleId, name, tenantId })
|
|
136
|
+
if (scoped) {
|
|
137
|
+
const record = toRecord(scoped, 'tenant')
|
|
138
|
+
await writeCache(cache, key, { found: true, record }, moduleId)
|
|
139
|
+
return record
|
|
140
|
+
}
|
|
141
|
+
const global = await repo.findOne({ moduleId, name, tenantId: null })
|
|
142
|
+
if (!global) {
|
|
143
|
+
await writeCache(cache, key, { found: false }, moduleId)
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
const record = toRecord(global, 'instance')
|
|
147
|
+
await writeCache(cache, key, { found: true, record }, moduleId)
|
|
148
|
+
return record
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const global = await repo.findOne({ moduleId, name, tenantId: null })
|
|
152
|
+
if (!global) {
|
|
114
153
|
await writeCache(cache, key, { found: false }, moduleId)
|
|
115
154
|
return null
|
|
116
155
|
}
|
|
117
|
-
const record = toRecord(
|
|
156
|
+
const record = toRecord(global, 'instance')
|
|
118
157
|
await writeCache(cache, key, { found: true, record }, moduleId)
|
|
119
158
|
return record
|
|
120
159
|
}
|
|
121
160
|
|
|
122
|
-
const getValue = async <T>(
|
|
123
|
-
|
|
161
|
+
const getValue = async <T>(
|
|
162
|
+
rawModuleId: string,
|
|
163
|
+
rawName: string,
|
|
164
|
+
options?: { defaultValue?: T | null; scope?: ConfigScope },
|
|
165
|
+
): Promise<T | null> => {
|
|
166
|
+
const record = await getRecord(rawModuleId, rawName, options?.scope)
|
|
124
167
|
if (!record) return options?.defaultValue ?? null
|
|
125
168
|
const value = record.value as T | null | undefined
|
|
126
169
|
if (value === undefined || value === null) return options?.defaultValue ?? null
|
|
127
170
|
return value
|
|
128
171
|
}
|
|
129
172
|
|
|
130
|
-
const setValue = async (
|
|
173
|
+
const setValue = async (
|
|
174
|
+
rawModuleId: string,
|
|
175
|
+
rawName: string,
|
|
176
|
+
value: unknown,
|
|
177
|
+
scope?: ConfigScope,
|
|
178
|
+
): Promise<ModuleConfigRecord | null> => {
|
|
131
179
|
const em = resolveEm(container)
|
|
132
180
|
if (!em) return null
|
|
133
181
|
const { moduleId, name } = normalizeKey(rawModuleId, rawName)
|
|
182
|
+
const tenantId = scope?.tenantId ?? null
|
|
183
|
+
const organizationId = scope?.organizationId ?? null
|
|
134
184
|
const repo = em.getRepository(ModuleConfig)
|
|
135
185
|
const now = new Date()
|
|
136
|
-
let entity = await repo.findOne({ moduleId, name })
|
|
186
|
+
let entity = await repo.findOne({ moduleId, name, tenantId })
|
|
137
187
|
if (!entity) {
|
|
138
188
|
entity = repo.create({
|
|
139
189
|
moduleId,
|
|
140
190
|
name,
|
|
141
191
|
valueJson: value ?? null,
|
|
192
|
+
tenantId,
|
|
193
|
+
organizationId,
|
|
142
194
|
createdAt: now,
|
|
143
195
|
updatedAt: now,
|
|
144
196
|
})
|
|
145
197
|
em.persist(entity)
|
|
146
198
|
} else {
|
|
147
199
|
entity.valueJson = value ?? null
|
|
200
|
+
entity.organizationId = organizationId
|
|
148
201
|
entity.updatedAt = now
|
|
149
202
|
}
|
|
150
203
|
await em.flush()
|
|
151
|
-
const record = toRecord(entity)
|
|
152
|
-
|
|
204
|
+
const record = toRecord(entity, tenantId ? 'tenant' : 'instance')
|
|
205
|
+
const cache = resolveCache(container)
|
|
206
|
+
if (tenantId) {
|
|
207
|
+
await writeCache(cache, cacheKey(moduleId, name, tenantId), { found: true, record }, moduleId)
|
|
208
|
+
} else {
|
|
209
|
+
await deleteCacheByModule(cache, moduleId)
|
|
210
|
+
}
|
|
153
211
|
return record
|
|
154
212
|
}
|
|
155
213
|
|
|
@@ -165,12 +223,14 @@ export function createModuleConfigService(container: AppContainer): ModuleConfig
|
|
|
165
223
|
let entity: ModuleConfig | null = null
|
|
166
224
|
try {
|
|
167
225
|
const { moduleId, name } = normalizeKey(entry.moduleId, entry.name)
|
|
168
|
-
entity = await repo.findOne({ moduleId, name })
|
|
226
|
+
entity = await repo.findOne({ moduleId, name, tenantId: null })
|
|
169
227
|
if (!entity) {
|
|
170
228
|
entity = repo.create({
|
|
171
229
|
moduleId,
|
|
172
230
|
name,
|
|
173
231
|
valueJson: entry.value ?? null,
|
|
232
|
+
tenantId: null,
|
|
233
|
+
organizationId: null,
|
|
174
234
|
})
|
|
175
235
|
em.persist(entity)
|
|
176
236
|
dirty = true
|
|
@@ -186,17 +246,17 @@ export function createModuleConfigService(container: AppContainer): ModuleConfig
|
|
|
186
246
|
if (!dirty) return
|
|
187
247
|
await em.flush()
|
|
188
248
|
for (const entity of touched) {
|
|
189
|
-
const record = toRecord(entity)
|
|
190
|
-
await writeCache(cache, cacheKey(entity.moduleId, entity.name), { found: true, record }, entity.moduleId)
|
|
249
|
+
const record = toRecord(entity, 'instance')
|
|
250
|
+
await writeCache(cache, cacheKey(entity.moduleId, entity.name, null), { found: true, record }, entity.moduleId)
|
|
191
251
|
}
|
|
192
252
|
}
|
|
193
253
|
|
|
194
|
-
const invalidate = async (rawModuleId: string, rawName?: string) => {
|
|
254
|
+
const invalidate = async (rawModuleId: string, rawName?: string, scope?: ConfigScope) => {
|
|
195
255
|
const cache = resolveCache(container)
|
|
196
256
|
if (!cache) return
|
|
197
257
|
if (rawName) {
|
|
198
258
|
const { moduleId, name } = normalizeKey(rawModuleId, rawName)
|
|
199
|
-
await deleteCacheKey(cache, cacheKey(moduleId, name))
|
|
259
|
+
await deleteCacheKey(cache, cacheKey(moduleId, name, scope?.tenantId ?? null))
|
|
200
260
|
return
|
|
201
261
|
}
|
|
202
262
|
const moduleId = moduleConfigKeySchema.shape.moduleId.parse(rawModuleId)
|
|
@@ -211,4 +271,3 @@ export function createModuleConfigService(container: AppContainer): ModuleConfig
|
|
|
211
271
|
invalidate,
|
|
212
272
|
}
|
|
213
273
|
}
|
|
214
|
-
|
|
@@ -43,6 +43,24 @@
|
|
|
43
43
|
"nullable": true,
|
|
44
44
|
"mappedType": "json"
|
|
45
45
|
},
|
|
46
|
+
"organization_id": {
|
|
47
|
+
"name": "organization_id",
|
|
48
|
+
"type": "uuid",
|
|
49
|
+
"unsigned": false,
|
|
50
|
+
"autoincrement": false,
|
|
51
|
+
"primary": false,
|
|
52
|
+
"nullable": true,
|
|
53
|
+
"mappedType": "uuid"
|
|
54
|
+
},
|
|
55
|
+
"tenant_id": {
|
|
56
|
+
"name": "tenant_id",
|
|
57
|
+
"type": "uuid",
|
|
58
|
+
"unsigned": false,
|
|
59
|
+
"autoincrement": false,
|
|
60
|
+
"primary": false,
|
|
61
|
+
"nullable": true,
|
|
62
|
+
"mappedType": "uuid"
|
|
63
|
+
},
|
|
46
64
|
"created_at": {
|
|
47
65
|
"name": "created_at",
|
|
48
66
|
"type": "timestamptz",
|
|
@@ -68,15 +86,34 @@
|
|
|
68
86
|
"schema": "public",
|
|
69
87
|
"indexes": [
|
|
70
88
|
{
|
|
71
|
-
"keyName": "
|
|
89
|
+
"keyName": "module_configs_global_unique",
|
|
90
|
+
"columnNames": [],
|
|
91
|
+
"composite": false,
|
|
92
|
+
"constraint": false,
|
|
93
|
+
"primary": false,
|
|
94
|
+
"unique": false,
|
|
95
|
+
"expression": "create unique index \"module_configs_global_unique\" on \"module_configs\" (\"module_id\", \"name\") where \"tenant_id\" is null"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"keyName": "module_configs_scoped_unique",
|
|
99
|
+
"columnNames": [],
|
|
100
|
+
"composite": false,
|
|
101
|
+
"constraint": false,
|
|
102
|
+
"primary": false,
|
|
103
|
+
"unique": false,
|
|
104
|
+
"expression": "create unique index \"module_configs_scoped_unique\" on \"module_configs\" (\"module_id\", \"name\", \"tenant_id\") where \"tenant_id\" is not null"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"keyName": "module_configs_module_name_tenant_idx",
|
|
72
108
|
"columnNames": [
|
|
73
109
|
"module_id",
|
|
74
|
-
"name"
|
|
110
|
+
"name",
|
|
111
|
+
"tenant_id"
|
|
75
112
|
],
|
|
76
113
|
"composite": true,
|
|
77
|
-
"constraint":
|
|
114
|
+
"constraint": false,
|
|
78
115
|
"primary": false,
|
|
79
|
-
"unique":
|
|
116
|
+
"unique": false
|
|
80
117
|
},
|
|
81
118
|
{
|
|
82
119
|
"keyName": "module_configs_pkey",
|
|
@@ -43,6 +43,24 @@
|
|
|
43
43
|
"nullable": true,
|
|
44
44
|
"mappedType": "json"
|
|
45
45
|
},
|
|
46
|
+
"organization_id": {
|
|
47
|
+
"name": "organization_id",
|
|
48
|
+
"type": "uuid",
|
|
49
|
+
"unsigned": false,
|
|
50
|
+
"autoincrement": false,
|
|
51
|
+
"primary": false,
|
|
52
|
+
"nullable": true,
|
|
53
|
+
"mappedType": "uuid"
|
|
54
|
+
},
|
|
55
|
+
"tenant_id": {
|
|
56
|
+
"name": "tenant_id",
|
|
57
|
+
"type": "uuid",
|
|
58
|
+
"unsigned": false,
|
|
59
|
+
"autoincrement": false,
|
|
60
|
+
"primary": false,
|
|
61
|
+
"nullable": true,
|
|
62
|
+
"mappedType": "uuid"
|
|
63
|
+
},
|
|
46
64
|
"created_at": {
|
|
47
65
|
"name": "created_at",
|
|
48
66
|
"type": "timestamptz",
|
|
@@ -68,15 +86,34 @@
|
|
|
68
86
|
"schema": "public",
|
|
69
87
|
"indexes": [
|
|
70
88
|
{
|
|
71
|
-
"keyName": "
|
|
89
|
+
"keyName": "module_configs_global_unique",
|
|
90
|
+
"columnNames": [],
|
|
91
|
+
"composite": false,
|
|
92
|
+
"constraint": false,
|
|
93
|
+
"primary": false,
|
|
94
|
+
"unique": false,
|
|
95
|
+
"expression": "create unique index \"module_configs_global_unique\" on \"module_configs\" (\"module_id\", \"name\") where \"tenant_id\" is null"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"keyName": "module_configs_scoped_unique",
|
|
99
|
+
"columnNames": [],
|
|
100
|
+
"composite": false,
|
|
101
|
+
"constraint": false,
|
|
102
|
+
"primary": false,
|
|
103
|
+
"unique": false,
|
|
104
|
+
"expression": "create unique index \"module_configs_scoped_unique\" on \"module_configs\" (\"module_id\", \"name\", \"tenant_id\") where \"tenant_id\" is not null"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"keyName": "module_configs_module_name_tenant_idx",
|
|
72
108
|
"columnNames": [
|
|
73
109
|
"module_id",
|
|
74
|
-
"name"
|
|
110
|
+
"name",
|
|
111
|
+
"tenant_id"
|
|
75
112
|
],
|
|
76
113
|
"composite": true,
|
|
77
|
-
"constraint":
|
|
114
|
+
"constraint": false,
|
|
78
115
|
"primary": false,
|
|
79
|
-
"unique":
|
|
116
|
+
"unique": false
|
|
80
117
|
},
|
|
81
118
|
{
|
|
82
119
|
"keyName": "module_configs_pkey",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260617150000 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table "module_configs" add column "organization_id" uuid null, add column "tenant_id" uuid null;`);
|
|
7
|
+
this.addSql(`alter table "module_configs" drop constraint "module_configs_module_name_unique";`);
|
|
8
|
+
this.addSql(`create unique index "module_configs_global_unique" on "module_configs" ("module_id", "name") where "tenant_id" is null;`);
|
|
9
|
+
this.addSql(`create unique index "module_configs_scoped_unique" on "module_configs" ("module_id", "name", "tenant_id") where "tenant_id" is not null;`);
|
|
10
|
+
this.addSql(`create index "module_configs_module_name_tenant_idx" on "module_configs" ("module_id", "name", "tenant_id");`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async down(): Promise<void> {
|
|
14
|
+
this.addSql(`drop index if exists "module_configs_module_name_tenant_idx";`);
|
|
15
|
+
this.addSql(`drop index if exists "module_configs_scoped_unique";`);
|
|
16
|
+
this.addSql(`drop index if exists "module_configs_global_unique";`);
|
|
17
|
+
this.addSql(`alter table "module_configs" add constraint "module_configs_module_name_unique" unique ("module_id", "name");`);
|
|
18
|
+
this.addSql(`alter table "module_configs" drop column "organization_id", drop column "tenant_id";`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -74,6 +74,12 @@ const ADAPTER_HEADERS = {
|
|
|
74
74
|
// canonical bridge) read path. Keeps memory bounded on tenants with large
|
|
75
75
|
// activity history; deep-pagination beyond this window is not supported here —
|
|
76
76
|
// use /api/customers/interactions instead.
|
|
77
|
+
//
|
|
78
|
+
// Audited for the #3386 rollout (P2): this merged path sorts only on
|
|
79
|
+
// non-encrypted system timestamps (createdAt/occurredAt, see
|
|
80
|
+
// resolveActivitySortValue), so the #3278 two-phase *encrypted*-sort migration
|
|
81
|
+
// is intentionally not applied here. Merge-order correctness across the page
|
|
82
|
+
// boundary is covered by __tests__/merged-pagination.test.ts.
|
|
77
83
|
const MERGED_ACTIVITY_FETCH_CAP = 2000
|
|
78
84
|
|
|
79
85
|
type ActivityItem = {
|
|
@@ -83,6 +83,11 @@ import { BulkActionsBar } from './components/BulkActionsBar'
|
|
|
83
83
|
import { ChangeStageDialog } from './components/ChangeStageDialog'
|
|
84
84
|
import { ChangeOwnerDialog } from './components/ChangeOwnerDialog'
|
|
85
85
|
import { buildCrudExportUrl, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
86
|
+
import {
|
|
87
|
+
readVersionedPreference,
|
|
88
|
+
writeVersionedPreference,
|
|
89
|
+
clearVersionedPreference,
|
|
90
|
+
} from '@open-mercato/shared/lib/browser/versionedPreference'
|
|
86
91
|
import { runBulkDelete, groupBulkDeleteFailures } from '@open-mercato/ui/backend/utils/bulkDelete'
|
|
87
92
|
import {
|
|
88
93
|
fetchAssignableStaffMembers,
|
|
@@ -170,35 +175,37 @@ const MIN_LANE_WIDTH = 240
|
|
|
170
175
|
const MAX_LANE_WIDTH = 576
|
|
171
176
|
const LANE_GAP = 14
|
|
172
177
|
const LANE_WIDTHS_STORAGE_KEY_PREFIX = 'kanban-lane-widths-v2'
|
|
178
|
+
const LANE_WIDTHS_STORAGE_VERSION = 1
|
|
179
|
+
|
|
180
|
+
function isNumberRecord(value: unknown): value is Record<string, number> {
|
|
181
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
182
|
+
&& Object.values(value as Record<string, unknown>).every((v) => typeof v === 'number')
|
|
183
|
+
}
|
|
173
184
|
|
|
174
185
|
function loadLaneWidths(scopeKey: string): Record<string, number> {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
const raw = readVersionedPreference<Record<string, number>>(
|
|
187
|
+
`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`,
|
|
188
|
+
LANE_WIDTHS_STORAGE_VERSION,
|
|
189
|
+
isNumberRecord,
|
|
190
|
+
{},
|
|
191
|
+
{ legacyIsValid: isNumberRecord },
|
|
192
|
+
)
|
|
193
|
+
const out: Record<string, number> = {}
|
|
194
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
195
|
+
if (Number.isFinite(v) && v >= MIN_LANE_WIDTH && v <= MAX_LANE_WIDTH) {
|
|
196
|
+
out[k] = v
|
|
186
197
|
}
|
|
187
|
-
return out
|
|
188
|
-
} catch {
|
|
189
|
-
return {}
|
|
190
198
|
}
|
|
199
|
+
return out
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
function saveLaneWidths(scopeKey: string, widths: Record<string, number>) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
} catch {}
|
|
203
|
+
const key = `${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`
|
|
204
|
+
if (Object.keys(widths).length === 0) {
|
|
205
|
+
clearVersionedPreference(key)
|
|
206
|
+
} else {
|
|
207
|
+
writeVersionedPreference(key, LANE_WIDTHS_STORAGE_VERSION, widths)
|
|
208
|
+
}
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
function normalizeAmount(value: unknown): number | null {
|
|
@@ -7,14 +7,15 @@ import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'
|
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
8
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '@open-mercato/shared/lib/browser/
|
|
10
|
+
readVersionedIdSet,
|
|
11
|
+
writeVersionedIdSet,
|
|
12
|
+
clearVersionedPreference,
|
|
13
|
+
} from '@open-mercato/shared/lib/browser/versionedPreference'
|
|
14
14
|
import type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'
|
|
15
15
|
import { formatCurrency } from './utils'
|
|
16
16
|
|
|
17
17
|
const STORAGE_KEY = 'om:company-detail-kpi-hidden'
|
|
18
|
+
const STORAGE_VERSION = 1
|
|
18
19
|
|
|
19
20
|
function sumActiveDeals(deals: DealSummary[]): number {
|
|
20
21
|
return deals
|
|
@@ -100,7 +101,7 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
|
|
|
100
101
|
}, [data.interactions, data.kpis?.clientTenureYears])
|
|
101
102
|
|
|
102
103
|
const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(
|
|
103
|
-
() =>
|
|
104
|
+
() => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
const toggleTile = React.useCallback((tileId: string) => {
|
|
@@ -108,14 +109,14 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
|
|
|
108
109
|
const next = new Set(prev)
|
|
109
110
|
if (next.has(tileId)) next.delete(tileId)
|
|
110
111
|
else next.add(tileId)
|
|
111
|
-
|
|
112
|
+
writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)
|
|
112
113
|
return next
|
|
113
114
|
})
|
|
114
115
|
}, [])
|
|
115
116
|
|
|
116
117
|
const showAllTiles = React.useCallback(() => {
|
|
117
118
|
setHiddenTiles(new Set())
|
|
118
|
-
|
|
119
|
+
clearVersionedPreference(STORAGE_KEY)
|
|
119
120
|
}, [])
|
|
120
121
|
|
|
121
122
|
const kpiTiles = React.useMemo(() => [
|
|
@@ -8,9 +8,9 @@ import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
|
8
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
9
|
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '@open-mercato/shared/lib/browser/
|
|
11
|
+
readVersionedIdSet,
|
|
12
|
+
writeVersionedIdSet,
|
|
13
|
+
} from '@open-mercato/shared/lib/browser/versionedPreference'
|
|
14
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
15
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
16
16
|
import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
@@ -60,6 +60,7 @@ export type CompanyPeopleSectionProps = {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const COMPANY_PEOPLE_PAGE_SIZE = 20
|
|
63
|
+
const STARRED_PEOPLE_STORAGE_VERSION = 1
|
|
63
64
|
|
|
64
65
|
function normalizeCompanyPerson(record: Record<string, unknown>): CompanyPersonSummary | null {
|
|
65
66
|
const id = typeof record.id === 'string' ? record.id : null
|
|
@@ -206,7 +207,7 @@ export function CompanyPeopleSection({
|
|
|
206
207
|
const [listTotalCount, setListTotalCount] = React.useState(initialPeople.length)
|
|
207
208
|
const [listLoading, setListLoading] = React.useState(true)
|
|
208
209
|
const [starredIds, setStarredIds] = React.useState<Set<string>>(
|
|
209
|
-
() =>
|
|
210
|
+
() => readVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION),
|
|
210
211
|
)
|
|
211
212
|
const pendingPeopleChangeRef = React.useRef(false)
|
|
212
213
|
|
|
@@ -229,7 +230,7 @@ export function CompanyPeopleSection({
|
|
|
229
230
|
const next = new Set(prev)
|
|
230
231
|
if (next.has(personId)) next.delete(personId)
|
|
231
232
|
else next.add(personId)
|
|
232
|
-
|
|
233
|
+
writeVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION, next)
|
|
233
234
|
return next
|
|
234
235
|
})
|
|
235
236
|
},
|