@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.
- package/dist/modules/customers/api/companies/[id]/route.js +3 -2
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js +59 -91
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +115 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/todos/route.js +14 -134
- package/dist/modules/customers/api/todos/route.js.map +2 -2
- package/dist/modules/customers/backend/customer-tasks/page.js +10 -0
- package/dist/modules/customers/backend/customer-tasks/page.js.map +7 -0
- package/dist/modules/customers/backend/customer-tasks/page.meta.js +25 -0
- package/dist/modules/customers/backend/customer-tasks/page.meta.js.map +7 -0
- package/dist/modules/customers/commands/interactions.js +40 -4
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/CustomerTodosTable.js +77 -47
- package/dist/modules/customers/components/CustomerTodosTable.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +4 -4
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +2 -1
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
- package/dist/modules/customers/components/detail/utils.js +3 -0
- package/dist/modules/customers/components/detail/utils.js.map +2 -2
- package/dist/modules/customers/data/entities.js +2 -2
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/interactionCompatibility.js +12 -2
- package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +167 -4
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260401172819.js +45 -0
- package/dist/modules/customers/migrations/Migration20260401172819.js.map +7 -0
- package/dist/modules/customers/search.js +3 -2
- package/dist/modules/customers/search.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +10 -2
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/customers/api/companies/[id]/route.ts +6 -5
- package/src/modules/customers/api/dashboard/widgets/customer-todos/route.ts +69 -126
- package/src/modules/customers/api/interactions/tasks/route.ts +122 -0
- package/src/modules/customers/api/people/[id]/route.ts +3 -2
- package/src/modules/customers/api/todos/route.ts +13 -181
- package/src/modules/customers/backend/customer-tasks/page.meta.ts +23 -0
- package/src/modules/customers/backend/customer-tasks/page.tsx +12 -0
- package/src/modules/customers/commands/interactions.ts +50 -2
- package/src/modules/customers/components/CustomerTodosTable.tsx +91 -66
- package/src/modules/customers/components/detail/TasksSection.tsx +8 -8
- package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +2 -1
- package/src/modules/customers/components/detail/types.ts +6 -0
- package/src/modules/customers/components/detail/utils.ts +3 -0
- package/src/modules/customers/data/entities.ts +2 -2
- package/src/modules/customers/data/validators.ts +2 -2
- package/src/modules/customers/lib/interactionCompatibility.ts +16 -0
- package/src/modules/customers/lib/todoCompatibility.ts +229 -10
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +1 -1
- package/src/modules/customers/migrations/Migration20260401172819.ts +45 -0
- package/src/modules/customers/search.ts +3 -2
- package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +24 -23
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
1
2
|
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
2
3
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
3
4
|
import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
|
|
4
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
CustomerInteraction,
|
|
7
|
+
CustomerTodoLink,
|
|
8
|
+
} from '../data/entities'
|
|
5
9
|
import type { InteractionRecord } from './interactionCompatibility'
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CUSTOMER_INTERACTION_TASK_SOURCE,
|
|
12
|
+
EXAMPLE_TODO_SOURCE,
|
|
13
|
+
resolveExampleIntegrationHref,
|
|
14
|
+
} from './interactionCompatibility'
|
|
15
|
+
import { hydrateCanonicalInteractions, loadCustomerSummaries } from './interactionReadModel'
|
|
7
16
|
|
|
8
17
|
export type CustomerTodoRow = {
|
|
9
18
|
id: string
|
|
@@ -20,6 +29,8 @@ export type CustomerTodoRow = {
|
|
|
20
29
|
organizationId: string
|
|
21
30
|
tenantId: string
|
|
22
31
|
createdAt: string
|
|
32
|
+
externalHref?: string | null
|
|
33
|
+
_integrations?: Record<string, unknown>
|
|
23
34
|
customer: {
|
|
24
35
|
id: string | null
|
|
25
36
|
displayName: string | null
|
|
@@ -44,6 +55,29 @@ type CustomerSummary = {
|
|
|
44
55
|
kind: string | null
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
type CustomersAuthLike = {
|
|
59
|
+
tenantId: string | null
|
|
60
|
+
orgId?: string | null
|
|
61
|
+
sub?: string | null
|
|
62
|
+
userId?: string | null
|
|
63
|
+
keyId?: string | null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type CustomersContainerLike = {
|
|
67
|
+
resolve: (name: string) => unknown
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type CanonicalTodoListResult = {
|
|
71
|
+
items: CustomerTodoRow[]
|
|
72
|
+
bridgeIds: Set<string>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveLegacyTodoSource(source: string | null | undefined): string {
|
|
76
|
+
return typeof source === 'string' && source.trim().length > 0
|
|
77
|
+
? source
|
|
78
|
+
: EXAMPLE_TODO_SOURCE
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
function extractTodoTitle(record: Record<string, unknown>): string | null {
|
|
48
82
|
const candidates = ['title', 'subject', 'name', 'summary', 'text', 'description']
|
|
49
83
|
for (const key of candidates) {
|
|
@@ -88,6 +122,64 @@ function readCustomField(record: Record<string, unknown>, key: string): unknown
|
|
|
88
122
|
return undefined
|
|
89
123
|
}
|
|
90
124
|
|
|
125
|
+
export function normalizeTodoSearch(value: string | undefined): string | null {
|
|
126
|
+
if (typeof value !== 'string') return null
|
|
127
|
+
const trimmed = value.trim().toLowerCase()
|
|
128
|
+
return trimmed.length > 0 ? trimmed : null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function sortTodoRows(rows: CustomerTodoRow[]): CustomerTodoRow[] {
|
|
132
|
+
return [...rows].sort((left, right) => {
|
|
133
|
+
const leftTime = new Date(left.createdAt).getTime()
|
|
134
|
+
const rightTime = new Date(right.createdAt).getTime()
|
|
135
|
+
if (leftTime === rightTime) {
|
|
136
|
+
return right.id.localeCompare(left.id)
|
|
137
|
+
}
|
|
138
|
+
return rightTime - leftTime
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function filterTodoRows(rows: CustomerTodoRow[], search: string | null): CustomerTodoRow[] {
|
|
143
|
+
if (!search) return rows
|
|
144
|
+
return rows.filter((row) => {
|
|
145
|
+
const haystack = [
|
|
146
|
+
row.customer.displayName,
|
|
147
|
+
row.todoTitle,
|
|
148
|
+
row.todoDescription,
|
|
149
|
+
]
|
|
150
|
+
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
151
|
+
.join(' ')
|
|
152
|
+
.toLowerCase()
|
|
153
|
+
return haystack.includes(search)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function paginateTodoRows(
|
|
158
|
+
rows: CustomerTodoRow[],
|
|
159
|
+
page: number,
|
|
160
|
+
pageSize: number,
|
|
161
|
+
exportAll: boolean,
|
|
162
|
+
): { items: CustomerTodoRow[]; total: number; page: number; pageSize: number; totalPages: number } {
|
|
163
|
+
const total = rows.length
|
|
164
|
+
if (exportAll) {
|
|
165
|
+
return {
|
|
166
|
+
items: rows,
|
|
167
|
+
total,
|
|
168
|
+
page: 1,
|
|
169
|
+
pageSize: total,
|
|
170
|
+
totalPages: 1,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const start = (page - 1) * pageSize
|
|
174
|
+
return {
|
|
175
|
+
items: rows.slice(start, start + pageSize),
|
|
176
|
+
total,
|
|
177
|
+
page,
|
|
178
|
+
pageSize,
|
|
179
|
+
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
91
183
|
export async function resolveLegacyTodoDetails(
|
|
92
184
|
queryEngine: QueryEngine,
|
|
93
185
|
links: CustomerTodoLink[],
|
|
@@ -103,10 +195,7 @@ export async function resolveLegacyTodoDetails(
|
|
|
103
195
|
|
|
104
196
|
const idsBySource = new Map<string, Set<string>>()
|
|
105
197
|
for (const link of links) {
|
|
106
|
-
const source =
|
|
107
|
-
typeof link.todoSource === 'string' && link.todoSource.trim().length > 0
|
|
108
|
-
? link.todoSource
|
|
109
|
-
: 'example:todo'
|
|
198
|
+
const source = resolveLegacyTodoSource(link.todoSource)
|
|
110
199
|
const id =
|
|
111
200
|
typeof link.todoId === 'string' && link.todoId.trim().length > 0
|
|
112
201
|
? link.todoId
|
|
@@ -264,6 +353,136 @@ export async function resolveLegacyTodoDetails(
|
|
|
264
353
|
return details
|
|
265
354
|
}
|
|
266
355
|
|
|
356
|
+
export async function listLegacyTodoRows(
|
|
357
|
+
em: EntityManager,
|
|
358
|
+
queryEngine: QueryEngine,
|
|
359
|
+
tenantId: string,
|
|
360
|
+
organizationIds: string[] | null,
|
|
361
|
+
entityId: string | undefined,
|
|
362
|
+
): Promise<CustomerTodoRow[]> {
|
|
363
|
+
const where: Record<string, unknown> = { tenantId }
|
|
364
|
+
if (organizationIds && organizationIds.length > 0) {
|
|
365
|
+
where.organizationId = { $in: organizationIds }
|
|
366
|
+
}
|
|
367
|
+
if (entityId) {
|
|
368
|
+
where.entity = entityId
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const links = await em.find(CustomerTodoLink, where, {
|
|
372
|
+
populate: ['entity'],
|
|
373
|
+
orderBy: { createdAt: 'desc' },
|
|
374
|
+
})
|
|
375
|
+
const details = await resolveLegacyTodoDetails(
|
|
376
|
+
queryEngine,
|
|
377
|
+
links,
|
|
378
|
+
tenantId,
|
|
379
|
+
organizationIds ?? [],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return links.map((link) => {
|
|
383
|
+
const source = resolveLegacyTodoSource(link.todoSource)
|
|
384
|
+
return mapLegacyTodoLinkToRow(
|
|
385
|
+
link,
|
|
386
|
+
details.get(`${source}:${link.todoId}`) ?? null,
|
|
387
|
+
)
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function listCanonicalTodoRows(
|
|
392
|
+
em: EntityManager,
|
|
393
|
+
container: CustomersContainerLike,
|
|
394
|
+
auth: CustomersAuthLike,
|
|
395
|
+
selectedOrganizationId: string | null,
|
|
396
|
+
organizationIds: string[] | null,
|
|
397
|
+
options?: {
|
|
398
|
+
entityId?: string
|
|
399
|
+
includeDeleted?: boolean
|
|
400
|
+
source?: string | string[] | null
|
|
401
|
+
},
|
|
402
|
+
): Promise<CanonicalTodoListResult> {
|
|
403
|
+
const where: Record<string, unknown> = {
|
|
404
|
+
tenantId: auth.tenantId,
|
|
405
|
+
interactionType: 'task',
|
|
406
|
+
}
|
|
407
|
+
if (!options?.includeDeleted) {
|
|
408
|
+
where.deletedAt = null
|
|
409
|
+
}
|
|
410
|
+
if (organizationIds && organizationIds.length > 0) {
|
|
411
|
+
where.organizationId = { $in: organizationIds }
|
|
412
|
+
}
|
|
413
|
+
if (options?.entityId) {
|
|
414
|
+
where.entity = options.entityId
|
|
415
|
+
}
|
|
416
|
+
if (options?.source) {
|
|
417
|
+
where.source = Array.isArray(options.source) ? { $in: options.source } : options.source
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const interactions = await em.find(CustomerInteraction, where, {
|
|
421
|
+
orderBy: { createdAt: 'desc' },
|
|
422
|
+
})
|
|
423
|
+
const activeInteractions = interactions.filter((interaction) => !interaction.deletedAt)
|
|
424
|
+
const groups = new Map<string, CustomerInteraction[]>()
|
|
425
|
+
|
|
426
|
+
for (const interaction of activeInteractions) {
|
|
427
|
+
const organizationId =
|
|
428
|
+
typeof interaction.organizationId === 'string' && interaction.organizationId.trim().length > 0
|
|
429
|
+
? interaction.organizationId
|
|
430
|
+
: selectedOrganizationId ?? ''
|
|
431
|
+
const bucket = groups.get(organizationId)
|
|
432
|
+
if (bucket) {
|
|
433
|
+
bucket.push(interaction)
|
|
434
|
+
} else {
|
|
435
|
+
groups.set(organizationId, [interaction])
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const rowByInteractionId = new Map<string, CustomerTodoRow>()
|
|
440
|
+
|
|
441
|
+
for (const [groupOrganizationId, groupedInteractions] of groups.entries()) {
|
|
442
|
+
const scopedOrganizationId = groupOrganizationId.length > 0 ? groupOrganizationId : null
|
|
443
|
+
const hydrated = await hydrateCanonicalInteractions({
|
|
444
|
+
em,
|
|
445
|
+
container,
|
|
446
|
+
auth: {
|
|
447
|
+
...auth,
|
|
448
|
+
orgId: auth.orgId ?? null,
|
|
449
|
+
},
|
|
450
|
+
selectedOrganizationId: scopedOrganizationId,
|
|
451
|
+
interactions: groupedInteractions,
|
|
452
|
+
})
|
|
453
|
+
const customerIds = Array.from(
|
|
454
|
+
new Set(
|
|
455
|
+
hydrated
|
|
456
|
+
.map((interaction) => interaction.entityId ?? null)
|
|
457
|
+
.filter((value): value is string => !!value),
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
const customerSummaries = await loadCustomerSummaries(
|
|
461
|
+
em,
|
|
462
|
+
customerIds,
|
|
463
|
+
auth.tenantId,
|
|
464
|
+
scopedOrganizationId,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
for (const interaction of hydrated) {
|
|
468
|
+
rowByInteractionId.set(
|
|
469
|
+
interaction.id,
|
|
470
|
+
mapInteractionRecordToTodoRow(
|
|
471
|
+
interaction,
|
|
472
|
+
interaction.entityId ? customerSummaries.get(interaction.entityId) ?? null : null,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
items: activeInteractions
|
|
480
|
+
.map((interaction) => rowByInteractionId.get(interaction.id) ?? null)
|
|
481
|
+
.filter((row): row is CustomerTodoRow => !!row),
|
|
482
|
+
bridgeIds: new Set(interactions.map((interaction) => interaction.id)),
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
267
486
|
export function mapLegacyTodoLinkToRow(
|
|
268
487
|
link: CustomerTodoLink,
|
|
269
488
|
detail: LegacyTodoDetail | null,
|
|
@@ -278,10 +497,7 @@ export function mapLegacyTodoLinkToRow(
|
|
|
278
497
|
return {
|
|
279
498
|
id: link.id,
|
|
280
499
|
todoId: link.todoId,
|
|
281
|
-
todoSource:
|
|
282
|
-
typeof link.todoSource === 'string' && link.todoSource.trim().length > 0
|
|
283
|
-
? link.todoSource
|
|
284
|
-
: 'example:todo',
|
|
500
|
+
todoSource: resolveLegacyTodoSource(link.todoSource),
|
|
285
501
|
todoTitle: detail?.title ?? null,
|
|
286
502
|
todoIsDone: detail?.isDone ?? null,
|
|
287
503
|
todoPriority: detail?.priority ?? null,
|
|
@@ -293,6 +509,7 @@ export function mapLegacyTodoLinkToRow(
|
|
|
293
509
|
organizationId: link.organizationId,
|
|
294
510
|
tenantId: link.tenantId,
|
|
295
511
|
createdAt: link.createdAt.toISOString(),
|
|
512
|
+
_integrations: undefined,
|
|
296
513
|
customer: entity,
|
|
297
514
|
}
|
|
298
515
|
}
|
|
@@ -337,6 +554,8 @@ export function mapInteractionRecordToTodoRow(
|
|
|
337
554
|
organizationId: interaction.organizationId ?? '',
|
|
338
555
|
tenantId: interaction.tenantId ?? '',
|
|
339
556
|
createdAt: interaction.createdAt,
|
|
557
|
+
externalHref: resolveExampleIntegrationHref(interaction),
|
|
558
|
+
_integrations: interaction._integrations ?? undefined,
|
|
340
559
|
customer: customer ?? {
|
|
341
560
|
id: interaction.entityId ?? null,
|
|
342
561
|
displayName: null,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260401172819 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table "customer_todo_links" alter column "todo_source" set default 'customers:interaction';`);
|
|
7
|
+
this.addSql(`
|
|
8
|
+
update "role_acls" as ra
|
|
9
|
+
set
|
|
10
|
+
"features_json" = case
|
|
11
|
+
when ra."features_json" is null or jsonb_typeof(ra."features_json") <> 'array'
|
|
12
|
+
then '["customers.interactions.view"]'::jsonb
|
|
13
|
+
else ra."features_json" || '"customers.interactions.view"'::jsonb
|
|
14
|
+
end,
|
|
15
|
+
"updated_at" = now()
|
|
16
|
+
where ra."deleted_at" is null
|
|
17
|
+
and ra."features_json" is not null
|
|
18
|
+
and jsonb_typeof(ra."features_json") = 'array'
|
|
19
|
+
and ra."features_json" ? 'example.todos.view'
|
|
20
|
+
and ra."features_json" ? 'customers.activities.view'
|
|
21
|
+
and not (ra."features_json" ? 'customers.interactions.view');
|
|
22
|
+
`);
|
|
23
|
+
this.addSql(`
|
|
24
|
+
update "user_acls" as ua
|
|
25
|
+
set
|
|
26
|
+
"features_json" = case
|
|
27
|
+
when ua."features_json" is null or jsonb_typeof(ua."features_json") <> 'array'
|
|
28
|
+
then '["customers.interactions.view"]'::jsonb
|
|
29
|
+
else ua."features_json" || '"customers.interactions.view"'::jsonb
|
|
30
|
+
end,
|
|
31
|
+
"updated_at" = now()
|
|
32
|
+
where ua."deleted_at" is null
|
|
33
|
+
and ua."features_json" is not null
|
|
34
|
+
and jsonb_typeof(ua."features_json") = 'array'
|
|
35
|
+
and ua."features_json" ? 'example.todos.view'
|
|
36
|
+
and ua."features_json" ? 'customers.activities.view'
|
|
37
|
+
and not (ua."features_json" ? 'customers.interactions.view');
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override async down(): Promise<void> {
|
|
42
|
+
this.addSql(`alter table "customer_todo_links" alter column "todo_source" set default 'example:todo';`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SearchResultLink,
|
|
7
7
|
SearchIndexSource,
|
|
8
8
|
} from '@open-mercato/shared/modules/search'
|
|
9
|
+
import { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
11
12
|
// Context Types
|
|
@@ -346,9 +347,9 @@ async function getLinkedTodo(ctx: SearchContext) {
|
|
|
346
347
|
if (todoCache.has(ctx.record)) {
|
|
347
348
|
return todoCache.get(ctx.record)
|
|
348
349
|
}
|
|
349
|
-
const sourceRaw = typeof ctx.record.todo_source === 'string' ? ctx.record.todo_source :
|
|
350
|
+
const sourceRaw = typeof ctx.record.todo_source === 'string' ? ctx.record.todo_source : EXAMPLE_TODO_SOURCE
|
|
350
351
|
const [moduleId, entityName] = sourceRaw.split(':')
|
|
351
|
-
const entityId = moduleId && entityName ? `${moduleId}:${entityName}` :
|
|
352
|
+
const entityId = moduleId && entityName ? `${moduleId}:${entityName}` : CUSTOMER_INTERACTION_TASK_SOURCE
|
|
352
353
|
const todo = await loadRecord(ctx, entityId, ctx.record.todo_id as string ?? ctx.record.todoId as string)
|
|
353
354
|
todoCache.set(ctx.record, todo ?? null)
|
|
354
355
|
return todo ?? null
|
|
@@ -7,6 +7,8 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
7
7
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
8
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
9
|
import { DEFAULT_SETTINGS, hydrateCustomerTodoSettings, type CustomerTodoWidgetSettings } from './config'
|
|
10
|
+
import { resolveExampleIntegrationHref } from '../../../lib/interactionCompatibility'
|
|
11
|
+
import { resolveTodoHref } from '../../../components/detail/utils'
|
|
10
12
|
|
|
11
13
|
type TodoLinkSummary = {
|
|
12
14
|
id: string
|
|
@@ -14,6 +16,12 @@ type TodoLinkSummary = {
|
|
|
14
16
|
todoSource: string
|
|
15
17
|
todoTitle: string | null
|
|
16
18
|
createdAt: string
|
|
19
|
+
_integrations?: {
|
|
20
|
+
example?: {
|
|
21
|
+
href?: string | null
|
|
22
|
+
}
|
|
23
|
+
[key: string]: unknown
|
|
24
|
+
}
|
|
17
25
|
entity: {
|
|
18
26
|
id: string | null
|
|
19
27
|
displayName: string | null
|
|
@@ -21,23 +29,6 @@ type TodoLinkSummary = {
|
|
|
21
29
|
}
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
// SPEC-046b: To enable canonical interactions mode, switch from
|
|
25
|
-
// /api/customers/dashboard/widgets/customer-todos to
|
|
26
|
-
// /api/customers/interactions?status=planned&pageSize={pageSize}
|
|
27
|
-
//
|
|
28
|
-
// Response mapping (InteractionSummary → TodoLinkSummary):
|
|
29
|
-
// interaction.id → id, todoId
|
|
30
|
-
// interaction.interactionType → todoSource
|
|
31
|
-
// interaction.title → todoTitle
|
|
32
|
-
// interaction.createdAt → createdAt
|
|
33
|
-
// interaction.entityId → entity.id (kind/displayName need enrichment
|
|
34
|
-
// or a follow-up customer lookup)
|
|
35
|
-
//
|
|
36
|
-
// The main gap is the `entity` sub-object: the interactions API returns a flat
|
|
37
|
-
// entityId but not customer displayName/kind. Options:
|
|
38
|
-
// a) Add an enricher to the interactions list API that resolves entity details
|
|
39
|
-
// b) Batch-fetch customer details client-side after loading interactions
|
|
40
|
-
// c) Add a dedicated /api/customers/interactions/widget endpoint
|
|
41
32
|
async function loadTodos(settings: CustomerTodoWidgetSettings): Promise<TodoLinkSummary[]> {
|
|
42
33
|
const params = new URLSearchParams({
|
|
43
34
|
limit: String(settings.pageSize),
|
|
@@ -67,6 +58,9 @@ async function loadTodos(settings: CustomerTodoWidgetSettings): Promise<TodoLink
|
|
|
67
58
|
todoSource: typeof data.todoSource === 'string' ? data.todoSource : '',
|
|
68
59
|
todoTitle: typeof data.todoTitle === 'string' ? data.todoTitle : null,
|
|
69
60
|
createdAt: typeof data.createdAt === 'string' ? data.createdAt : '',
|
|
61
|
+
_integrations: data._integrations && typeof data._integrations === 'object'
|
|
62
|
+
? (data._integrations as TodoLinkSummary['_integrations'])
|
|
63
|
+
: undefined,
|
|
70
64
|
entity: {
|
|
71
65
|
id: typeof entity.id === 'string' ? entity.id : null,
|
|
72
66
|
displayName: typeof entity.displayName === 'string' ? entity.displayName : null,
|
|
@@ -84,8 +78,8 @@ function formatDate(value: string | null, locale?: string): string {
|
|
|
84
78
|
return date.toLocaleString(locale ?? undefined)
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
function resolveDetailHref(entity: { id: string | null; kind: string | null }): string | null {
|
|
88
|
-
if (!entity
|
|
81
|
+
function resolveDetailHref(entity: { id: string | null; kind: string | null } | null | undefined): string | null {
|
|
82
|
+
if (!entity?.id) return null
|
|
89
83
|
if (entity.kind === 'company') return `/backend/customers/companies-v2/${encodeURIComponent(entity.id)}`
|
|
90
84
|
if (entity.kind === 'person') return `/backend/customers/people-v2/${encodeURIComponent(entity.id)}`
|
|
91
85
|
return `/backend/customers/people-v2/${encodeURIComponent(entity.id)}`
|
|
@@ -171,6 +165,8 @@ const CustomerTodosWidget: React.FC<DashboardWidgetComponentProps<CustomerTodoWi
|
|
|
171
165
|
{items.map((item) => {
|
|
172
166
|
const createdLabel = formatDate(item.createdAt, locale)
|
|
173
167
|
const href = resolveDetailHref(item.entity)
|
|
168
|
+
const exampleHref = resolveExampleIntegrationHref(item)
|
|
169
|
+
const taskHref = exampleHref ?? resolveTodoHref(item.todoSource, item.todoId)
|
|
174
170
|
return (
|
|
175
171
|
<li key={item.id} className="rounded-md border p-3">
|
|
176
172
|
<div className="flex items-start justify-between gap-3 text-sm font-medium">
|
|
@@ -185,13 +181,18 @@ const CustomerTodosWidget: React.FC<DashboardWidgetComponentProps<CustomerTodoWi
|
|
|
185
181
|
<p className="text-xs text-muted-foreground">{item.todoSource}</p>
|
|
186
182
|
) : null}
|
|
187
183
|
</div>
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
<div className="mt-2 flex flex-wrap gap-3 text-xs">
|
|
185
|
+
{href ? (
|
|
190
186
|
<Link className="text-primary hover:underline" href={href}>
|
|
191
187
|
{t('customers.widgets.common.viewRecord')}
|
|
192
188
|
</Link>
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
) : null}
|
|
190
|
+
{taskHref ? (
|
|
191
|
+
<Link className="text-primary hover:underline" href={taskHref}>
|
|
192
|
+
{t('customers.workPlan.customerTodos.table.actions.openTask')}
|
|
193
|
+
</Link>
|
|
194
|
+
) : null}
|
|
195
|
+
</div>
|
|
195
196
|
</li>
|
|
196
197
|
)
|
|
197
198
|
})}
|