@open-mercato/core 0.4.11-develop.1309.4b37381a7a → 0.4.11-develop.1347.c693e6dfee

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 (60) hide show
  1. package/dist/modules/customers/api/companies/[id]/route.js +3 -2
  2. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  3. package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js +59 -91
  4. package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js.map +2 -2
  5. package/dist/modules/customers/api/interactions/tasks/route.js +115 -0
  6. package/dist/modules/customers/api/interactions/tasks/route.js.map +7 -0
  7. package/dist/modules/customers/api/people/[id]/route.js +3 -2
  8. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  9. package/dist/modules/customers/api/todos/route.js +14 -134
  10. package/dist/modules/customers/api/todos/route.js.map +2 -2
  11. package/dist/modules/customers/backend/customer-tasks/page.js +10 -0
  12. package/dist/modules/customers/backend/customer-tasks/page.js.map +7 -0
  13. package/dist/modules/customers/backend/customer-tasks/page.meta.js +25 -0
  14. package/dist/modules/customers/backend/customer-tasks/page.meta.js.map +7 -0
  15. package/dist/modules/customers/commands/interactions.js +40 -4
  16. package/dist/modules/customers/commands/interactions.js.map +2 -2
  17. package/dist/modules/customers/components/CustomerTodosTable.js +77 -47
  18. package/dist/modules/customers/components/CustomerTodosTable.js.map +2 -2
  19. package/dist/modules/customers/components/detail/TasksSection.js +4 -4
  20. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  21. package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +2 -1
  22. package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
  23. package/dist/modules/customers/components/detail/utils.js +3 -0
  24. package/dist/modules/customers/components/detail/utils.js.map +2 -2
  25. package/dist/modules/customers/data/entities.js +2 -2
  26. package/dist/modules/customers/data/entities.js.map +2 -2
  27. package/dist/modules/customers/data/validators.js +2 -2
  28. package/dist/modules/customers/data/validators.js.map +2 -2
  29. package/dist/modules/customers/lib/interactionCompatibility.js +12 -2
  30. package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
  31. package/dist/modules/customers/lib/todoCompatibility.js +167 -4
  32. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  33. package/dist/modules/customers/migrations/Migration20260401172819.js +45 -0
  34. package/dist/modules/customers/migrations/Migration20260401172819.js.map +7 -0
  35. package/dist/modules/customers/search.js +3 -2
  36. package/dist/modules/customers/search.js.map +2 -2
  37. package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +10 -2
  38. package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +2 -2
  39. package/package.json +3 -3
  40. package/src/modules/customers/api/companies/[id]/route.ts +6 -5
  41. package/src/modules/customers/api/dashboard/widgets/customer-todos/route.ts +69 -126
  42. package/src/modules/customers/api/interactions/tasks/route.ts +122 -0
  43. package/src/modules/customers/api/people/[id]/route.ts +3 -2
  44. package/src/modules/customers/api/todos/route.ts +13 -181
  45. package/src/modules/customers/backend/customer-tasks/page.meta.ts +23 -0
  46. package/src/modules/customers/backend/customer-tasks/page.tsx +12 -0
  47. package/src/modules/customers/commands/interactions.ts +50 -2
  48. package/src/modules/customers/components/CustomerTodosTable.tsx +91 -66
  49. package/src/modules/customers/components/detail/TasksSection.tsx +8 -8
  50. package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +2 -1
  51. package/src/modules/customers/components/detail/types.ts +6 -0
  52. package/src/modules/customers/components/detail/utils.ts +3 -0
  53. package/src/modules/customers/data/entities.ts +2 -2
  54. package/src/modules/customers/data/validators.ts +2 -2
  55. package/src/modules/customers/lib/interactionCompatibility.ts +16 -0
  56. package/src/modules/customers/lib/todoCompatibility.ts +229 -10
  57. package/src/modules/customers/migrations/.snapshot-open-mercato.json +1 -1
  58. package/src/modules/customers/migrations/Migration20260401172819.ts +45 -0
  59. package/src/modules/customers/search.ts +3 -2
  60. package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +24 -23
@@ -53,6 +53,26 @@ const interactionCrudEvents: CrudEventsConfig = {
53
53
  id: ctx.identifiers.id,
54
54
  organizationId: ctx.identifiers.organizationId,
55
55
  tenantId: ctx.identifiers.tenantId,
56
+ entityId:
57
+ ctx.entity && typeof ctx.entity === 'object' && 'entity' in (ctx.entity as Record<string, unknown>)
58
+ ? (() => {
59
+ const entityRef = (ctx.entity as CustomerInteraction).entity
60
+ return typeof entityRef === 'string' ? entityRef : entityRef?.id ?? null
61
+ })()
62
+ : null,
63
+ interactionType:
64
+ ctx.entity && typeof ctx.entity === 'object' && 'interactionType' in (ctx.entity as Record<string, unknown>)
65
+ ? (ctx.entity as CustomerInteraction).interactionType
66
+ : null,
67
+ status:
68
+ ctx.entity && typeof ctx.entity === 'object' && 'status' in (ctx.entity as Record<string, unknown>)
69
+ ? (ctx.entity as CustomerInteraction).status
70
+ : null,
71
+ source:
72
+ ctx.entity && typeof ctx.entity === 'object' && 'source' in (ctx.entity as Record<string, unknown>)
73
+ ? (ctx.entity as CustomerInteraction).source ?? null
74
+ : null,
75
+ ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),
56
76
  }),
57
77
  }
58
78
 
@@ -167,8 +187,12 @@ async function emitInteractionRevertedEvent(
167
187
  id: interaction.id,
168
188
  organizationId: interaction.organizationId,
169
189
  tenantId: interaction.tenantId,
190
+ entityId: interaction.entityId,
191
+ interactionType: interaction.interactionType,
192
+ source: interaction.source ?? null,
170
193
  status: interaction.status,
171
194
  occurredAt: interaction.occurredAt?.toISOString() ?? null,
195
+ ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),
172
196
  })
173
197
  }
174
198
 
@@ -287,6 +311,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
287
311
  organizationId: interaction.organizationId,
288
312
  tenantId: interaction.tenantId,
289
313
  },
314
+ syncOrigin: ctx.syncOrigin,
290
315
  indexer: interactionCrudIndexer,
291
316
  events: interactionCrudEvents,
292
317
  })
@@ -413,6 +438,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
413
438
  organizationId: interaction.organizationId,
414
439
  tenantId: interaction.tenantId,
415
440
  },
441
+ syncOrigin: ctx.syncOrigin,
416
442
  indexer: interactionCrudIndexer,
417
443
  events: interactionCrudEvents,
418
444
  })
@@ -532,6 +558,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
532
558
  organizationId: interaction.organizationId,
533
559
  tenantId: interaction.tenantId,
534
560
  },
561
+ syncOrigin: ctx.syncOrigin,
535
562
  indexer: interactionCrudIndexer,
536
563
  events: interactionCrudEvents,
537
564
  })
@@ -587,10 +614,19 @@ const completeInteractionCommand: CommandHandler<InteractionCompleteInput, { int
587
614
  action: 'updated',
588
615
  entity: interaction,
589
616
  identifiers,
617
+ syncOrigin: ctx.syncOrigin,
590
618
  indexer: interactionCrudIndexer,
591
619
  events: interactionCrudEvents,
592
620
  })
593
- await emitLifecycleEvent(ctx, 'customers.interaction.completed', identifiers)
621
+ await emitLifecycleEvent(ctx, 'customers.interaction.completed', {
622
+ ...identifiers,
623
+ entityId,
624
+ interactionType: interaction.interactionType,
625
+ status: interaction.status,
626
+ source: interaction.source ?? null,
627
+ occurredAt: interaction.occurredAt?.toISOString() ?? null,
628
+ ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),
629
+ })
594
630
  await emitNextInteractionUpdatedEvent(ctx, { entityId, nextInteractionId }, identifiers)
595
631
 
596
632
  return { interactionId: interaction.id }
@@ -653,6 +689,7 @@ const completeInteractionCommand: CommandHandler<InteractionCompleteInput, { int
653
689
  organizationId: result.interaction.organizationId,
654
690
  tenantId: result.interaction.tenantId,
655
691
  },
692
+ syncOrigin: ctx.syncOrigin,
656
693
  indexer: interactionCrudIndexer,
657
694
  events: interactionCrudEvents,
658
695
  })
@@ -708,10 +745,18 @@ const cancelInteractionCommand: CommandHandler<InteractionCancelInput, { interac
708
745
  action: 'updated',
709
746
  entity: interaction,
710
747
  identifiers,
748
+ syncOrigin: ctx.syncOrigin,
711
749
  indexer: interactionCrudIndexer,
712
750
  events: interactionCrudEvents,
713
751
  })
714
- await emitLifecycleEvent(ctx, 'customers.interaction.canceled', identifiers)
752
+ await emitLifecycleEvent(ctx, 'customers.interaction.canceled', {
753
+ ...identifiers,
754
+ entityId,
755
+ interactionType: interaction.interactionType,
756
+ status: interaction.status,
757
+ source: interaction.source ?? null,
758
+ ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),
759
+ })
715
760
  await emitNextInteractionUpdatedEvent(ctx, { entityId, nextInteractionId }, identifiers)
716
761
 
717
762
  return { interactionId: interaction.id }
@@ -773,6 +818,7 @@ const cancelInteractionCommand: CommandHandler<InteractionCancelInput, { interac
773
818
  organizationId: result.interaction.organizationId,
774
819
  tenantId: result.interaction.tenantId,
775
820
  },
821
+ syncOrigin: ctx.syncOrigin,
776
822
  indexer: interactionCrudIndexer,
777
823
  events: interactionCrudEvents,
778
824
  })
@@ -828,6 +874,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
828
874
  organizationId: interaction.organizationId,
829
875
  tenantId: interaction.tenantId,
830
876
  },
877
+ syncOrigin: ctx.syncOrigin,
831
878
  indexer: interactionCrudIndexer,
832
879
  events: interactionCrudEvents,
833
880
  })
@@ -939,6 +986,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
939
986
  organizationId: interaction.organizationId,
940
987
  tenantId: interaction.tenantId,
941
988
  },
989
+ syncOrigin: ctx.syncOrigin,
942
990
  indexer: interactionCrudIndexer,
943
991
  events: interactionCrudEvents,
944
992
  })
@@ -11,10 +11,10 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
11
11
  import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
12
12
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
13
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
14
- import { buildCrudExportUrl } from '@open-mercato/ui/backend/utils/crud'
15
14
  import { Button } from '@open-mercato/ui/primitives/button'
16
15
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
17
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
17
+ import { resolveTodoHref } from './detail/utils'
18
18
 
19
19
  type CustomerTodoItem = {
20
20
  id: string
@@ -31,6 +31,8 @@ type CustomerTodoItem = {
31
31
  organizationId: string
32
32
  tenantId: string
33
33
  createdAt: string
34
+ externalHref?: string | null
35
+ _integrations?: Record<string, unknown>
34
36
  customer: {
35
37
  id: string | null
36
38
  displayName: string | null
@@ -47,6 +49,7 @@ type CustomerTodosResponse = {
47
49
  }
48
50
 
49
51
  const TASKS_TAB_QUERY = 'tab=tasks'
52
+ const CUSTOMER_TASKS_API_PATH = '/api/customers/interactions/tasks'
50
53
 
51
54
  function buildCustomerHref(item: CustomerTodoItem): string | null {
52
55
  const customerId = item.customer?.id
@@ -59,25 +62,31 @@ function buildCustomerHref(item: CustomerTodoItem): string | null {
59
62
  return `${base}?${TASKS_TAB_QUERY}`
60
63
  }
61
64
 
62
- // SPEC-046b: To enable canonical interactions mode, switch this table to
63
- // /api/customers/interactions?status=planned&pageSize=N&page=N&search=...
64
- //
65
- // Column mapping (InteractionSummary → CustomerTodoItem shape):
66
- // interaction.id → todoId (also use as id)
67
- // interaction.title → todoTitle
68
- // interaction.status → todoIsDone: status === 'done'
69
- // interaction.entityId → requires a separate lookup or API enricher
70
- // to resolve customer displayName/kind
71
- //
72
- // The main gap is the `customer` sub-object: the interactions API does not
73
- // embed customer details. Options:
74
- // a) Add a response enricher to the interactions API that populates
75
- // customer displayName/kind from entityId
76
- // b) Fetch customer details client-side for visible rows
77
- // c) Add an /api/customers/interactions/table endpoint that joins entities
78
- //
79
- // Export config would switch from 'customers/todos' to 'customers/interactions'
80
- // with exportScope: 'full' and an adjusted column mapping.
65
+ function buildCustomerTasksQueryString(input: {
66
+ page: number
67
+ pageSize: number
68
+ search: string
69
+ all?: boolean
70
+ }): string {
71
+ const usp = new URLSearchParams({
72
+ page: String(input.page),
73
+ pageSize: String(input.pageSize),
74
+ })
75
+ if (input.search.trim().length > 0) usp.set('search', input.search.trim())
76
+ if (input.all) usp.set('all', 'true')
77
+ return usp.toString()
78
+ }
79
+
80
+ function readValueAtPath(record: Record<string, unknown>, path: string): unknown {
81
+ const segments = path.split('.').filter((segment) => segment.length > 0)
82
+ let current: unknown = record
83
+ for (const segment of segments) {
84
+ if (!current || typeof current !== 'object') return null
85
+ current = (current as Record<string, unknown>)[segment]
86
+ }
87
+ return current ?? null
88
+ }
89
+
81
90
  export function CustomerTodosTable(): React.JSX.Element {
82
91
  const t = useT()
83
92
  const router = useRouter()
@@ -87,14 +96,11 @@ export function CustomerTodosTable(): React.JSX.Element {
87
96
  const [page, setPage] = React.useState(1)
88
97
  const [pageSize] = React.useState(50)
89
98
 
90
- const params = React.useMemo(() => {
91
- const usp = new URLSearchParams({
92
- page: String(page),
93
- pageSize: String(pageSize),
94
- })
95
- if (search.trim().length > 0) usp.set('search', search.trim())
96
- return usp.toString()
97
- }, [page, pageSize, search])
99
+ const params = React.useMemo(() => buildCustomerTasksQueryString({
100
+ page,
101
+ pageSize,
102
+ search,
103
+ }), [page, pageSize, search])
98
104
 
99
105
  const columns = React.useMemo<ColumnDef<CustomerTodoItem>[]>(() => [
100
106
  {
@@ -118,10 +124,10 @@ export function CustomerTodosTable(): React.JSX.Element {
118
124
  header: t('customers.workPlan.customerTodos.table.column.todo'),
119
125
  cell: ({ row }) => {
120
126
  const title = row.original.todoTitle ?? t('customers.workPlan.customerTodos.table.column.todo.unnamed')
121
- const todoId = row.original.todoId
122
- if (!todoId) return <span className="text-muted-foreground">{title}</span>
127
+ const todoHref = row.original.externalHref ?? resolveTodoHref(row.original.todoSource, row.original.todoId)
128
+ if (!todoHref) return <span className="text-muted-foreground">{title}</span>
123
129
  return (
124
- <Link href={`/backend/todos/${todoId}/edit`} className="underline-offset-2 hover:underline">
130
+ <Link href={todoHref} className="underline-offset-2 hover:underline">
125
131
  {title}
126
132
  </Link>
127
133
  )
@@ -150,15 +156,30 @@ export function CustomerTodosTable(): React.JSX.Element {
150
156
  .filter((col): col is { field: string; header: string } => !!col)
151
157
  }, [columns])
152
158
 
153
- const { data, isLoading, error, refetch, isFetching } = useQuery<CustomerTodosResponse>({
154
- queryKey: ['customers-todos', params, scopeVersion],
155
- queryFn: async () => {
156
- return readApiResultOrThrow<CustomerTodosResponse>(
157
- `/api/customers/todos?${params}`,
158
- undefined,
159
- { errorMessage: t('customers.workPlan.customerTodos.table.error.load') },
159
+ const buildPreparedExport = React.useCallback((
160
+ exportRows: CustomerTodoItem[],
161
+ exportColumns: Array<{ field: string; header: string }>,
162
+ ): PreparedExport => ({
163
+ columns: exportColumns.map((col) => ({ field: col.field, header: col.header })),
164
+ rows: exportRows.map((row) => {
165
+ const record = row as Record<string, unknown>
166
+ return Object.fromEntries(
167
+ exportColumns.map((col) => [col.field, readValueAtPath(record, col.field)]),
160
168
  )
161
- },
169
+ }),
170
+ }), [])
171
+
172
+ const fetchTasks = React.useCallback(async (queryString: string): Promise<CustomerTodosResponse> => {
173
+ return readApiResultOrThrow<CustomerTodosResponse>(
174
+ `${CUSTOMER_TASKS_API_PATH}?${queryString}`,
175
+ undefined,
176
+ { errorMessage: t('customers.workPlan.customerTodos.table.error.load') },
177
+ )
178
+ }, [t])
179
+
180
+ const { data, isLoading, error, refetch, isFetching } = useQuery<CustomerTodosResponse>({
181
+ queryKey: ['customers-interactions-tasks', params, scopeVersion],
182
+ queryFn: async () => fetchTasks(params),
162
183
  placeholderData: keepPreviousData,
163
184
  })
164
185
 
@@ -168,27 +189,28 @@ export function CustomerTodosTable(): React.JSX.Element {
168
189
  view: {
169
190
  description: t('customers.workPlan.customerTodos.table.export.view'),
170
191
  prepare: async (): Promise<{ prepared: PreparedExport; filename: string }> => {
171
- const rowsForExport = rows.map((row) => {
172
- const out: Record<string, unknown> = {}
173
- for (const col of viewExportColumns) {
174
- out[col.field] = (row as Record<string, unknown>)[col.field]
175
- }
176
- return out
177
- })
178
- const prepared: PreparedExport = {
179
- columns: viewExportColumns.map((col) => ({ field: col.field, header: col.header })),
180
- rows: rowsForExport,
192
+ return {
193
+ prepared: buildPreparedExport(rows, viewExportColumns),
194
+ filename: 'customer_todos_view',
181
195
  }
182
- return { prepared, filename: 'customer_todos_view' }
183
196
  },
184
197
  },
185
198
  full: {
186
199
  description: t('customers.workPlan.customerTodos.table.export.full'),
187
- getUrl: (format: DataTableExportFormat) =>
188
- buildCrudExportUrl('customers/todos', { exportScope: 'full', all: 'true' }, format),
189
- filename: () => 'customer_todos_full',
200
+ prepare: async (_format: DataTableExportFormat): Promise<{ prepared: PreparedExport; filename: string }> => {
201
+ const fullData = await fetchTasks(buildCustomerTasksQueryString({
202
+ page: 1,
203
+ pageSize,
204
+ search,
205
+ all: true,
206
+ }))
207
+ return {
208
+ prepared: buildPreparedExport(fullData.items, viewExportColumns),
209
+ filename: 'customer_todos_full',
210
+ }
211
+ },
190
212
  },
191
- }), [rows, t, viewExportColumns])
213
+ }), [buildPreparedExport, fetchTasks, pageSize, rows, search, t, viewExportColumns])
192
214
 
193
215
  const handleRefresh = React.useCallback(async () => {
194
216
  try {
@@ -234,18 +256,21 @@ export function CustomerTodosTable(): React.JSX.Element {
234
256
  perspective={{ tableId: 'customers.todos.list' }}
235
257
  rowActions={(row) => {
236
258
  const customerLink = buildCustomerHref(row)
237
- if (!customerLink) return null
238
- return (
239
- <RowActions
240
- items={[
241
- {
242
- id: 'open-customer',
243
- label: t('customers.workPlan.customerTodos.table.actions.openCustomer'),
244
- href: customerLink,
245
- },
246
- ]}
247
- />
248
- )
259
+ const todoHref = row.externalHref ?? resolveTodoHref(row.todoSource, row.todoId)
260
+ const items = [
261
+ customerLink ? {
262
+ id: 'open-customer',
263
+ label: t('customers.workPlan.customerTodos.table.actions.openCustomer'),
264
+ href: customerLink,
265
+ } : null,
266
+ todoHref ? {
267
+ id: 'open-task',
268
+ label: t('customers.workPlan.customerTodos.table.actions.openTask'),
269
+ href: todoHref,
270
+ } : null,
271
+ ].filter((item): item is { id: string; label: string; href: string } => !!item)
272
+ if (!items.length) return null
273
+ return <RowActions items={items} />
249
274
  }}
250
275
  onRowClick={handleNavigate}
251
276
  pagination={{
@@ -449,7 +449,7 @@ export function TasksSection({
449
449
  </div>
450
450
  ) : null}
451
451
  {sortedTasks.map((task) => {
452
- const todoHref = resolveTodoHref(task.todoSource, task.todoId)
452
+ const todoHref = task.externalHref ?? resolveTodoHref(task.todoSource, task.todoId)
453
453
  const createdLabel = formatDateTime(task.createdAt) ?? emptyLabel
454
454
  const meta = renderTaskMeta(task)
455
455
  const title = task.title ?? t('customers.people.detail.tasks.untitled', 'Untitled task')
@@ -570,15 +570,15 @@ export function TasksSection({
570
570
  {t('customers.people.detail.tasks.loadingMore', 'Loading…')}
571
571
  </div>
572
572
  ) : null}
573
- <div className="flex justify-center">
574
- <Button asChild variant="outline" size="sm">
575
- <Link href="/backend/customer-tasks">
576
- {t('customers.people.detail.tasks.viewAll', 'View all tasks')}
577
- </Link>
578
- </Button>
579
- </div>
580
573
  </div>
581
574
  ) : null}
575
+ <div className="flex justify-center">
576
+ <Button asChild variant="outline" size="sm">
577
+ <Link href="/backend/customer-tasks">
578
+ {t('customers.people.detail.tasks.viewAll', 'View all tasks')}
579
+ </Link>
580
+ </Button>
581
+ </div>
582
582
  </div>
583
583
 
584
584
  <TaskDialog
@@ -6,8 +6,9 @@ import { resolveTodoApiPath } from '../utils'
6
6
  import type { TodoLinkSummary } from '../types'
7
7
  import { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'
8
8
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
9
+ import { CUSTOMER_INTERACTION_TASK_SOURCE } from '../../../lib/interactionCompatibility'
9
10
 
10
- const DEFAULT_TODO_SOURCE = 'example:todo'
11
+ const DEFAULT_TODO_SOURCE = CUSTOMER_INTERACTION_TASK_SOURCE
11
12
 
12
13
  type CustomerTodoRow = {
13
14
  id: string
@@ -109,6 +109,7 @@ export type TodoLinkSummary = {
109
109
  dueAt?: string | null
110
110
  todoOrganizationId?: string | null
111
111
  customValues?: Record<string, unknown> | null
112
+ externalHref?: string | null
112
113
  }
113
114
 
114
115
  export type InteractionSummary = {
@@ -133,6 +134,11 @@ export type InteractionSummary = {
133
134
  authorEmail?: string | null
134
135
  dealTitle?: string | null
135
136
  customValues?: Record<string, unknown> | null
137
+ customer?: {
138
+ id: string | null
139
+ displayName: string | null
140
+ kind: string | null
141
+ } | null
136
142
  _integrations?: Record<string, unknown>
137
143
  createdAt: string
138
144
  updatedAt: string
@@ -39,6 +39,9 @@ export function resolveTodoHref(source: string, todoId: string | null | undefine
39
39
  if (source === CUSTOMER_INTERACTION_TASK_SOURCE || source === CUSTOMER_INTERACTION_TASK_TYPE) return null
40
40
  const [module] = source.split(':')
41
41
  if (!module) return null
42
+ if (module === 'example') {
43
+ return `/backend/todos/${encodeURIComponent(todoId)}/edit`
44
+ }
42
45
  return `/backend/${module}/todos/${encodeURIComponent(todoId)}/edit`
43
46
  }
44
47
 
@@ -809,8 +809,8 @@ export class CustomerTodoLink {
809
809
  @Property({ name: 'todo_id', type: 'uuid' })
810
810
  todoId!: string
811
811
 
812
- @Property({ name: 'todo_source', type: 'text', default: 'example:todo' })
813
- todoSource: string = 'example:todo'
812
+ @Property({ name: 'todo_source', type: 'text', default: 'customers:interaction' })
813
+ todoSource: string = 'customers:interaction'
814
814
 
815
815
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
816
816
  createdAt: Date = new Date()
@@ -282,7 +282,7 @@ export const tagAssignmentSchema = scopedSchema.extend({
282
282
  export const todoLinkCreateSchema = scopedSchema.extend({
283
283
  entityId: uuid(),
284
284
  todoId: uuid(),
285
- todoSource: z.string().min(1).max(120).default('example:todo'),
285
+ todoSource: z.string().min(1).max(120).default('customers:interaction'),
286
286
  createdByUserId: uuid().optional(),
287
287
  })
288
288
 
@@ -291,7 +291,7 @@ export const todoLinkWithTodoCreateSchema = scopedSchema.extend({
291
291
  title: z.string().min(1).max(200),
292
292
  isDone: z.boolean().optional(),
293
293
  is_done: z.boolean().optional(),
294
- todoSource: z.string().min(1).max(120).default('example:todo'),
294
+ todoSource: z.string().min(1).max(120).default('customers:interaction'),
295
295
  createdByUserId: uuid().optional(),
296
296
  todoCustom: z.record(z.string(), z.any()).optional(),
297
297
  custom: z.record(z.string(), z.any()).optional(),
@@ -5,6 +5,7 @@ export const CUSTOMER_INTERACTION_TASK_SOURCE = 'customers:interaction'
5
5
  export const CUSTOMER_INTERACTION_TASK_TYPE = 'task'
6
6
  export const CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE = 'adapter:activity'
7
7
  export const CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE = 'adapter:todo'
8
+ export const EXAMPLE_TODO_SOURCE = 'example:todo'
8
9
 
9
10
  export type InteractionRecord = InteractionSummary & {
10
11
  authorName?: string | null
@@ -48,6 +49,7 @@ export function mapInteractionRecordToActivitySummary(interaction: InteractionRe
48
49
 
49
50
  export function mapInteractionRecordToTodoSummary(interaction: InteractionRecord): TodoLinkSummary {
50
51
  const customValues: Record<string, unknown> = { ...(interaction.customValues ?? {}) }
52
+ const externalHref = resolveExampleIntegrationHref(interaction)
51
53
  if (interaction.priority !== undefined) customValues.priority = interaction.priority
52
54
  if (interaction.body !== undefined && customValues.description === undefined) {
53
55
  customValues.description = interaction.body ?? null
@@ -73,5 +75,19 @@ export function mapInteractionRecordToTodoSummary(interaction: InteractionRecord
73
75
  dueAt: interaction.scheduledAt ?? null,
74
76
  todoOrganizationId: null,
75
77
  customValues: Object.keys(customValues).length > 0 ? customValues : null,
78
+ externalHref,
76
79
  }
77
80
  }
81
+
82
+ type IntegrationCarrier = {
83
+ _integrations?: {
84
+ example?: { href?: string | null; syncStatus?: string | null; [key: string]: unknown }
85
+ [key: string]: unknown
86
+ }
87
+ }
88
+
89
+ export function resolveExampleIntegrationHref(item: IntegrationCarrier): string | null {
90
+ const example = item._integrations?.example
91
+ if (!example || typeof example !== 'object') return null
92
+ return typeof example.href === 'string' && example.href.trim().length > 0 ? example.href : null
93
+ }