@open-mercato/core 0.4.5-develop-811deeb983 → 0.4.5-develop-3d8e759e45
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/catalog/inbox-actions.js +51 -0
- package/dist/modules/catalog/inbox-actions.js.map +7 -0
- package/dist/modules/customers/inbox-actions.js +230 -0
- package/dist/modules/customers/inbox-actions.js.map +7 -0
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +40 -1
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/extract/route.js +87 -0
- package/dist/modules/inbox_ops/api/extract/route.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js +6 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/translate/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +40 -14
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +161 -79
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +109 -62
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +3 -3
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +36 -14
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.meta.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +65 -10
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +58 -10
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/inbox_ops/lib/constants.js.map +2 -2
- package/dist/modules/inbox_ops/lib/contactValidation.js +40 -0
- package/dist/modules/inbox_ops/lib/contactValidation.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js +31 -826
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +3 -3
- package/dist/modules/inbox_ops/lib/executionHelpers.js +368 -0
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +7 -0
- package/dist/modules/inbox_ops/lib/extractionPrompt.js +28 -35
- package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +3 -3
- package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js +1 -0
- package/dist/modules/inbox_ops/lib/inbox-actions-generated.d.js.map +7 -0
- package/dist/modules/inbox_ops/lib/translationProvider.js +15 -10
- package/dist/modules/inbox_ops/lib/translationProvider.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +16 -16
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +278 -0
- package/dist/modules/sales/inbox-actions.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.mocks/inbox-actions.generated.js +5 -0
- package/package.json +2 -2
- package/src/modules/catalog/inbox-actions.ts +60 -0
- package/src/modules/customers/inbox-actions.ts +285 -0
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +44 -0
- package/src/modules/inbox_ops/api/extract/route.ts +94 -0
- package/src/modules/inbox_ops/api/proposals/[id]/translate/route.ts +6 -1
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +2 -0
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +43 -13
- package/src/modules/inbox_ops/backend/inbox-ops/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +176 -81
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +122 -68
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.meta.ts +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +36 -14
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +91 -7
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +64 -12
- package/src/modules/inbox_ops/lib/constants.ts +9 -0
- package/src/modules/inbox_ops/lib/contactValidation.ts +54 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +47 -1060
- package/src/modules/inbox_ops/lib/executionHelpers.ts +527 -0
- package/src/modules/inbox_ops/lib/extractionPrompt.ts +45 -34
- package/src/modules/inbox_ops/lib/inbox-actions-generated.d.ts +11 -0
- package/src/modules/inbox_ops/lib/translationProvider.ts +16 -10
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +16 -18
- package/src/modules/sales/inbox-actions.ts +359 -0
|
@@ -19,6 +19,61 @@ import {
|
|
|
19
19
|
ShoppingBag,
|
|
20
20
|
} from 'lucide-react'
|
|
21
21
|
import type { ActionDetail, DiscrepancyDetail } from './types'
|
|
22
|
+
import { hasContactNameIssue } from '../../lib/contactValidation'
|
|
23
|
+
|
|
24
|
+
export { hasContactNameIssue }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves discrepancy description i18n keys stored in the database.
|
|
28
|
+
* Falls back to the raw description string for legacy data or LLM-generated descriptions.
|
|
29
|
+
*/
|
|
30
|
+
export function useDiscrepancyDescriptions(): (description: string, foundValue?: string | null) => string {
|
|
31
|
+
const t = useT()
|
|
32
|
+
const translations: Record<string, string> = {
|
|
33
|
+
'inbox_ops.discrepancy.desc.no_channel': t('inbox_ops.discrepancy.desc.no_channel', 'No sales channel available. Create a channel in Sales settings before accepting this order.'),
|
|
34
|
+
'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code or configure a sales channel with a default currency.'),
|
|
35
|
+
'inbox_ops.discrepancy.desc.product_not_matched': t('inbox_ops.discrepancy.desc.product_not_matched', 'Product could not be matched to any catalog product'),
|
|
36
|
+
'inbox_ops.discrepancy.desc.no_matching_contact': t('inbox_ops.discrepancy.desc.no_matching_contact', 'No matching contact found'),
|
|
37
|
+
'inbox_ops.discrepancy.desc.draft_reply_no_contact': t('inbox_ops.discrepancy.desc.draft_reply_no_contact', 'Draft reply target has no matching contact. Create the contact first.'),
|
|
38
|
+
'inbox_ops.discrepancy.desc.duplicate_order_reference': t('inbox_ops.discrepancy.desc.duplicate_order_reference', 'An order with this customer reference already exists'),
|
|
39
|
+
}
|
|
40
|
+
return (description: string, foundValue?: string | null) => {
|
|
41
|
+
const translated = translations[description]
|
|
42
|
+
if (!translated) return description
|
|
43
|
+
if (foundValue && (description === 'inbox_ops.discrepancy.desc.product_not_matched' || description === 'inbox_ops.discrepancy.desc.no_matching_contact')) {
|
|
44
|
+
return `${translated}: ${foundValue}`
|
|
45
|
+
}
|
|
46
|
+
return translated
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves action description i18n keys stored in the database.
|
|
52
|
+
* Auto-generated actions store keys like `inbox_ops.action.desc.create_contact`;
|
|
53
|
+
* LLM-generated actions store plain text which is returned as-is.
|
|
54
|
+
*/
|
|
55
|
+
export function useActionDescriptionResolver(): (description: string, payload: Record<string, unknown>) => string {
|
|
56
|
+
const t = useT()
|
|
57
|
+
return (description: string, payload: Record<string, unknown>) => {
|
|
58
|
+
if (!description.startsWith('inbox_ops.action.desc.')) return description
|
|
59
|
+
const name = (payload.name as string) || (payload.contactName as string) || ''
|
|
60
|
+
const email = (payload.email as string) || (payload.emailAddress as string) || ''
|
|
61
|
+
const title = (payload.title as string) || ''
|
|
62
|
+
const toName = (payload.toName as string) || (payload.to as string) || ''
|
|
63
|
+
const subject = (payload.subject as string) || ''
|
|
64
|
+
const translations: Record<string, string> = {
|
|
65
|
+
'inbox_ops.action.desc.create_contact': t('inbox_ops.action.desc.create_contact', 'Create contact for {name} ({email})')
|
|
66
|
+
.replace('{name}', name).replace('{email}', email),
|
|
67
|
+
'inbox_ops.action.desc.link_contact': t('inbox_ops.action.desc.link_contact', 'Link {name} ({email}) to existing contact')
|
|
68
|
+
.replace('{name}', name).replace('{email}', email),
|
|
69
|
+
'inbox_ops.action.desc.create_product': t('inbox_ops.action.desc.create_product', 'Create catalog product "{title}"')
|
|
70
|
+
.replace('{title}', title),
|
|
71
|
+
'inbox_ops.action.desc.draft_reply': t('inbox_ops.action.desc.draft_reply', 'Draft reply to {toName}: {subject}')
|
|
72
|
+
.replace('{toName}', toName).replace('{subject}', subject),
|
|
73
|
+
}
|
|
74
|
+
return translations[description] || description
|
|
75
|
+
}
|
|
76
|
+
}
|
|
22
77
|
|
|
23
78
|
const ACTION_TYPE_ICONS: Record<string, React.ElementType> = {
|
|
24
79
|
create_order: Package,
|
|
@@ -169,6 +224,7 @@ export function ActionCard({
|
|
|
169
224
|
onRetry,
|
|
170
225
|
onEdit,
|
|
171
226
|
translatedDescription,
|
|
227
|
+
resolveDiscrepancyDescription,
|
|
172
228
|
}: {
|
|
173
229
|
action: ActionDetail
|
|
174
230
|
discrepancies: DiscrepancyDetail[]
|
|
@@ -178,14 +234,16 @@ export function ActionCard({
|
|
|
178
234
|
onRetry: (id: string) => void
|
|
179
235
|
onEdit: (action: ActionDetail) => void
|
|
180
236
|
translatedDescription?: string
|
|
237
|
+
resolveDiscrepancyDescription?: (description: string, foundValue?: string | null) => string
|
|
181
238
|
}) {
|
|
182
239
|
const t = useT()
|
|
183
240
|
const Icon = ACTION_TYPE_ICONS[action.actionType] || Package
|
|
184
241
|
const label = actionTypeLabels[action.actionType] || action.actionType
|
|
242
|
+
const resolveActionDescription = useActionDescriptionResolver()
|
|
185
243
|
|
|
186
244
|
const actionDiscrepancies = discrepancies.filter((d) => d.actionId === action.id && !d.resolved)
|
|
187
245
|
const hasBlockingDiscrepancies = actionDiscrepancies.some((d) => d.severity === 'error')
|
|
188
|
-
const displayDescription = translatedDescription || action.description
|
|
246
|
+
const displayDescription = translatedDescription || resolveActionDescription(action.description, action.payload)
|
|
189
247
|
|
|
190
248
|
if (action.status === 'executed') {
|
|
191
249
|
return (
|
|
@@ -198,7 +256,7 @@ export function ActionCard({
|
|
|
198
256
|
{action.createdEntityId && (
|
|
199
257
|
<div className="mt-2">
|
|
200
258
|
<span className="text-xs text-green-600">
|
|
201
|
-
Created {action.createdEntityType} · {action.executedAt && new Date(action.executedAt).toLocaleString()}
|
|
259
|
+
{t('inbox_ops.action.created_entity', 'Created {type}').replace('{type}', action.createdEntityType || '')} · {action.executedAt && new Date(action.executedAt).toLocaleString()}
|
|
202
260
|
</span>
|
|
203
261
|
</div>
|
|
204
262
|
)}
|
|
@@ -233,6 +291,7 @@ export function ActionCard({
|
|
|
233
291
|
)}
|
|
234
292
|
<div className="mt-3 flex items-center gap-2">
|
|
235
293
|
<Button
|
|
294
|
+
type="button"
|
|
236
295
|
size="sm"
|
|
237
296
|
className="h-11 md:h-9"
|
|
238
297
|
onClick={() => onRetry(action.id)}
|
|
@@ -241,6 +300,7 @@ export function ActionCard({
|
|
|
241
300
|
{t('inbox_ops.action.retry', 'Retry')}
|
|
242
301
|
</Button>
|
|
243
302
|
<Button
|
|
303
|
+
type="button"
|
|
244
304
|
variant="outline"
|
|
245
305
|
size="sm"
|
|
246
306
|
className="h-11 md:h-9"
|
|
@@ -250,6 +310,7 @@ export function ActionCard({
|
|
|
250
310
|
{t('inbox_ops.action.edit', 'Edit')}
|
|
251
311
|
</Button>
|
|
252
312
|
<Button
|
|
313
|
+
type="button"
|
|
253
314
|
variant="outline"
|
|
254
315
|
size="sm"
|
|
255
316
|
className="h-11 md:h-9"
|
|
@@ -263,6 +324,8 @@ export function ActionCard({
|
|
|
263
324
|
)
|
|
264
325
|
}
|
|
265
326
|
|
|
327
|
+
const hasNameIssue = hasContactNameIssue(action)
|
|
328
|
+
|
|
266
329
|
return (
|
|
267
330
|
<div className="border rounded-lg p-3 md:p-4">
|
|
268
331
|
<div className="flex items-center gap-2 mb-2">
|
|
@@ -288,12 +351,12 @@ export function ActionCard({
|
|
|
288
351
|
}`}>
|
|
289
352
|
<AlertTriangle className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
|
290
353
|
<div>
|
|
291
|
-
<span>{d.description}</span>
|
|
354
|
+
<span>{resolveDiscrepancyDescription ? resolveDiscrepancyDescription(d.description, d.foundValue) : d.description}</span>
|
|
292
355
|
{(d.expectedValue || d.foundValue) && (
|
|
293
356
|
<div className="mt-0.5 text-[11px] opacity-80">
|
|
294
|
-
{d.expectedValue && <span>Expected: {d.expectedValue}</span>}
|
|
357
|
+
{d.expectedValue && <span>{t('inbox_ops.discrepancy.expected', 'Expected')}: {d.expectedValue}</span>}
|
|
295
358
|
{d.expectedValue && d.foundValue && <span> · </span>}
|
|
296
|
-
{d.foundValue && <span>Found: {d.foundValue}</span>}
|
|
359
|
+
{d.foundValue && <span>{t('inbox_ops.discrepancy.found', 'Found')}: {d.foundValue}</span>}
|
|
297
360
|
</div>
|
|
298
361
|
)}
|
|
299
362
|
</div>
|
|
@@ -302,19 +365,39 @@ export function ActionCard({
|
|
|
302
365
|
</div>
|
|
303
366
|
)}
|
|
304
367
|
|
|
368
|
+
{hasNameIssue && (
|
|
369
|
+
<div className="mb-3 flex items-start gap-2 text-xs rounded px-2 py-1.5 bg-amber-50 text-amber-700 dark:bg-amber-950/20 dark:text-amber-300">
|
|
370
|
+
<AlertTriangle className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
|
371
|
+
<span>{action.actionType === 'link_contact'
|
|
372
|
+
? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')
|
|
373
|
+
: t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')
|
|
374
|
+
}</span>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
|
|
305
378
|
<div className="flex items-center gap-2">
|
|
306
|
-
<div title={
|
|
379
|
+
<div title={
|
|
380
|
+
hasNameIssue
|
|
381
|
+
? action.actionType === 'link_contact'
|
|
382
|
+
? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')
|
|
383
|
+
: t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')
|
|
384
|
+
: hasBlockingDiscrepancies
|
|
385
|
+
? t('inbox_ops.action.accept_blocked', 'Resolve errors before accepting')
|
|
386
|
+
: undefined
|
|
387
|
+
}>
|
|
307
388
|
<Button
|
|
389
|
+
type="button"
|
|
308
390
|
size="sm"
|
|
309
391
|
className="h-11 md:h-9"
|
|
310
392
|
onClick={() => onAccept(action.id)}
|
|
311
|
-
disabled={hasBlockingDiscrepancies}
|
|
393
|
+
disabled={hasBlockingDiscrepancies || hasNameIssue}
|
|
312
394
|
>
|
|
313
395
|
<CheckCircle className="h-4 w-4 mr-1" />
|
|
314
396
|
{t('inbox_ops.action.accept', 'Accept')}
|
|
315
397
|
</Button>
|
|
316
398
|
</div>
|
|
317
399
|
<Button
|
|
400
|
+
type="button"
|
|
318
401
|
variant="outline"
|
|
319
402
|
size="sm"
|
|
320
403
|
className="h-11 md:h-9"
|
|
@@ -324,6 +407,7 @@ export function ActionCard({
|
|
|
324
407
|
{t('inbox_ops.action.edit', 'Edit')}
|
|
325
408
|
</Button>
|
|
326
409
|
<Button
|
|
410
|
+
type="button"
|
|
327
411
|
variant="outline"
|
|
328
412
|
size="sm"
|
|
329
413
|
className="h-11 md:h-9"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
5
5
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
6
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
9
|
import { Loader2 } from 'lucide-react'
|
|
@@ -75,18 +76,62 @@ function ContactPayloadEditor({
|
|
|
75
76
|
updateField: (key: string, value: unknown) => void
|
|
76
77
|
}) {
|
|
77
78
|
const t = useT()
|
|
79
|
+
const type = (payload.type as string) || 'person'
|
|
80
|
+
const isPerson = type === 'person'
|
|
81
|
+
|
|
82
|
+
const existingName = (payload.name as string) || ''
|
|
83
|
+
const nameParts = existingName.trim().split(/\s+/).filter((p) => p.length > 0)
|
|
84
|
+
const [firstName, setFirstName] = React.useState(() => isPerson ? (nameParts[0] || '') : '')
|
|
85
|
+
const [lastName, setLastName] = React.useState(() => isPerson ? (nameParts.slice(1).join(' ') || '') : '')
|
|
86
|
+
|
|
87
|
+
const updatePersonName = React.useCallback((first: string, last: string) => {
|
|
88
|
+
const combined = `${first} ${last}`.trim()
|
|
89
|
+
updateField('name', combined)
|
|
90
|
+
}, [updateField])
|
|
91
|
+
|
|
92
|
+
const lastNameMissing = isPerson && !lastName.trim()
|
|
93
|
+
|
|
78
94
|
return (
|
|
79
95
|
<div className="space-y-3">
|
|
80
96
|
<div className="grid grid-cols-2 gap-3">
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
{isPerson ? (
|
|
98
|
+
<>
|
|
99
|
+
<div>
|
|
100
|
+
<Label>{t('inbox_ops.contact.first_name', 'First Name')}</Label>
|
|
101
|
+
<Input
|
|
102
|
+
value={firstName}
|
|
103
|
+
onChange={(event) => {
|
|
104
|
+
setFirstName(event.target.value)
|
|
105
|
+
updatePersonName(event.target.value, lastName)
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div>
|
|
110
|
+
<Label>{t('inbox_ops.contact.last_name', 'Last Name')}</Label>
|
|
111
|
+
<Input
|
|
112
|
+
value={lastName}
|
|
113
|
+
onChange={(event) => {
|
|
114
|
+
setLastName(event.target.value)
|
|
115
|
+
updatePersonName(firstName, event.target.value)
|
|
116
|
+
}}
|
|
117
|
+
className={lastNameMissing ? 'border-red-500' : ''}
|
|
118
|
+
/>
|
|
119
|
+
{lastNameMissing && (
|
|
120
|
+
<p className="text-xs text-red-600 mt-1">{t('inbox_ops.contact.last_name_required', 'Last name is required')}</p>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</>
|
|
124
|
+
) : (
|
|
125
|
+
<div>
|
|
126
|
+
<Label>{t('inbox_ops.edit_dialog.name', 'Name')}</Label>
|
|
127
|
+
<Input value={existingName} onChange={(event) => updateField('name', event.target.value)} />
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
85
130
|
<div>
|
|
86
131
|
<Label>{t('inbox_ops.edit_dialog.type', 'Type')}</Label>
|
|
87
132
|
<select
|
|
88
133
|
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm"
|
|
89
|
-
value={
|
|
134
|
+
value={type}
|
|
90
135
|
onChange={(event) => updateField('type', event.target.value)}
|
|
91
136
|
>
|
|
92
137
|
<option value="person">{t('inbox_ops.contact_type.person', 'Person')}</option>
|
|
@@ -367,6 +412,9 @@ export function EditActionDialog({
|
|
|
367
412
|
onSaved: () => void
|
|
368
413
|
}) {
|
|
369
414
|
const t = useT()
|
|
415
|
+
const { runMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
416
|
+
contextId: 'inbox-ops-edit-action',
|
|
417
|
+
})
|
|
370
418
|
const [payload, setPayload] = React.useState<Record<string, unknown>>(
|
|
371
419
|
() => structuredClone(action.payload),
|
|
372
420
|
)
|
|
@@ -388,10 +436,13 @@ export function EditActionDialog({
|
|
|
388
436
|
}
|
|
389
437
|
|
|
390
438
|
setIsSaving(true)
|
|
391
|
-
const result = await
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
439
|
+
const result = await runMutation({
|
|
440
|
+
operation: () => apiCall<{ ok: boolean; error?: string }>(
|
|
441
|
+
`/api/inbox_ops/proposals/${action.proposalId}/actions/${action.id}`,
|
|
442
|
+
{ method: 'PATCH', body: JSON.stringify({ payload: finalPayload }) },
|
|
443
|
+
),
|
|
444
|
+
context: {},
|
|
445
|
+
})
|
|
395
446
|
if (result?.ok && result.result?.ok) {
|
|
396
447
|
flash(t('inbox_ops.edit_dialog.saved', 'Action updated successfully'), 'success')
|
|
397
448
|
onSaved()
|
|
@@ -400,7 +451,7 @@ export function EditActionDialog({
|
|
|
400
451
|
flash(result?.result?.error || t('inbox_ops.flash.save_failed', 'Failed to save'), 'error')
|
|
401
452
|
}
|
|
402
453
|
setIsSaving(false)
|
|
403
|
-
}, [action, payload, jsonMode, jsonText, t, onSaved, onClose])
|
|
454
|
+
}, [action, payload, jsonMode, jsonText, t, onSaved, onClose, runMutation])
|
|
404
455
|
|
|
405
456
|
React.useEffect(() => {
|
|
406
457
|
function handleKeyDown(event: KeyboardEvent) {
|
|
@@ -471,6 +522,7 @@ export function EditActionDialog({
|
|
|
471
522
|
|
|
472
523
|
{hasTypedEditor && (
|
|
473
524
|
<Button
|
|
525
|
+
type="button"
|
|
474
526
|
variant="ghost"
|
|
475
527
|
size="sm"
|
|
476
528
|
onClick={() => {
|
|
@@ -494,10 +546,10 @@ export function EditActionDialog({
|
|
|
494
546
|
</div>
|
|
495
547
|
|
|
496
548
|
<DialogFooter>
|
|
497
|
-
<Button variant="outline" onClick={onClose} disabled={isSaving}>
|
|
549
|
+
<Button type="button" variant="outline" onClick={onClose} disabled={isSaving}>
|
|
498
550
|
{t('inbox_ops.edit_dialog.cancel', 'Cancel')}
|
|
499
551
|
</Button>
|
|
500
|
-
<Button onClick={handleSave} disabled={isSaving}>
|
|
552
|
+
<Button type="button" onClick={handleSave} disabled={isSaving}>
|
|
501
553
|
{isSaving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
|
|
502
554
|
{t('inbox_ops.edit_dialog.save', 'Save Changes')}
|
|
503
555
|
</Button>
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { InboxActionType } from '../data/entities'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous action-type-to-RBAC-feature mapping.
|
|
5
|
+
*
|
|
6
|
+
* The generated inbox action registry (`getInboxAction(type)?.requiredFeature`)
|
|
7
|
+
* provides the same data but requires async loading. This map exists as the
|
|
8
|
+
* synchronous equivalent used by the extraction worker and execution engine.
|
|
9
|
+
*
|
|
10
|
+
* TODO: Consolidate with the generated registry once it supports sync access.
|
|
11
|
+
*/
|
|
3
12
|
export const REQUIRED_FEATURES_MAP: Record<InboxActionType, string> = {
|
|
4
13
|
create_order: 'sales.orders.manage',
|
|
5
14
|
create_quote: 'sales.quotes.manage',
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { InboxActionType } from '../data/entities'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if a contact action has a name issue that prevents acceptance.
|
|
5
|
+
* - create_contact (person): requires first+last name (2+ space-separated parts)
|
|
6
|
+
* - link_contact: requires a non-empty contactName
|
|
7
|
+
*/
|
|
8
|
+
export function hasContactNameIssue(action: {
|
|
9
|
+
actionType: InboxActionType | string
|
|
10
|
+
payload: Record<string, unknown>
|
|
11
|
+
}): boolean {
|
|
12
|
+
if (action.actionType === 'link_contact') {
|
|
13
|
+
const contactName = (action.payload.contactName as string) || ''
|
|
14
|
+
return contactName.trim().length === 0
|
|
15
|
+
}
|
|
16
|
+
if (action.actionType !== 'create_contact') return false
|
|
17
|
+
const type = (action.payload.type as string) || 'person'
|
|
18
|
+
if (type !== 'person') return false
|
|
19
|
+
const name = (action.payload.name as string) || ''
|
|
20
|
+
return name.trim().split(/\s+/).length < 2
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Split a full name into first and last name parts.
|
|
25
|
+
* Falls back to deriving name parts from email when name is a single word.
|
|
26
|
+
*/
|
|
27
|
+
export function splitPersonName(name: string, email?: string): { firstName: string; lastName: string } {
|
|
28
|
+
const trimmed = name.trim()
|
|
29
|
+
const parts = trimmed.split(/\s+/).filter((item) => item.length > 0)
|
|
30
|
+
|
|
31
|
+
if (parts.length >= 2) {
|
|
32
|
+
return {
|
|
33
|
+
firstName: parts[0],
|
|
34
|
+
lastName: parts.slice(1).join(' '),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback: try to derive first/last from email address
|
|
39
|
+
if (email) {
|
|
40
|
+
const localPart = email.split('@')[0] || ''
|
|
41
|
+
const emailParts = localPart.split(/[._-]/).filter((p) => p.length > 0)
|
|
42
|
+
if (emailParts.length >= 2) {
|
|
43
|
+
return {
|
|
44
|
+
firstName: emailParts[0].charAt(0).toUpperCase() + emailParts[0].slice(1).toLowerCase(),
|
|
45
|
+
lastName: emailParts.slice(1).map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(' '),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
firstName: parts[0] || trimmed,
|
|
52
|
+
lastName: '',
|
|
53
|
+
}
|
|
54
|
+
}
|