@open-mercato/core 0.10.1-develop.7836.1.0ef0f453c6 → 0.10.1-develop.7841.1.af87ac614d

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 (102) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
  3. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/admin/users.js +24 -7
  5. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  6. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
  7. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
  8. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
  9. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  10. package/dist/modules/customer_accounts/data/validators.js +2 -1
  11. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  12. package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
  13. package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
  14. package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
  15. package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
  16. package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
  17. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  18. package/dist/modules/customers/cli.js +28 -110
  19. package/dist/modules/customers/cli.js.map +3 -3
  20. package/dist/modules/customers/components/DictionarySettings.js +1 -1
  21. package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
  22. package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
  23. package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
  24. package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
  25. package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
  26. package/dist/modules/customers/components/detail/TaskForm.js +1 -16
  27. package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
  28. package/dist/modules/customers/components/detail/TasksSection.js +38 -10
  29. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  30. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
  31. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
  32. package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
  33. package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
  34. package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
  35. package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
  36. package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
  37. package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
  38. package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
  39. package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
  40. package/dist/modules/customers/translations.js +9 -0
  41. package/dist/modules/customers/translations.js.map +7 -0
  42. package/dist/modules/eudr/index.js +13 -0
  43. package/dist/modules/eudr/index.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
  45. package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
  46. package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
  47. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  48. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
  49. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  50. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
  51. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  52. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
  53. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
  54. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
  55. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
  56. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
  57. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
  58. package/dist/modules/sync_excel/index.js +11 -1
  59. package/dist/modules/sync_excel/index.js.map +2 -2
  60. package/dist/modules/translations/di.js +2 -0
  61. package/dist/modules/translations/di.js.map +2 -2
  62. package/package.json +7 -7
  63. package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
  64. package/src/modules/customer_accounts/api/admin/users.ts +26 -7
  65. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
  66. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
  67. package/src/modules/customer_accounts/data/validators.ts +1 -0
  68. package/src/modules/customer_accounts/i18n/de.json +10 -0
  69. package/src/modules/customer_accounts/i18n/en.json +10 -0
  70. package/src/modules/customer_accounts/i18n/es.json +10 -0
  71. package/src/modules/customer_accounts/i18n/ko.json +10 -0
  72. package/src/modules/customer_accounts/i18n/pl.json +10 -0
  73. package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
  74. package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
  75. package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
  76. package/src/modules/customers/cli.ts +23 -124
  77. package/src/modules/customers/components/DictionarySettings.tsx +1 -1
  78. package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
  79. package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
  80. package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
  81. package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
  82. package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
  83. package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
  84. package/src/modules/customers/i18n/de.json +81 -0
  85. package/src/modules/customers/i18n/en.json +81 -0
  86. package/src/modules/customers/i18n/es.json +81 -0
  87. package/src/modules/customers/i18n/ko.json +81 -0
  88. package/src/modules/customers/i18n/pl.json +81 -0
  89. package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
  90. package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
  91. package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
  92. package/src/modules/customers/translations.ts +5 -0
  93. package/src/modules/eudr/index.ts +13 -0
  94. package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
  95. package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
  96. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
  97. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
  98. package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
  99. package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
  100. package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
  101. package/src/modules/sync_excel/index.ts +10 -0
  102. package/src/modules/translations/di.ts +2 -0
@@ -24,6 +24,8 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
24
24
  import { createLogger } from '@open-mercato/shared/lib/logger'
25
25
  import { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'
26
26
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
27
+ import { getTranslationOverlayPlugin } from '@open-mercato/shared/lib/localization/overlay-plugin'
28
+ import { localizeCustomerDictionaryEntries } from '../../../lib/dictionaryLabels'
27
29
  import { calendarEventTypeBehaviorSchema } from '../../../calendar-event-types'
28
30
  import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
29
31
 
@@ -42,6 +44,9 @@ const postSchema = z.object({
42
44
 
43
45
  const querySchema = z.object({
44
46
  organizationId: z.string().uuid().optional(),
47
+ labels: z.enum(['localized', 'base']).optional().describe(
48
+ 'Label projection. `localized` (default) resolves labels for the request locale; `base` returns the stored labels that management screens edit.',
49
+ ),
45
50
  })
46
51
 
47
52
  export const metadata = {
@@ -54,11 +59,24 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
54
59
  const url = new URL(req.url)
55
60
  const query = querySchema.parse({
56
61
  organizationId: url.searchParams.get('organizationId') ?? undefined,
62
+ labels: url.searchParams.get('labels') ?? undefined,
57
63
  })
58
64
  const { translate, em, organizationId, readableOrganizationIds, tenantId, cache, container } = await resolveDictionaryRouteContext(req, {
59
65
  selectedId: query.organizationId ?? undefined,
60
66
  })
61
67
  const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)
68
+ const localizeResponse = async (body: z.infer<typeof dictionaryCacheResponseSchema>) => {
69
+ if (query.labels === 'base') return body
70
+ const { resolveLocale } = getTranslationOverlayPlugin()
71
+ const locale = resolveLocale?.(req) ?? (await resolveTranslations()).locale ?? 'en'
72
+ return {
73
+ ...body,
74
+ items: sortDictionaryEntries(
75
+ await localizeCustomerDictionaryEntries(body.items, { kind: mappedKind, locale, tenantId, container }),
76
+ resolveDictionaryEntrySortMode(body.sortMode),
77
+ ),
78
+ }
79
+ }
62
80
  if (!organizationId) {
63
81
  throw new CrudHttpError(400, {
64
82
  error: translate('customers.errors.organization_required', 'Organization context is required'),
@@ -87,16 +105,16 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
87
105
  sortMode,
88
106
  readableOrganizationIds: scopedOrganizationIds,
89
107
  })
90
- const cached = await cache.get(cacheKey)
91
- if (cached) {
92
- return NextResponse.json(cached)
108
+ const cached = dictionaryCacheResponseSchema.safeParse(await cache.get(cacheKey))
109
+ if (cached.success) {
110
+ return NextResponse.json(await localizeResponse(cached.data))
93
111
  }
94
112
  }
95
113
 
96
114
  const entries = await findWithDecryption(
97
115
  em,
98
116
  CustomerDictionaryEntry,
99
- { tenantId, kind: mappedKind, organizationId: { $in: scopedOrganizationIds } } as any,
117
+ { tenantId, kind: mappedKind, organizationId: { $in: scopedOrganizationIds } },
100
118
  { orderBy: { label: 'asc' } },
101
119
  { tenantId, organizationId },
102
120
  )
@@ -180,7 +198,7 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
180
198
  }
181
199
  }
182
200
 
183
- return NextResponse.json(responseBody)
201
+ return NextResponse.json(await localizeResponse(responseBody))
184
202
  } catch (err) {
185
203
  if (isCrudHttpError(err)) {
186
204
  return NextResponse.json(err.body, { status: err.status })
@@ -315,6 +333,10 @@ const dictionaryListResponseSchema = z.object({
315
333
  items: z.array(dictionaryEntrySchema),
316
334
  })
317
335
 
336
+ const dictionaryCacheResponseSchema = dictionaryListResponseSchema.extend({
337
+ items: z.array(dictionaryEntrySchema.extend({ label: z.string(), organizationId: z.string() }).passthrough()),
338
+ })
339
+
318
340
  const dictionaryErrorSchema = z.object({
319
341
  error: z.string(),
320
342
  code: z.string().optional(),
@@ -326,7 +348,8 @@ export const openApi: OpenApiRouteDoc = {
326
348
  methods: {
327
349
  GET: {
328
350
  summary: 'List dictionary entries',
329
- description: 'Returns dictionary entries for the requested kind within the currently selected organization.',
351
+ description: 'Returns dictionary entries for the requested kind within the currently selected organization. Labels are localized for the request locale unless `labels=base` is passed.',
352
+ query: querySchema,
330
353
  responses: [
331
354
  { status: 200, description: 'Dictionary entries', schema: dictionaryListResponseSchema },
332
355
  { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },
@@ -39,129 +39,38 @@ import {
39
39
  CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,
40
40
  } from './lib/interactionCompatibility'
41
41
  import { CUSTOMER_CUSTOM_FIELD_SETS } from './customFieldDefaults'
42
+ import {
43
+ ENTITY_STATUS_DEFAULTS,
44
+ ENTITY_SOURCE_DEFAULTS,
45
+ ENTITY_LIFECYCLE_STAGE_DEFAULTS,
46
+ ADDRESS_TYPE_DEFAULTS,
47
+ ACTIVITY_TYPE_DEFAULTS,
48
+ INTERACTION_STATUS_DEFAULTS,
49
+ JOB_TITLE_DEFAULTS,
50
+ DEAL_STATUS_DEFAULTS,
51
+ PIPELINE_STAGE_DEFAULTS,
52
+ INDUSTRY_DEFAULTS,
53
+ TEMPERATURE_DEFAULTS,
54
+ PERSON_COMPANY_ROLE_DEFAULTS,
55
+ } from './lib/dictionaryDefaults'
56
+
57
+ export {
58
+ DEAL_STATUS_DEFAULTS,
59
+ PIPELINE_STAGE_DEFAULTS,
60
+ ENTITY_STATUS_DEFAULTS,
61
+ ENTITY_LIFECYCLE_STAGE_DEFAULTS,
62
+ ENTITY_SOURCE_DEFAULTS,
63
+ INTERACTION_STATUS_DEFAULTS,
64
+ } from './lib/dictionaryDefaults'
42
65
 
43
66
  type SeedArgs = {
44
67
  tenantId: string
45
68
  organizationId: string
46
69
  }
47
70
 
48
- type DictionaryDefault = {
49
- value: string
50
- label: string
51
- color?: string
52
- icon?: string
53
- }
54
-
55
71
  type CustomFieldValuesPayload = Parameters<DataEngine['setCustomFields']>[0]['values']
56
72
  type ProgressBarHandle = ReturnType<typeof createProgressBar>
57
73
 
58
- export const DEAL_STATUS_DEFAULTS: DictionaryDefault[] = [
59
- { value: 'open', label: 'Open', color: '#2563eb', icon: 'lucide:circle' },
60
- { value: 'closed', label: 'Closed', color: '#6b7280', icon: 'lucide:check-circle' },
61
- { value: 'win', label: 'Win', color: '#22c55e', icon: 'lucide:trophy' },
62
- { value: 'lost', label: 'Lost', color: '#ef4444', icon: 'lucide:flag' },
63
- { value: 'in_progress', label: 'In progress', color: '#f59e0b', icon: 'lucide:activity' },
64
- ]
65
-
66
- export const PIPELINE_STAGE_DEFAULTS: DictionaryDefault[] = [
67
- { value: 'opportunity', label: 'Opportunity', color: '#38bdf8', icon: 'lucide:target' },
68
- { value: 'marketing_qualified_lead', label: 'Marketing Qualified Lead', color: '#a855f7', icon: 'lucide:sparkles' },
69
- { value: 'sales_qualified_lead', label: 'Sales Qualified Lead', color: '#f97316', icon: 'lucide:users' },
70
- { value: 'offering', label: 'Offering', color: '#22c55e', icon: 'lucide:package' },
71
- { value: 'negotiations', label: 'Negotiations', color: '#facc15', icon: 'lucide:handshake' },
72
- { value: 'win', label: 'Win', color: '#16a34a', icon: 'lucide:award' },
73
- { value: 'lost', label: 'Lost', color: '#ef4444', icon: 'lucide:flag' },
74
- { value: 'stalled', label: 'Stalled', color: '#6b7280', icon: 'lucide:alert-circle' },
75
- ]
76
-
77
- export const ENTITY_STATUS_DEFAULTS: DictionaryDefault[] = [
78
- { value: 'active', label: 'Active', color: '#22c55e', icon: 'lucide:user-check' },
79
- { value: 'inactive', label: 'Inactive', color: '#94a3b8', icon: 'lucide:pause-circle' },
80
- { value: 'pending', label: 'Pending', color: '#f59e0b', icon: 'lucide:clock' },
81
- { value: 'archived', label: 'Archived', color: '#64748b', icon: 'lucide:archive' },
82
- ]
83
-
84
- export const ENTITY_LIFECYCLE_STAGE_DEFAULTS: DictionaryDefault[] = [
85
- { value: 'lead', label: 'Lead', color: '#3b82f6', icon: 'lucide:sparkles' },
86
- { value: 'prospect', label: 'Prospect', color: '#8b5cf6', icon: 'lucide:eye' },
87
- { value: 'customer', label: 'Customer', color: '#22c55e', icon: 'lucide:handshake' },
88
- { value: 'subscriber', label: 'Subscriber', color: '#10b981', icon: 'lucide:bell' },
89
- { value: 'churned', label: 'Churned', color: '#ef4444', icon: 'lucide:user-x' },
90
- { value: 'other', label: 'Other', color: '#94a3b8', icon: 'lucide:circle' },
91
- ]
92
-
93
- export const ENTITY_SOURCE_DEFAULTS: DictionaryDefault[] = [
94
- { value: 'linkedin', label: 'LinkedIn', color: '#0a66c2', icon: 'lucide:linkedin' },
95
- { value: 'email', label: 'Email', color: '#3b82f6', icon: 'lucide:mail' },
96
- { value: 'web_form', label: 'Web form', color: '#22c55e', icon: 'lucide:globe' },
97
- { value: 'referral', label: 'Referral', color: '#8b5cf6', icon: 'lucide:users' },
98
- { value: 'customer_referral', label: 'Customer referral', color: '#22c55e', icon: 'lucide:thumbs-up' },
99
- { value: 'partner_referral', label: 'Partner referral', color: '#3b82f6', icon: 'lucide:handshake' },
100
- { value: 'event', label: 'Conference / Event', color: '#f59e0b', icon: 'lucide:calendar' },
101
- { value: 'cold_outreach', label: 'Cold outreach', color: '#94a3b8', icon: 'lucide:phone' },
102
- { value: 'facebook', label: 'Facebook', color: '#1877f2', icon: 'lucide:facebook' },
103
- { value: 'typeform', label: 'Typeform', color: '#262627', icon: 'lucide:file-text' },
104
- { value: 'other', label: 'Other', color: '#64748b', icon: 'lucide:circle' },
105
- ]
106
-
107
- const ADDRESS_TYPE_DEFAULTS: DictionaryDefault[] = [
108
- { value: 'office', label: 'Office', color: '#3b82f6', icon: 'lucide:building' },
109
- { value: 'work', label: 'Work', color: '#6366f1', icon: 'lucide:briefcase' },
110
- { value: 'billing', label: 'Billing', color: '#f97316', icon: 'lucide:wallet' },
111
- { value: 'shipping', label: 'Shipping', color: '#22c55e', icon: 'lucide:truck' },
112
- { value: 'home', label: 'Home', color: '#10b981', icon: 'lucide:map-pin' },
113
- ]
114
-
115
- const ACTIVITY_TYPE_DEFAULTS: DictionaryDefault[] = [
116
- { value: 'call', label: 'Call', color: '#2563eb', icon: 'lucide:phone-call' },
117
- { value: 'email', label: 'Email', color: '#16a34a', icon: 'lucide:mail' },
118
- { value: 'event', label: 'Event', color: '#6366f1', icon: 'lucide:calendar' },
119
- { value: 'meeting', label: 'Meeting', color: '#f59e0b', icon: 'lucide:users' },
120
- { value: 'note', label: 'Note', color: '#a855f7', icon: 'lucide:notebook' },
121
- { value: 'task', label: 'Task', color: '#ef4444', icon: 'lucide:check-square' },
122
- ]
123
-
124
- export const INTERACTION_STATUS_DEFAULTS: DictionaryDefault[] = [
125
- { value: 'planned', label: 'Planned', color: '#2563eb', icon: 'lucide:circle' },
126
- { value: 'in_progress', label: 'In progress', color: '#f59e0b', icon: 'lucide:activity' },
127
- { value: 'waiting', label: 'Waiting / blocked', color: '#a855f7', icon: 'lucide:pause-circle' },
128
- { value: 'done', label: 'Done', color: '#22c55e', icon: 'lucide:check-circle' },
129
- { value: 'canceled', label: 'Canceled', color: '#6b7280', icon: 'lucide:x-circle' },
130
- ]
131
-
132
- const JOB_TITLE_DEFAULTS: DictionaryDefault[] = [
133
- { value: 'Director of Operations', label: 'Director of Operations', color: '#f97316', icon: 'lucide:settings' },
134
- { value: 'VP of Partnerships', label: 'VP of Partnerships', color: '#6366f1', icon: 'lucide:users' },
135
- { value: 'Founder & Principal', label: 'Founder & Principal', color: '#ec4899', icon: 'lucide:star' },
136
- { value: 'Senior Project Manager', label: 'Senior Project Manager', color: '#0ea5e9', icon: 'lucide:clipboard-list' },
137
- { value: 'Chief Revenue Officer', label: 'Chief Revenue Officer', color: '#8b5cf6', icon: 'lucide:bar-chart-3' },
138
- { value: 'Director of Retail Partnerships', label: 'Director of Retail Partnerships', color: '#f59e0b', icon: 'lucide:shopping-bag' },
139
- ]
140
-
141
- const INDUSTRY_DEFAULTS: DictionaryDefault[] = [
142
- { value: 'Renewable Energy', label: 'Renewable Energy' },
143
- { value: 'Software', label: 'Software' },
144
- { value: 'Interior Design', label: 'Interior Design' },
145
- { value: 'SaaS', label: 'SaaS' },
146
- { value: 'E-commerce', label: 'E-commerce' },
147
- { value: 'Healthcare', label: 'Healthcare' },
148
- { value: 'Manufacturing', label: 'Manufacturing' },
149
- { value: 'Logistics', label: 'Logistics' },
150
- { value: 'Financial Services', label: 'Financial Services' },
151
- { value: 'Retail', label: 'Retail' },
152
- { value: 'Hospitality', label: 'Hospitality' },
153
- { value: 'Energy', label: 'Energy' },
154
- { value: 'Media', label: 'Media' },
155
- ]
156
-
157
- const TEMPERATURE_DEFAULTS: DictionaryDefault[] = [
158
- { value: 'hot', label: 'Hot', color: '#ef4444', icon: 'lucide:flame' },
159
- { value: 'high', label: 'High', color: '#f59e0b', icon: 'lucide:trending-up' },
160
- { value: 'medium', label: 'Medium', color: '#8b5cf6', icon: 'lucide:sparkles' },
161
- { value: 'low', label: 'Low', color: '#64748b', icon: 'lucide:clock' },
162
- { value: 'cold', label: 'Cold', color: '#94a3b8', icon: 'lucide:snowflake' },
163
- ]
164
-
165
74
  const CUSTOM_TAG_SEED_DEFAULTS = [
166
75
  { value: 'architecture', label: 'architecture' },
167
76
  { value: 'hospitality', label: 'hospitality' },
@@ -179,15 +88,6 @@ const CUSTOM_TAG_SEED_DEFAULTS = [
179
88
  { value: 'case-study-candidate', label: 'case-study-candidate' },
180
89
  ]
181
90
 
182
- const PERSON_COMPANY_ROLE_DEFAULTS = [
183
- { value: 'decision_maker', label: 'Decision maker', color: '#f59e0b', icon: 'lucide:crown' },
184
- { value: 'influencer', label: 'Influencer', color: '#8b5cf6', icon: 'lucide:sparkles' },
185
- { value: 'budget_holder', label: 'Budget holder', color: '#3b82f6', icon: 'lucide:wallet' },
186
- { value: 'technical_evaluator', label: 'Technical evaluator', color: '#22c55e', icon: 'lucide:wrench' },
187
- { value: 'primary_contact', label: 'Primary contact', color: '#0ea5e9', icon: 'lucide:star' },
188
- { value: 'end_user', label: 'End user', color: '#64748b', icon: 'lucide:user' },
189
- ]
190
-
191
91
  const PRIORITY_CURRENCIES = ['EUR', 'USD', 'GBP', 'PLN']
192
92
 
193
93
  type ExampleAddress = {
@@ -2679,7 +2579,6 @@ async function seedCustomerStressTest(
2679
2579
  return { created: toCreate, existing: existingPersons }
2680
2580
  }
2681
2581
 
2682
-
2683
2582
  const STRESS_TEST_REQUIRED_COLUMNS: Record<string, readonly string[]> = {
2684
2583
  customer_entities: [
2685
2584
  'id',
@@ -237,7 +237,7 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
237
237
  setLoading(true)
238
238
  try {
239
239
  const data = await readApiResultOrThrow<{ items?: unknown[] }>(
240
- `/api/customers/dictionaries/${kind}`,
240
+ `/api/customers/dictionaries/${kind}?labels=base`,
241
241
  undefined,
242
242
  { errorMessage: errorLoad },
243
243
  )
@@ -560,7 +560,7 @@ export function ManageTagsDialog({ open, onClose }: ManageTagsDialogProps) {
560
560
  try {
561
561
  const data = await readApiResultOrThrow<{
562
562
  items?: Array<Record<string, unknown>>
563
- }>(`/api/customers/dictionaries/${category.kind}`, { cache: 'no-store' })
563
+ }>(`/api/customers/dictionaries/${category.kind}?labels=base`, { cache: 'no-store' })
564
564
  const entries = Array.isArray(data?.items)
565
565
  ? data.items
566
566
  .map(makeDraftEntry)
@@ -38,9 +38,23 @@ export function TaskDialog({ open, mode, onOpenChange, initialValues, onSubmit,
38
38
  [onOpenChange, onSubmit],
39
39
  )
40
40
 
41
+ const handleKeyDown = React.useCallback(
42
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
43
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
44
+ event.preventDefault()
45
+ event.currentTarget.querySelector('form')?.requestSubmit()
46
+ }
47
+ if (event.key === 'Escape') {
48
+ event.preventDefault()
49
+ handleCancel()
50
+ }
51
+ },
52
+ [handleCancel],
53
+ )
54
+
41
55
  return (
42
56
  <Dialog open={open} onOpenChange={onOpenChange}>
43
- <DialogContent className="sm:max-w-2xl">
57
+ <DialogContent className="sm:max-w-2xl" onKeyDown={handleKeyDown}>
44
58
  <DialogHeader>
45
59
  <DialogTitle>{dialogTitle}</DialogTitle>
46
60
  {contextMessage ? <DialogDescription>{contextMessage}</DialogDescription> : null}
@@ -59,7 +59,6 @@ export function TaskForm({
59
59
  useCanonicalInteractions = false,
60
60
  }: TaskFormProps) {
61
61
  const t = useT()
62
- const containerRef = React.useRef<HTMLDivElement | null>(null)
63
62
  const scopeVersion = useOrganizationScopeVersion()
64
63
  const statusDictionary = useCustomerDictionary('interaction-statuses', scopeVersion)
65
64
 
@@ -101,21 +100,6 @@ export function TaskForm({
101
100
  : t('customers.people.detail.tasks.form.submitCreate', 'Save task (⌘/Ctrl + Enter)'))
102
101
  const resolvedCancelLabel = cancelLabel ?? t('customers.people.detail.tasks.cancel', 'Cancel')
103
102
 
104
- const handleKeyDown = React.useCallback(
105
- (event: React.KeyboardEvent<HTMLDivElement>) => {
106
- if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
107
- event.preventDefault()
108
- const form = containerRef.current?.querySelector('form')
109
- form?.requestSubmit()
110
- }
111
- if (event.key === 'Escape') {
112
- event.preventDefault()
113
- onCancel()
114
- }
115
- },
116
- [onCancel],
117
- )
118
-
119
103
  const handleSubmit = React.useCallback(
120
104
  async (values: Record<string, unknown>) => {
121
105
  const payload = buildTaskSubmitPayload(values, t)
@@ -133,7 +117,7 @@ export function TaskForm({
133
117
  }, [initialValues, mode, useCanonicalInteractions])
134
118
 
135
119
  return (
136
- <div ref={containerRef} onKeyDown={handleKeyDown}>
120
+ <div>
137
121
  <CrudForm
138
122
  embedded
139
123
  entityId={formEntityId}
@@ -9,6 +9,11 @@ import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitive
9
9
  import { mapDictionaryColorToTone } from '@open-mercato/shared/lib/query/advanced-filter'
10
10
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
11
11
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
+ import { dismissRecordConflict } from '@open-mercato/ui/backend/conflicts'
13
+ import {
14
+ extractOptimisticLockConflict,
15
+ extractRecordLockConflict,
16
+ } from '@open-mercato/ui/backend/utils/optimisticLock'
12
17
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
13
18
  import { LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'
14
19
  import { cn } from '@open-mercato/shared/lib/utils'
@@ -49,6 +54,10 @@ type TasksSectionProps = {
49
54
 
50
55
  const RESERVED_TASK_CUSTOM_KEYS = new Set(['priority', 'description', 'due_at', 'dueAt'])
51
56
 
57
+ function isRecordConflict(error: unknown): boolean {
58
+ return Boolean(extractOptimisticLockConflict(error) || extractRecordLockConflict(error))
59
+ }
60
+
52
61
  function toTimestamp(value: string | null | undefined): number | null {
53
62
  if (!value) return null
54
63
  const timestamp = new Date(value).getTime()
@@ -240,6 +249,13 @@ export function TasksSection({
240
249
  setEditingTask(null)
241
250
  }, [])
242
251
 
252
+ React.useEffect(() => {
253
+ if (!dialogOpen || dialogMode !== 'edit' || !editingTask) return
254
+ const refreshedTask = tasks.find((task) => task.id === editingTask.id)
255
+ if (!refreshedTask || refreshedTask === editingTask) return
256
+ setEditingTask(refreshedTask)
257
+ }, [dialogMode, dialogOpen, editingTask, tasks])
258
+
243
259
  React.useEffect(() => {
244
260
  if (!onActionChange) return
245
261
  if (!entityId) {
@@ -295,8 +311,10 @@ export function TasksSection({
295
311
  flash(t('customers.people.detail.tasks.createSuccess', 'Task created'), 'success')
296
312
  await Promise.resolve(onDataRefresh?.())
297
313
  } catch (err) {
298
- const message = err instanceof Error ? err.message : t('customers.people.detail.tasks.error', 'Failed to create task')
299
- flash(message, 'error')
314
+ if (!isRecordConflict(err)) {
315
+ const message = err instanceof Error ? err.message : t('customers.people.detail.tasks.error', 'Failed to create task')
316
+ flash(message, 'error')
317
+ }
300
318
  throw err
301
319
  }
302
320
  },
@@ -315,12 +333,15 @@ export function TasksSection({
315
333
  isDone: payload.base.is_done ?? undefined,
316
334
  },
317
335
  )
336
+ dismissRecordConflict()
318
337
  flash(t('customers.people.detail.tasks.updateSuccess', 'Task updated'), 'success')
319
338
  await Promise.resolve(onDataRefresh?.())
320
339
  } catch (err) {
321
- const message =
322
- err instanceof Error ? err.message : t('customers.people.detail.tasks.updateError', 'Failed to update task')
323
- flash(message, 'error')
340
+ if (!isRecordConflict(err)) {
341
+ const message =
342
+ err instanceof Error ? err.message : t('customers.people.detail.tasks.updateError', 'Failed to update task')
343
+ flash(message, 'error')
344
+ }
324
345
  throw err
325
346
  }
326
347
  },
@@ -338,6 +359,7 @@ export function TasksSection({
338
359
  isDone: nextIsDone,
339
360
  },
340
361
  )
362
+ dismissRecordConflict()
341
363
  flash(
342
364
  nextIsDone
343
365
  ? t('customers.people.detail.tasks.completeSuccess', 'Task marked as done')
@@ -346,9 +368,11 @@ export function TasksSection({
346
368
  )
347
369
  await Promise.resolve(onDataRefresh?.())
348
370
  } catch (err) {
349
- const message =
350
- err instanceof Error ? err.message : t('customers.people.detail.tasks.toggleError', 'Failed to update task status')
351
- flash(message, 'error')
371
+ if (!isRecordConflict(err)) {
372
+ const message =
373
+ err instanceof Error ? err.message : t('customers.people.detail.tasks.toggleError', 'Failed to update task status')
374
+ flash(message, 'error')
375
+ }
352
376
  }
353
377
  },
354
378
  [onDataRefresh, runWriteMutation, t, toggleTask],
@@ -374,13 +398,16 @@ export function TasksSection({
374
398
  todoId: task.todoId,
375
399
  },
376
400
  )
401
+ dismissRecordConflict()
377
402
  flash(t('customers.people.detail.tasks.deleteSuccess', 'Task removed'), 'success')
378
403
  await Promise.resolve(onDataRefresh?.())
379
404
  await refresh()
380
405
  } catch (err) {
381
- const message =
382
- err instanceof Error ? err.message : t('customers.people.detail.tasks.deleteError', 'Failed to remove task')
383
- flash(message, 'error')
406
+ if (!isRecordConflict(err)) {
407
+ const message =
408
+ err instanceof Error ? err.message : t('customers.people.detail.tasks.deleteError', 'Failed to remove task')
409
+ flash(message, 'error')
410
+ }
384
411
  }
385
412
  },
386
413
  [confirm, onDataRefresh, refresh, runWriteMutation, t, unlinkTask],
@@ -394,12 +421,15 @@ export function TasksSection({
394
421
  () => canonicalResult.cancelInteraction(task.todoId),
395
422
  { id: task.todoId },
396
423
  )
424
+ dismissRecordConflict()
397
425
  flash(t('customers.people.detail.tasks.cancelSuccess', 'Task canceled'), 'success')
398
426
  await Promise.resolve(onDataRefresh?.())
399
427
  } catch (err) {
400
- const message =
401
- err instanceof Error ? err.message : t('customers.people.detail.tasks.cancelError', 'Failed to cancel task')
402
- flash(message, 'error')
428
+ if (!isRecordConflict(err)) {
429
+ const message =
430
+ err instanceof Error ? err.message : t('customers.people.detail.tasks.cancelError', 'Failed to cancel task')
431
+ flash(message, 'error')
432
+ }
403
433
  }
404
434
  },
405
435
  [canonicalResult, onDataRefresh, runWriteMutation, t, useCanonicalInteractions],
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
4
4
  import { useQuery, type QueryClient, type UseQueryResult } from '@tanstack/react-query'
5
+ import { useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'
5
6
  import {
6
7
  createDictionaryMap,
7
8
  normalizeDictionaryEntries,
@@ -26,17 +27,21 @@ const DICTIONARY_STALE_TIME = 5 * 60 * 1000
26
27
 
27
28
  const BASE_DICTIONARY_QUERY_KEY = ['customers', 'dictionaries'] as const
28
29
 
29
- export const customerDictionaryQueryKey = (kind: CustomerDictionaryKind, scopeVersion = 0, organizationId?: string | null) =>
30
- [...BASE_DICTIONARY_QUERY_KEY, kind, `scope:${scopeVersion}`, organizationId ? `org:${organizationId}` : 'org:default'] as const
30
+ function resolveDictionaryLocale(): string {
31
+ return typeof document === 'undefined' ? 'en' : document.documentElement.lang || 'en'
32
+ }
33
+
34
+ export const customerDictionaryQueryKey = (kind: CustomerDictionaryKind, scopeVersion = 0, organizationId?: string | null, locale = resolveDictionaryLocale()) =>
35
+ [...BASE_DICTIONARY_QUERY_KEY, kind, `scope:${scopeVersion}`, organizationId ? `org:${organizationId}` : 'org:default', `locale:${locale}`] as const
31
36
 
32
- export const customerDictionaryQueryOptions = (kind: CustomerDictionaryKind, scopeVersion = 0, organizationId?: string | null) => ({
33
- queryKey: customerDictionaryQueryKey(kind, scopeVersion, organizationId),
37
+ export const customerDictionaryQueryOptions = (kind: CustomerDictionaryKind, scopeVersion = 0, organizationId?: string | null, locale = resolveDictionaryLocale()) => ({
38
+ queryKey: customerDictionaryQueryKey(kind, scopeVersion, organizationId, locale),
34
39
  staleTime: DICTIONARY_STALE_TIME,
35
40
  gcTime: DICTIONARY_STALE_TIME,
36
41
  queryFn: async (): Promise<CustomerDictionaryQueryData> => {
37
- const url = organizationId
38
- ? `/api/customers/dictionaries/${kind}?organizationId=${encodeURIComponent(organizationId)}`
39
- : `/api/customers/dictionaries/${kind}`
42
+ const params = new URLSearchParams({ locale })
43
+ if (organizationId) params.set('organizationId', organizationId)
44
+ const url = `/api/customers/dictionaries/${kind}?${params}`
40
45
  const payload = await readApiResultOrThrow<Record<string, unknown>>(
41
46
  url,
42
47
  undefined,
@@ -87,7 +92,8 @@ export function useCustomerDictionary(
87
92
  scopeVersion = 0,
88
93
  organizationId?: string | null,
89
94
  ): UseQueryResult<CustomerDictionaryQueryData> {
90
- return useQuery(customerDictionaryQueryOptions(kind, scopeVersion, organizationId))
95
+ const locale = useOptionalLocale()
96
+ return useQuery(customerDictionaryQueryOptions(kind, scopeVersion, organizationId, locale))
91
97
  }
92
98
 
93
99
  export async function invalidateCustomerDictionary(queryClient: QueryClient, kind: CustomerDictionaryKind) {
@@ -249,7 +249,7 @@ export function useInteractions({
249
249
  )
250
250
  await refresh()
251
251
  } catch (err) {
252
- if (surfaceRecordConflict(err, t)) { await refresh(); return }
252
+ if (surfaceRecordConflict(err, t)) await refresh()
253
253
  throw err
254
254
  } finally {
255
255
  setIsMutating(false)
@@ -278,7 +278,7 @@ export function useInteractions({
278
278
  )
279
279
  await refresh()
280
280
  } catch (err) {
281
- if (surfaceRecordConflict(err, t)) { await refresh(); return }
281
+ if (surfaceRecordConflict(err, t)) await refresh()
282
282
  throw err
283
283
  } finally {
284
284
  setPendingId(null)
@@ -308,7 +308,7 @@ export function useInteractions({
308
308
  )
309
309
  await refresh()
310
310
  } catch (err) {
311
- if (surfaceRecordConflict(err, t)) { await refresh(); return }
311
+ if (surfaceRecordConflict(err, t)) await refresh()
312
312
  throw err
313
313
  } finally {
314
314
  setPendingId(null)
@@ -340,7 +340,6 @@ export function useInteractions({
340
340
  // the unified conflict bar and re-syncs the list instead of a raw error (#2055).
341
341
  if (surfaceRecordConflict(err, t)) {
342
342
  await refresh()
343
- return
344
343
  }
345
344
  throw err
346
345
  } finally {