@open-mercato/core 0.6.6-develop.6313.1.618a49a6b4 → 0.6.6-develop.6317.1.b3be10ab84

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 (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/ai-tools/_shared.js +158 -0
  3. package/dist/modules/customers/ai-tools/_shared.js.map +7 -0
  4. package/dist/modules/customers/ai-tools/companies-pack.js +15 -121
  5. package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
  6. package/dist/modules/customers/ai-tools/people-pack.js +9 -107
  7. package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
  8. package/dist/modules/customers/components/DictionarySettings.js +92 -37
  9. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  10. package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -0
  11. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
  12. package/dist/modules/customers/components/calendar/EventPeekPopover.js +12 -1
  13. package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
  14. package/dist/modules/customers/components/calendar/TimeGrid.js +3 -0
  15. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  16. package/dist/modules/customers/components/calendar/types.js.map +1 -1
  17. package/dist/modules/customers/components/formConfig.js +7 -1
  18. package/dist/modules/customers/components/formConfig.js.map +2 -2
  19. package/dist/modules/customers/lib/dictionaries.js +10 -0
  20. package/dist/modules/customers/lib/dictionaries.js.map +2 -2
  21. package/package.json +7 -7
  22. package/src/modules/customers/ai-tools/_shared.ts +270 -0
  23. package/src/modules/customers/ai-tools/companies-pack.ts +17 -157
  24. package/src/modules/customers/ai-tools/people-pack.ts +11 -133
  25. package/src/modules/customers/components/DictionarySettings.tsx +60 -2
  26. package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -0
  27. package/src/modules/customers/components/calendar/EventPeekPopover.tsx +29 -11
  28. package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -0
  29. package/src/modules/customers/components/calendar/types.ts +1 -0
  30. package/src/modules/customers/components/formConfig.tsx +8 -2
  31. package/src/modules/customers/i18n/de.json +1 -0
  32. package/src/modules/customers/i18n/en.json +1 -0
  33. package/src/modules/customers/i18n/es.json +1 -0
  34. package/src/modules/customers/i18n/pl.json +1 -0
  35. package/src/modules/customers/lib/dictionaries.ts +10 -0
@@ -17,7 +17,6 @@
17
17
  * documented aggregate detail route). Tool name, schema, requiredFeatures,
18
18
  * and output shape are unchanged.
19
19
  */
20
- import type { EntityManager } from '@mikro-orm/postgresql'
21
20
  import { z } from 'zod'
22
21
  import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
23
22
  import {
@@ -30,20 +29,17 @@ import {
30
29
  CustomerPersonProfile,
31
30
  } from '../data/entities'
32
31
  import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
32
+ import {
33
+ buildRelatedRecords,
34
+ buildScope,
35
+ resolveEm,
36
+ toCustomerListSummary,
37
+ toIso,
38
+ type CustomerRelatedRecords,
39
+ } from './_shared'
33
40
 
34
41
  const NIL_UUID = '00000000-0000-0000-0000-000000000000'
35
42
 
36
- function resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {
37
- return ctx.container.resolve<EntityManager>('em')
38
- }
39
-
40
- function buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {
41
- return {
42
- tenantId,
43
- organizationId: ctx.organizationId,
44
- }
45
- }
46
-
47
43
  const listPeopleInput = z
48
44
  .object({
49
45
  q: z.string().trim().optional().describe('Optional search text matched against display name / email / phone. Omit or leave empty to list all.'),
@@ -146,23 +142,7 @@ const listPeopleTool = defineApiBackedAiTool<ListPeopleInput, ListPeopleApiRespo
146
142
  const data = (response.data ?? {}) as ListPeopleApiResponse
147
143
  const rawItems: ListPeopleApiItem[] = Array.isArray(data.items) ? data.items : []
148
144
  return {
149
- items: rawItems.map((row) => {
150
- const createdAtRaw = row.created_at ?? row.createdAt ?? null
151
- const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
152
- return {
153
- id: row.id,
154
- displayName: row.display_name ?? row.displayName ?? null,
155
- primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
156
- primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
157
- status: row.status ?? null,
158
- lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
159
- source: row.source ?? null,
160
- ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
161
- organizationId: row.organization_id ?? row.organizationId ?? null,
162
- tenantId: row.tenant_id ?? row.tenantId ?? null,
163
- createdAt,
164
- }
165
- }),
145
+ items: rawItems.map((row) => toCustomerListSummary(row)),
166
146
  total: typeof data.total === 'number' ? data.total : 0,
167
147
  limit,
168
148
  offset,
@@ -182,13 +162,6 @@ type GetPersonInput = z.infer<typeof getPersonInput>
182
162
 
183
163
  type ApiPersonDetailRow = Record<string, unknown> | null | undefined
184
164
 
185
- function toIso(value: unknown): string | null {
186
- if (!value) return null
187
- const dt = value instanceof Date ? value : new Date(String(value))
188
- if (Number.isNaN(dt.getTime())) return null
189
- return dt.toISOString()
190
- }
191
-
192
165
  const getPersonTool: CustomersAiToolDefinition = {
193
166
  name: 'customers.get_person',
194
167
  displayName: 'Get person',
@@ -226,104 +199,9 @@ const getPersonTool: CustomersAiToolDefinition = {
226
199
  const profileRow = (data.profile ?? null) as ApiPersonDetailRow
227
200
  const customFields = (data.customFields ?? {}) as Record<string, unknown>
228
201
 
229
- let related: Record<string, unknown> | null = null
202
+ let related: CustomerRelatedRecords | null = null
230
203
  if (includeRelated) {
231
- const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []
232
- const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []
233
- const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []
234
- const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []
235
- const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []
236
- const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []
237
- const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []
238
- related = {
239
- addresses: addresses.map((address) => ({
240
- id: address.id,
241
- name: address.name ?? null,
242
- purpose: address.purpose ?? null,
243
- addressLine1: address.addressLine1 ?? null,
244
- addressLine2: address.addressLine2 ?? null,
245
- city: address.city ?? null,
246
- region: address.region ?? null,
247
- postalCode: address.postalCode ?? null,
248
- country: address.country ?? null,
249
- isPrimary: !!address.isPrimary,
250
- })),
251
- activities: activities.map((activity) => ({
252
- id: activity.id,
253
- activityType: activity.activityType,
254
- subject: activity.subject ?? null,
255
- body: activity.body ?? null,
256
- occurredAt: toIso(activity.occurredAt),
257
- createdAt: toIso(activity.createdAt),
258
- })),
259
- notes: notes.map((comment) => ({
260
- id: comment.id,
261
- body: comment.body,
262
- authorUserId: comment.authorUserId ?? null,
263
- createdAt: toIso(comment.createdAt),
264
- })),
265
- tasks: todos.map((task) => ({
266
- id: task.id,
267
- todoId: task.todoId ?? task.id,
268
- todoSource: task.todoSource ?? null,
269
- createdAt: toIso(task.createdAt),
270
- })),
271
- interactions: interactions.map((interaction) => ({
272
- id: interaction.id,
273
- interactionType: interaction.interactionType,
274
- title: interaction.title ?? null,
275
- status: interaction.status,
276
- scheduledAt: toIso(interaction.scheduledAt),
277
- occurredAt: toIso(interaction.occurredAt),
278
- })),
279
- tags: tagsRows
280
- .map((tag) => {
281
- if (!tag || typeof tag !== 'object') return null
282
- const id = typeof tag.id === 'string' ? tag.id : null
283
- const label = typeof tag.label === 'string' ? tag.label : null
284
- if (!id || !label) return null
285
- const slug = typeof tag.slug === 'string' ? tag.slug : label
286
- const color = typeof tag.color === 'string' ? tag.color : null
287
- return { id, slug, label, color }
288
- })
289
- .filter(
290
- (entry): entry is { id: string; slug: string; label: string; color: string | null } =>
291
- entry !== null,
292
- ),
293
- deals: dealsRows
294
- .map((deal) => {
295
- if (!deal || typeof deal !== 'object') return null
296
- const id = typeof deal.id === 'string' ? deal.id : null
297
- if (!id) return null
298
- return {
299
- id,
300
- title: typeof deal.title === 'string' ? deal.title : '',
301
- status: typeof deal.status === 'string' ? deal.status : null,
302
- pipelineStageId:
303
- typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
304
- valueAmount:
305
- typeof deal.valueAmount === 'string'
306
- ? deal.valueAmount
307
- : deal.valueAmount === null || deal.valueAmount === undefined
308
- ? null
309
- : String(deal.valueAmount),
310
- valueCurrency:
311
- typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
312
- }
313
- })
314
- .filter(
315
- (
316
- value,
317
- ): value is {
318
- id: string
319
- title: string
320
- status: string | null
321
- pipelineStageId: string | null
322
- valueAmount: string | null
323
- valueCurrency: string | null
324
- } => value !== null,
325
- ),
326
- }
204
+ related = buildRelatedRecords(data)
327
205
  }
328
206
 
329
207
  return {
@@ -14,7 +14,10 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
14
14
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
15
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
16
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
17
- import type { CustomerDictionaryKind } from '../lib/dictionaries'
17
+ import {
18
+ getCustomerDictionarySettingsSectionId,
19
+ type CustomerDictionaryKind,
20
+ } from '../lib/dictionaries'
18
21
  import { ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
19
22
  import {
20
23
  DictionaryForm,
@@ -47,6 +50,19 @@ const DEFAULT_FORM_VALUES: DictionaryFormValues = {
47
50
  icon: null,
48
51
  }
49
52
 
53
+ const DICTIONARY_HASH_SCROLL_RETRY_DELAYS_MS = [100, 350, 1000] as const
54
+
55
+ function getCurrentDictionaryHashTarget(): HTMLElement | null {
56
+ if (typeof window === 'undefined') return null
57
+ const hash = window.location.hash
58
+ if (!hash.startsWith('#customer-dictionary-')) return null
59
+ try {
60
+ return document.getElementById(decodeURIComponent(hash.slice(1)))
61
+ } catch {
62
+ return document.getElementById(hash.slice(1))
63
+ }
64
+ }
65
+
50
66
  export default function DictionarySettings() {
51
67
  const t = useT()
52
68
 
@@ -98,6 +114,45 @@ export default function DictionarySettings() {
98
114
  },
99
115
  ], [t])
100
116
 
117
+ React.useEffect(() => {
118
+ const timeoutIds: number[] = []
119
+ let frameId: number | null = null
120
+
121
+ const clearScheduledScrolls = () => {
122
+ if (frameId !== null) {
123
+ window.cancelAnimationFrame(frameId)
124
+ frameId = null
125
+ }
126
+ while (timeoutIds.length > 0) {
127
+ const timeoutId = timeoutIds.pop()
128
+ if (timeoutId !== undefined) window.clearTimeout(timeoutId)
129
+ }
130
+ }
131
+
132
+ const scrollToHashTarget = () => {
133
+ getCurrentDictionaryHashTarget()?.scrollIntoView({ block: 'start' })
134
+ }
135
+
136
+ const scheduleHashScroll = () => {
137
+ clearScheduledScrolls()
138
+ if (!window.location.hash.startsWith('#customer-dictionary-')) return
139
+ scrollToHashTarget()
140
+ if (typeof window.requestAnimationFrame === 'function') {
141
+ frameId = window.requestAnimationFrame(scrollToHashTarget)
142
+ }
143
+ DICTIONARY_HASH_SCROLL_RETRY_DELAYS_MS.forEach((delay) => {
144
+ timeoutIds.push(window.setTimeout(scrollToHashTarget, delay))
145
+ })
146
+ }
147
+
148
+ scheduleHashScroll()
149
+ window.addEventListener('hashchange', scheduleHashScroll)
150
+ return () => {
151
+ window.removeEventListener('hashchange', scheduleHashScroll)
152
+ clearScheduledScrolls()
153
+ }
154
+ }, [])
155
+
101
156
  return (
102
157
  <div className="space-y-8">
103
158
  <header className="space-y-2">
@@ -393,7 +448,10 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
393
448
  }), [dialog, t])
394
449
 
395
450
  return (
396
- <section className="rounded border bg-card text-card-foreground shadow-sm">
451
+ <section
452
+ id={getCustomerDictionarySettingsSectionId(kind)}
453
+ className="scroll-mt-24 rounded border bg-card text-card-foreground shadow-sm"
454
+ >
397
455
  <div className="border-b px-6 py-4 space-y-1">
398
456
  <h2 className="text-lg font-medium">{title}</h2>
399
457
  <p className="text-sm text-muted-foreground">{description}</p>
@@ -499,6 +499,7 @@ export function CalendarScreen() {
499
499
  showWeekends={preferences.showWeekends}
500
500
  showConflicts={preferences.conflictWarnings}
501
501
  aiSummaries={preferences.aiSummaries}
502
+ canManage={canManage}
502
503
  highlightItemId={highlightItemId}
503
504
  onItemClick={openEditEditor}
504
505
  onJoin={handleJoin}
@@ -5,6 +5,7 @@ import { Sparkles } from 'lucide-react'
5
5
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import { Button } from '@open-mercato/ui/primitives/button'
7
7
  import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
8
+ import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
8
9
  import { formatTimeRange } from './EventBlock'
9
10
  import type { CalendarItem, CalendarPlatform } from './types'
10
11
 
@@ -20,6 +21,7 @@ export type EventPeekPopoverProps = {
20
21
  open: boolean
21
22
  joinUrl: string | null
22
23
  aiSummaries: boolean
24
+ canManage?: boolean
23
25
  onOpenChange(open: boolean): void
24
26
  onJoin(item: CalendarItem): void
25
27
  onEdit(item: CalendarItem): void
@@ -31,6 +33,7 @@ export function EventPeekPopover({
31
33
  open,
32
34
  joinUrl,
33
35
  aiSummaries,
36
+ canManage = true,
34
37
  onOpenChange,
35
38
  onJoin,
36
39
  onEdit,
@@ -81,17 +84,32 @@ export function EventPeekPopover({
81
84
  {t('customers.calendar.peek.join', 'Join')}
82
85
  </Button>
83
86
  ) : null}
84
- <Button
85
- type="button"
86
- size="sm"
87
- variant="secondary"
88
- onClick={() => {
89
- onOpenChange(false)
90
- onEdit(item)
91
- }}
92
- >
93
- {t('customers.calendar.peek.edit', 'Edit')}
94
- </Button>
87
+ {canManage ? (
88
+ <Button
89
+ type="button"
90
+ size="sm"
91
+ variant="secondary"
92
+ onClick={() => {
93
+ onOpenChange(false)
94
+ onEdit(item)
95
+ }}
96
+ >
97
+ {t('customers.calendar.peek.edit', 'Edit')}
98
+ </Button>
99
+ ) : (
100
+ <SimpleTooltip
101
+ content={t(
102
+ 'customers.calendar.peek.editForbidden',
103
+ "You don't have permission to edit events",
104
+ )}
105
+ >
106
+ <span className="inline-flex">
107
+ <Button type="button" size="sm" variant="secondary" disabled className="pointer-events-none">
108
+ {t('customers.calendar.peek.edit', 'Edit')}
109
+ </Button>
110
+ </span>
111
+ </SimpleTooltip>
112
+ )}
95
113
  </div>
96
114
  </div>
97
115
  </PopoverContent>
@@ -160,6 +160,7 @@ export function TimeGrid({
160
160
  showWeekends,
161
161
  showConflicts,
162
162
  aiSummaries,
163
+ canManage = true,
163
164
  highlightItemId,
164
165
  onItemClick,
165
166
  onJoin,
@@ -351,6 +352,7 @@ export function TimeGrid({
351
352
  open={selectedId === item.id}
352
353
  joinUrl={resolveJoinUrl(item.location)}
353
354
  aiSummaries={aiSummaries}
355
+ canManage={canManage}
354
356
  onOpenChange={(open) => setSelectedId(open ? item.id : null)}
355
357
  onJoin={onJoin}
356
358
  onEdit={onItemClick}
@@ -462,6 +464,7 @@ export function TimeGrid({
462
464
  open={selectedId === block.item.id}
463
465
  joinUrl={resolveJoinUrl(block.item.location)}
464
466
  aiSummaries={aiSummaries}
467
+ canManage={canManage}
465
468
  onOpenChange={(open) => setSelectedId(open ? block.item.id : null)}
466
469
  onJoin={onJoin}
467
470
  onEdit={onItemClick}
@@ -87,6 +87,7 @@ export interface TimeGridProps {
87
87
  showWeekends: boolean
88
88
  showConflicts: boolean
89
89
  aiSummaries: boolean
90
+ canManage?: boolean
90
91
  highlightItemId?: string | null
91
92
  onItemClick(item: CalendarItem): void
92
93
  onJoin(item: CalendarItem): void
@@ -51,7 +51,11 @@ import {
51
51
  ensureCustomerDictionary,
52
52
  invalidateCustomerDictionary,
53
53
  } from './detail/hooks/useCustomerDictionary'
54
- import type { CustomerDictionaryKind } from '../lib/dictionaries'
54
+ import {
55
+ CUSTOMER_DICTIONARIES_MANAGE_HREF,
56
+ getCustomerDictionaryManageHref,
57
+ type CustomerDictionaryKind,
58
+ } from '../lib/dictionaries'
55
59
  import { normalizeCustomFieldSubmitValue } from './detail/customFieldUtils'
56
60
  import { CUSTOMER_PHONE_INVALID_MESSAGE_KEY } from '../data/validators'
57
61
 
@@ -116,6 +120,8 @@ type DictionarySelectFieldProps = {
116
120
  showActiveAppearance?: boolean
117
121
  }
118
122
 
123
+ export { CUSTOMER_DICTIONARIES_MANAGE_HREF, getCustomerDictionaryManageHref }
124
+
119
125
  const emailValidationSchema = z.string().email()
120
126
  const EMAIL_CHECK_DEBOUNCE_MS = 350
121
127
 
@@ -233,7 +239,7 @@ export function DictionarySelectField({
233
239
  fetchOptions={fetchOptions}
234
240
  createOption={createOption}
235
241
  labels={labels}
236
- manageHref={manageHref}
242
+ manageHref={manageHref ?? getCustomerDictionaryManageHref(kind)}
237
243
  selectClassName={selectClassName}
238
244
  allowInlineCreate={allowInlineCreate}
239
245
  allowAppearance={allowAppearance}
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 Teilnehmer",
313
313
  "customers.calendar.peek.attendees": "{count} Teilnehmer",
314
314
  "customers.calendar.peek.edit": "Bearbeiten",
315
+ "customers.calendar.peek.editForbidden": "Sie haben keine Berechtigung, Termine zu bearbeiten",
315
316
  "customers.calendar.peek.join": "Beitreten",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 attendee",
313
313
  "customers.calendar.peek.attendees": "{count} attendees",
314
314
  "customers.calendar.peek.edit": "Edit",
315
+ "customers.calendar.peek.editForbidden": "You don't have permission to edit events",
315
316
  "customers.calendar.peek.join": "Join",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 asistente",
313
313
  "customers.calendar.peek.attendees": "{count} asistentes",
314
314
  "customers.calendar.peek.edit": "Editar",
315
+ "customers.calendar.peek.editForbidden": "No tienes permiso para editar eventos",
315
316
  "customers.calendar.peek.join": "Unirse",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -312,6 +312,7 @@
312
312
  "customers.calendar.peek.attendee": "1 uczestnik",
313
313
  "customers.calendar.peek.attendees": "{count} uczestników",
314
314
  "customers.calendar.peek.edit": "Edytuj",
315
+ "customers.calendar.peek.editForbidden": "Nie masz uprawnień do edycji wydarzeń",
315
316
  "customers.calendar.peek.join": "Dołącz",
316
317
  "customers.calendar.platform.meet": "Meet",
317
318
  "customers.calendar.platform.slack": "Slack",
@@ -30,6 +30,16 @@ export type CustomerDictionaryKind = typeof CUSTOMER_DICTIONARY_KINDS[number]
30
30
  export type CustomerDictionaryDisplayEntry = DictionaryDisplayEntry
31
31
  export type CustomerDictionaryMap = DictionaryMap
32
32
 
33
+ export const CUSTOMER_DICTIONARIES_MANAGE_HREF = '/backend/config/customers'
34
+
35
+ export function getCustomerDictionarySettingsSectionId(kind: CustomerDictionaryKind) {
36
+ return `customer-dictionary-${kind}`
37
+ }
38
+
39
+ export function getCustomerDictionaryManageHref(kind: CustomerDictionaryKind) {
40
+ return `${CUSTOMER_DICTIONARIES_MANAGE_HREF}#${getCustomerDictionarySettingsSectionId(kind)}`
41
+ }
42
+
33
43
  export function createEmptyCustomerDictionaryMaps(): Record<CustomerDictionaryKind, CustomerDictionaryMap> {
34
44
  return CUSTOMER_DICTIONARY_KINDS.reduce<Record<CustomerDictionaryKind, CustomerDictionaryMap>>(
35
45
  (acc, kind) => {