@open-mercato/core 0.10.1-develop.7833.1.d6cbf0e3ab → 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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/cart/api/current/lines/bulk/route.js +1 -1
- package/dist/modules/cart/api/current/lines/bulk/route.js.map +2 -2
- package/dist/modules/cart/api/current/lines/route.js +1 -1
- package/dist/modules/cart/api/current/lines/route.js.map +2 -2
- package/dist/modules/cart/commands/lines.js +5 -10
- package/dist/modules/cart/commands/lines.js.map +2 -2
- package/dist/modules/cart/lib/currentCartRoute.js +8 -0
- package/dist/modules/cart/lib/currentCartRoute.js.map +3 -3
- package/dist/modules/cart/lib/lineMutationLock.js +52 -0
- package/dist/modules/cart/lib/lineMutationLock.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
- package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +24 -7
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +2 -1
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/cli.js +28 -110
- package/dist/modules/customers/cli.js.map +3 -3
- package/dist/modules/customers/components/DictionarySettings.js +1 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskForm.js +1 -16
- package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +38 -10
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
- package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
- package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
- package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
- package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
- package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
- package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
- package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
- package/dist/modules/customers/translations.js +9 -0
- package/dist/modules/customers/translations.js.map +7 -0
- package/dist/modules/eudr/index.js +13 -0
- package/dist/modules/eudr/index.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +3 -3
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +75 -21
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
- package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
- package/dist/modules/sync_excel/index.js +11 -1
- package/dist/modules/sync_excel/index.js.map +2 -2
- package/dist/modules/translations/di.js +2 -0
- package/dist/modules/translations/di.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/cart/api/current/lines/bulk/route.ts +1 -1
- package/src/modules/cart/api/current/lines/route.ts +1 -1
- package/src/modules/cart/commands/lines.ts +5 -10
- package/src/modules/cart/i18n/de.json +1 -0
- package/src/modules/cart/i18n/en.json +1 -0
- package/src/modules/cart/i18n/es.json +1 -0
- package/src/modules/cart/i18n/ko.json +1 -0
- package/src/modules/cart/i18n/pl.json +1 -0
- package/src/modules/cart/lib/currentCartRoute.ts +8 -0
- package/src/modules/cart/lib/lineMutationLock.ts +59 -0
- package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
- package/src/modules/customer_accounts/api/admin/users.ts +26 -7
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/i18n/de.json +10 -0
- package/src/modules/customer_accounts/i18n/en.json +10 -0
- package/src/modules/customer_accounts/i18n/es.json +10 -0
- package/src/modules/customer_accounts/i18n/ko.json +10 -0
- package/src/modules/customer_accounts/i18n/pl.json +10 -0
- package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
- package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
- package/src/modules/customers/cli.ts +23 -124
- package/src/modules/customers/components/DictionarySettings.tsx +1 -1
- package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
- package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
- package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
- package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
- package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
- package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
- package/src/modules/customers/i18n/de.json +81 -0
- package/src/modules/customers/i18n/en.json +81 -0
- package/src/modules/customers/i18n/es.json +81 -0
- package/src/modules/customers/i18n/ko.json +81 -0
- package/src/modules/customers/i18n/pl.json +81 -0
- package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
- package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
- package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
- package/src/modules/customers/translations.ts +5 -0
- package/src/modules/eudr/index.ts +13 -0
- package/src/modules/sales/commands/documents.ts +3 -3
- package/src/modules/sales/components/documents/ItemsSection.tsx +82 -23
- package/src/modules/sales/components/documents/lineItemUtils.ts +2 -2
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
- package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
- package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
- package/src/modules/sync_excel/index.ts +10 -0
- package/src/modules/translations/di.ts +2 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { AwilixContainer } from 'awilix'
|
|
4
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
6
|
+
import {
|
|
7
|
+
organizationScopeRequiredResponse,
|
|
8
|
+
resolveActiveOrganizationId,
|
|
9
|
+
} from '@open-mercato/shared/lib/auth/organizationScope'
|
|
10
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
11
|
+
|
|
12
|
+
export type AdminTargetOrganizationResult =
|
|
13
|
+
| { ok: true; organizationId: string }
|
|
14
|
+
| { ok: false; response: Response }
|
|
15
|
+
|
|
16
|
+
const organizationIdSchema = z.string().uuid()
|
|
17
|
+
|
|
18
|
+
async function organizationNotFound(): Promise<AdminTargetOrganizationResult> {
|
|
19
|
+
const { translate } = await resolveTranslations()
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
response: NextResponse.json({ ok: false, error: translate('customer_accounts.admin.errors.organizationNotFound', 'Organization not found') }, { status: 400 }),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the organization a staff-initiated customer-accounts write targets.
|
|
28
|
+
*
|
|
29
|
+
* Without a request the caller's active organization is used, falling back to the
|
|
30
|
+
* actor's own organization under an "all organizations" selection.
|
|
31
|
+
*
|
|
32
|
+
* An explicitly requested organization is honored only when the organization-scope
|
|
33
|
+
* resolver grants it for the caller's own tenant: it must exist, not be deleted, and
|
|
34
|
+
* sit inside the caller's allowed organization set.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveAdminTargetOrganization({
|
|
37
|
+
container,
|
|
38
|
+
auth,
|
|
39
|
+
request,
|
|
40
|
+
requestedOrganizationId,
|
|
41
|
+
}: {
|
|
42
|
+
container: AwilixContainer
|
|
43
|
+
auth: NonNullable<AuthContext>
|
|
44
|
+
request: Request
|
|
45
|
+
requestedOrganizationId?: string | null
|
|
46
|
+
}): Promise<AdminTargetOrganizationResult> {
|
|
47
|
+
const requested = typeof requestedOrganizationId === 'string' && requestedOrganizationId.trim().length > 0
|
|
48
|
+
? requestedOrganizationId.trim()
|
|
49
|
+
: null
|
|
50
|
+
const activeOrganizationId = resolveActiveOrganizationId(auth)
|
|
51
|
+
|
|
52
|
+
if (!requested) {
|
|
53
|
+
if (!activeOrganizationId) return { ok: false, response: organizationScopeRequiredResponse() }
|
|
54
|
+
return { ok: true, organizationId: activeOrganizationId }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!organizationIdSchema.safeParse(requested).success) return organizationNotFound()
|
|
58
|
+
if (requested === activeOrganizationId) return { ok: true, organizationId: requested }
|
|
59
|
+
|
|
60
|
+
const scope = await resolveOrganizationScopeForRequest({
|
|
61
|
+
container,
|
|
62
|
+
auth,
|
|
63
|
+
request,
|
|
64
|
+
selectedId: requested,
|
|
65
|
+
tenantId: auth.tenantId ?? null,
|
|
66
|
+
})
|
|
67
|
+
if (scope.selectionRejected || scope.selectedId !== requested || scope.tenantId !== (auth.tenantId ?? null)) {
|
|
68
|
+
return organizationNotFound()
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, organizationId: requested }
|
|
71
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { FilterQuery } from '@mikro-orm/core'
|
|
4
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
|
+
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
7
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
|
+
import { CustomerDictionaryEntry } from '../../data/entities'
|
|
10
|
+
import { resolveDictionaryRouteContext } from '../dictionaries/context'
|
|
11
|
+
import { createPagedListResponseSchema } from '../openapi'
|
|
12
|
+
|
|
13
|
+
const querySchema = z.object({
|
|
14
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
15
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
16
|
+
search: z.string().trim().max(150).optional(),
|
|
17
|
+
id: z.string().uuid().optional(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const metadata = {
|
|
21
|
+
GET: { requireAuth: true, requireFeatures: ['customers.settings.manage', 'translations.view'] },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function GET(req: Request) {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(req.url)
|
|
27
|
+
const query = querySchema.parse({
|
|
28
|
+
page: url.searchParams.get('page') ?? undefined,
|
|
29
|
+
pageSize: url.searchParams.get('pageSize') ?? undefined,
|
|
30
|
+
search: url.searchParams.get('search') ?? undefined,
|
|
31
|
+
id: url.searchParams.get('id') ?? undefined,
|
|
32
|
+
})
|
|
33
|
+
const { em, tenantId, organizationId, translate } = await resolveDictionaryRouteContext(req)
|
|
34
|
+
if (!organizationId) {
|
|
35
|
+
return NextResponse.json({ error: translate('customers.errors.organization_required', 'Organization context is required') }, { status: 400 })
|
|
36
|
+
}
|
|
37
|
+
const where: FilterQuery<CustomerDictionaryEntry> = {
|
|
38
|
+
tenantId,
|
|
39
|
+
organizationId,
|
|
40
|
+
...(query.id ? { id: query.id } : {}),
|
|
41
|
+
...(query.search ? { label: { $ilike: `%${escapeLikePattern(query.search)}%` } } : {}),
|
|
42
|
+
}
|
|
43
|
+
const total = await em.count(CustomerDictionaryEntry, where)
|
|
44
|
+
const entries = await findWithDecryption(em, CustomerDictionaryEntry, where, {
|
|
45
|
+
orderBy: { kind: 'asc', label: 'asc', id: 'asc' },
|
|
46
|
+
limit: query.pageSize,
|
|
47
|
+
offset: (query.page - 1) * query.pageSize,
|
|
48
|
+
}, { tenantId, organizationId })
|
|
49
|
+
return NextResponse.json({
|
|
50
|
+
items: entries.map(({ id, kind, value, label, updatedAt }) => ({ id, kind, value, label, updatedAt })),
|
|
51
|
+
total,
|
|
52
|
+
page: query.page,
|
|
53
|
+
pageSize: query.pageSize,
|
|
54
|
+
totalPages: Math.ceil(total / query.pageSize),
|
|
55
|
+
})
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })
|
|
58
|
+
if (error instanceof z.ZodError) {
|
|
59
|
+
const { translate } = await resolveTranslations()
|
|
60
|
+
return NextResponse.json({ error: translate('customers.errors.invalid_query', 'Invalid query parameters') }, { status: 400 })
|
|
61
|
+
}
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const openApi: OpenApiRouteDoc = {
|
|
67
|
+
tag: 'Customers',
|
|
68
|
+
summary: 'Customer dictionary translation records',
|
|
69
|
+
methods: {
|
|
70
|
+
GET: {
|
|
71
|
+
summary: 'List base labels for translation management',
|
|
72
|
+
query: querySchema,
|
|
73
|
+
responses: [{
|
|
74
|
+
status: 200,
|
|
75
|
+
description: 'Dictionary entries in the selected organization with their stored base labels',
|
|
76
|
+
schema: createPagedListResponseSchema(z.object({
|
|
77
|
+
id: z.string().uuid(), kind: z.string(), value: z.string(), label: z.string(), updatedAt: z.string(),
|
|
78
|
+
})),
|
|
79
|
+
}],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
@@ -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 } }
|
|
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
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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],
|