@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.
Files changed (36) hide show
  1. package/AGENTS.md +8 -0
  2. package/dist/generated/entities/module_config/index.js +4 -0
  3. package/dist/generated/entities/module_config/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +2 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/auth/lib/setup-app.js +1 -1
  7. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  8. package/dist/modules/configs/data/entities.js +15 -1
  9. package/dist/modules/configs/data/entities.js.map +2 -2
  10. package/dist/modules/configs/lib/module-config-service.js +52 -19
  11. package/dist/modules/configs/lib/module-config-service.js.map +3 -3
  12. package/dist/modules/configs/migrations/Migration20260617150000.js +21 -0
  13. package/dist/modules/configs/migrations/Migration20260617150000.js.map +7 -0
  14. package/dist/modules/customers/api/activities/route.js.map +2 -2
  15. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -22
  16. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  17. package/dist/modules/customers/components/detail/CompanyKpiBar.js +8 -7
  18. package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
  19. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +6 -5
  20. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  21. package/generated/entities/module_config/index.ts +2 -0
  22. package/generated/entity-fields-registry.ts +2 -0
  23. package/package.json +7 -7
  24. package/src/modules/auth/lib/setup-app.ts +1 -1
  25. package/src/modules/configs/data/entities.ts +17 -1
  26. package/src/modules/configs/lib/module-config-service.ts +83 -24
  27. package/src/modules/configs/migrations/.snapshot-open-mercato.json +41 -4
  28. package/src/modules/configs/migrations/.snapshot-openmercato.json +41 -4
  29. package/src/modules/configs/migrations/Migration20260617150000.ts +21 -0
  30. package/src/modules/customers/api/activities/route.ts +6 -0
  31. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +29 -22
  32. package/src/modules/customers/components/detail/CompanyKpiBar.tsx +8 -7
  33. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +6 -5
  34. package/dist/modules/customers/components/detail/CompanyDashboardTab.js +0 -133
  35. package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +0 -7
  36. 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 = 'v1'
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 cacheKey = (moduleId: string, name: string) => `module-config:${CACHE_VERSION}:${moduleId}:${name}`
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>(moduleId: string, name: string, options?: { defaultValue?: T | null }): Promise<T | null>
93
- setValue(moduleId: string, name: string, value: unknown): Promise<ModuleConfigRecord | null>
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
- const entity = await repo.findOne({ moduleId, name })
113
- if (!entity) {
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(entity)
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>(rawModuleId: string, rawName: string, options?: { defaultValue?: T | null }): Promise<T | null> => {
123
- const record = await getRecord(rawModuleId, rawName)
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 (rawModuleId: string, rawName: string, value: unknown): Promise<ModuleConfigRecord | null> => {
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
- await writeCache(resolveCache(container), cacheKey(moduleId, name), { found: true, record }, moduleId)
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": "module_configs_module_name_unique",
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": true,
114
+ "constraint": false,
78
115
  "primary": false,
79
- "unique": true
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": "module_configs_module_name_unique",
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": true,
114
+ "constraint": false,
78
115
  "primary": false,
79
- "unique": true
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
- if (typeof window === 'undefined') return {}
176
- try {
177
- const raw = window.localStorage.getItem(`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`)
178
- if (!raw) return {}
179
- const parsed = JSON.parse(raw)
180
- if (!parsed || typeof parsed !== 'object') return {}
181
- const out: Record<string, number> = {}
182
- for (const [k, v] of Object.entries(parsed)) {
183
- if (typeof v === 'number' && Number.isFinite(v) && v >= MIN_LANE_WIDTH && v <= MAX_LANE_WIDTH) {
184
- out[k] = v
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
- if (typeof window === 'undefined') return
195
- try {
196
- if (Object.keys(widths).length === 0) {
197
- window.localStorage.removeItem(`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`)
198
- } else {
199
- window.localStorage.setItem(`${LANE_WIDTHS_STORAGE_KEY_PREFIX}:${scopeKey}`, JSON.stringify(widths))
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
- readJsonFromLocalStorage,
11
- writeJsonToLocalStorage,
12
- removeLocalStorageKey,
13
- } from '@open-mercato/shared/lib/browser/safeLocalStorage'
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
- () => new Set(readJsonFromLocalStorage<string[]>(STORAGE_KEY, [])),
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
- writeJsonToLocalStorage(STORAGE_KEY, [...next])
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
- removeLocalStorageKey(STORAGE_KEY)
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
- readJsonFromLocalStorage,
12
- writeJsonToLocalStorage,
13
- } from '@open-mercato/shared/lib/browser/safeLocalStorage'
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
- () => new Set(readJsonFromLocalStorage<string[]>(`om:starred-people:${companyId}`, [])),
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
- writeJsonToLocalStorage(`om:starred-people:${companyId}`, [...next])
233
+ writeVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION, next)
233
234
  return next
234
235
  })
235
236
  },