@open-mercato/core 0.5.1-develop.2996.ce62fd491c → 0.5.1-develop.3032.01699048cb
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/customers/api/companies/[id]/route.js +30 -20
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +12 -7
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +12 -7
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +12 -7
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +21 -0
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +27 -30
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +56 -0
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesCard.js +175 -0
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +324 -0
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesSection.js +62 -13
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +14 -23
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +13 -13
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +35 -22
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/AiActionChips.js +15 -22
- package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +196 -28
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/FooterFields.js +14 -2
- package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +9 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +9 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +25 -4
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +20 -3
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/customers/api/companies/[id]/route.ts +30 -20
- package/src/modules/customers/api/companies/route.ts +12 -7
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +12 -7
- package/src/modules/customers/api/people/route.ts +12 -7
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +22 -0
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +28 -21
- package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +67 -0
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +231 -0
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +390 -0
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +91 -40
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +25 -23
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +15 -19
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +36 -29
- package/src/modules/customers/components/detail/AiActionChips.tsx +17 -23
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +233 -41
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +6 -2
- package/src/modules/customers/components/detail/schedule/FooterFields.tsx +22 -2
- package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +10 -2
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +10 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +26 -6
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +32 -3
- package/src/modules/customers/i18n/de.json +69 -2
- package/src/modules/customers/i18n/en.json +69 -2
- package/src/modules/customers/i18n/es.json +69 -2
- package/src/modules/customers/i18n/pl.json +68 -1
|
@@ -33,6 +33,51 @@ const TYPE_TABS: Array<{ type: ActivityType; icon: React.ComponentType<{ classNa
|
|
|
33
33
|
{ type: 'email', icon: Mail, labelKey: 'customers.schedule.types.email', fallback: 'Email' },
|
|
34
34
|
]
|
|
35
35
|
|
|
36
|
+
type DialogChrome = { titleKey: string; titleFallback: string; subtitleKey: string; subtitleFallback: string; saveKey: string; saveFallback: string; saveIcon: React.ComponentType<{ className?: string }> }
|
|
37
|
+
|
|
38
|
+
const TYPE_CHROME: Record<ActivityType, DialogChrome> = {
|
|
39
|
+
meeting: {
|
|
40
|
+
titleKey: 'customers.schedule.meeting.title', titleFallback: 'New meeting',
|
|
41
|
+
subtitleKey: 'customers.schedule.meeting.subtitle', subtitleFallback: 'Block time on the calendar with attendees',
|
|
42
|
+
saveKey: 'customers.schedule.meeting.save', saveFallback: 'Save activity', saveIcon: Calendar,
|
|
43
|
+
},
|
|
44
|
+
call: {
|
|
45
|
+
titleKey: 'customers.schedule.call.title', titleFallback: 'Log call',
|
|
46
|
+
subtitleKey: 'customers.schedule.call.subtitle', subtitleFallback: 'Log a call you just had or schedule one',
|
|
47
|
+
saveKey: 'customers.schedule.call.save', saveFallback: 'Log call', saveIcon: Phone,
|
|
48
|
+
},
|
|
49
|
+
task: {
|
|
50
|
+
titleKey: 'customers.schedule.task.title', titleFallback: 'New task',
|
|
51
|
+
subtitleKey: 'customers.schedule.task.subtitle', subtitleFallback: 'Capture something to follow up on',
|
|
52
|
+
saveKey: 'customers.schedule.task.save', saveFallback: 'Save task', saveIcon: Check,
|
|
53
|
+
},
|
|
54
|
+
email: {
|
|
55
|
+
titleKey: 'customers.schedule.email.title', titleFallback: 'Compose email',
|
|
56
|
+
subtitleKey: 'customers.schedule.email.subtitle', subtitleFallback: 'Compose and send a tracked email',
|
|
57
|
+
saveKey: 'customers.schedule.email.save', saveFallback: 'Send email', saveIcon: Mail,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CALL_DIRECTIONS: Array<{ key: 'outbound' | 'inbound'; labelKey: string; labelFallback: string; dot: string }> = [
|
|
62
|
+
{ key: 'outbound', labelKey: 'customers.schedule.call.direction.outbound', labelFallback: 'Outbound', dot: 'bg-status-info-icon' },
|
|
63
|
+
{ key: 'inbound', labelKey: 'customers.schedule.call.direction.inbound', labelFallback: 'Inbound', dot: 'bg-status-success-icon' },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const CALL_OUTCOMES: Array<{ key: string; labelKey: string; labelFallback: string; dot: string }> = [
|
|
67
|
+
{ key: 'connected', labelKey: 'customers.schedule.call.outcome.connected', labelFallback: 'Connected', dot: 'bg-status-success-icon' },
|
|
68
|
+
{ key: 'voicemail', labelKey: 'customers.schedule.call.outcome.voicemail', labelFallback: 'Voicemail', dot: 'bg-status-warning-icon' },
|
|
69
|
+
{ key: 'noanswer', labelKey: 'customers.schedule.call.outcome.noAnswer', labelFallback: 'No answer', dot: 'bg-muted-foreground' },
|
|
70
|
+
{ key: 'busy', labelKey: 'customers.schedule.call.outcome.busy', labelFallback: 'Busy', dot: 'bg-status-warning-icon' },
|
|
71
|
+
{ key: 'badnumber', labelKey: 'customers.schedule.call.outcome.badNumber', labelFallback: 'Bad number', dot: 'bg-status-error-icon' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const TASK_PRIORITIES: Array<{ key: string; labelKey: string; labelFallback: string; dot: string }> = [
|
|
75
|
+
{ key: 'low', labelKey: 'customers.schedule.task.priority.low', labelFallback: 'Low', dot: 'bg-muted-foreground' },
|
|
76
|
+
{ key: 'medium', labelKey: 'customers.schedule.task.priority.medium', labelFallback: 'Medium', dot: 'bg-status-info-icon' },
|
|
77
|
+
{ key: 'high', labelKey: 'customers.schedule.task.priority.high', labelFallback: 'High', dot: 'bg-status-warning-icon' },
|
|
78
|
+
{ key: 'urgent', labelKey: 'customers.schedule.task.priority.urgent', labelFallback: 'Urgent', dot: 'bg-status-error-icon' },
|
|
79
|
+
]
|
|
80
|
+
|
|
36
81
|
interface ScheduleActivityDialogProps {
|
|
37
82
|
open: boolean
|
|
38
83
|
onClose: () => void
|
|
@@ -61,6 +106,33 @@ export function ScheduleActivityDialog({
|
|
|
61
106
|
const state = useScheduleFormState({ open, editData: editData ?? null })
|
|
62
107
|
const visibleFields = FIELD_VISIBILITY[state.activityType]
|
|
63
108
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
109
|
+
const isEditing = Boolean(editData?.id)
|
|
110
|
+
const chrome = TYPE_CHROME[state.activityType]
|
|
111
|
+
const SaveIcon = chrome.saveIcon
|
|
112
|
+
const [callDirection, setCallDirection] = React.useState<'outbound' | 'inbound'>('outbound')
|
|
113
|
+
const [callOutcome, setCallOutcome] = React.useState<string | null>(null)
|
|
114
|
+
const [callPhoneNumber, setCallPhoneNumber] = React.useState('')
|
|
115
|
+
const [taskPriority, setTaskPriority] = React.useState<string>('medium')
|
|
116
|
+
|
|
117
|
+
React.useEffect(() => {
|
|
118
|
+
if (!open) return
|
|
119
|
+
const raw = editData as (Record<string, unknown> & { customValues?: unknown }) | null | undefined
|
|
120
|
+
const cv = (raw?.customValues && typeof raw.customValues === 'object' ? raw.customValues : null) as Record<string, unknown> | null
|
|
121
|
+
setCallDirection(typeof cv?.callDirection === 'string' && cv.callDirection === 'inbound' ? 'inbound' : 'outbound')
|
|
122
|
+
setCallOutcome(typeof cv?.callOutcome === 'string' ? cv.callOutcome : null)
|
|
123
|
+
setCallPhoneNumber(typeof cv?.callPhoneNumber === 'string' ? cv.callPhoneNumber : '')
|
|
124
|
+
setTaskPriority(typeof cv?.taskPriority === 'string' ? cv.taskPriority : 'medium')
|
|
125
|
+
}, [open, editData])
|
|
126
|
+
|
|
127
|
+
// Reset per-type chip state when the user switches activity type in create mode.
|
|
128
|
+
// In edit mode, the persisted customValues should win, so we skip the reset.
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
if (!open || isEditing) return
|
|
131
|
+
setCallDirection('outbound')
|
|
132
|
+
setCallOutcome(null)
|
|
133
|
+
setCallPhoneNumber('')
|
|
134
|
+
setTaskPriority('medium')
|
|
135
|
+
}, [state.activityType, open, isEditing])
|
|
64
136
|
|
|
65
137
|
const formSnapshot = React.useMemo(() => JSON.stringify({
|
|
66
138
|
activityType: state.activityType,
|
|
@@ -224,9 +296,18 @@ export function ScheduleActivityDialog({
|
|
|
224
296
|
? buildRecurrenceRule(state.recurrenceDays, state.recurrenceEndType, state.recurrenceCount, state.recurrenceEndDate)
|
|
225
297
|
: null
|
|
226
298
|
|
|
227
|
-
const
|
|
299
|
+
const isSaveEdit = Boolean(editData?.id)
|
|
300
|
+
const customValues: Record<string, unknown> = {}
|
|
301
|
+
if (state.activityType === 'call') {
|
|
302
|
+
customValues.callDirection = callDirection
|
|
303
|
+
if (callOutcome) customValues.callOutcome = callOutcome
|
|
304
|
+
if (callPhoneNumber.trim()) customValues.callPhoneNumber = callPhoneNumber.trim()
|
|
305
|
+
}
|
|
306
|
+
if (state.activityType === 'task') {
|
|
307
|
+
customValues.taskPriority = taskPriority
|
|
308
|
+
}
|
|
228
309
|
const payload = {
|
|
229
|
-
...(
|
|
310
|
+
...(isSaveEdit ? { id: editData!.id } : {}),
|
|
230
311
|
entityId,
|
|
231
312
|
dealId,
|
|
232
313
|
interactionType: state.activityType,
|
|
@@ -250,16 +331,17 @@ export function ScheduleActivityDialog({
|
|
|
250
331
|
: null,
|
|
251
332
|
reminderMinutes: visibleFields.has('reminder') ? state.reminderMinutes : null,
|
|
252
333
|
visibility: visibleFields.has('visibility') ? state.visibility : null,
|
|
334
|
+
...(Object.keys(customValues).length > 0 ? { customValues } : {}),
|
|
253
335
|
}
|
|
254
336
|
await runGuardedMutation(
|
|
255
337
|
() =>
|
|
256
338
|
apiCallOrThrow('/api/customers/interactions', {
|
|
257
|
-
method:
|
|
339
|
+
method: isSaveEdit ? 'PUT' : 'POST',
|
|
258
340
|
headers: { 'content-type': 'application/json' },
|
|
259
341
|
body: JSON.stringify(payload),
|
|
260
342
|
}),
|
|
261
343
|
{
|
|
262
|
-
operation:
|
|
344
|
+
operation: isSaveEdit ? 'updateActivity' : 'createActivity',
|
|
263
345
|
interactionId: editData?.id ?? null,
|
|
264
346
|
interactionType: state.activityType,
|
|
265
347
|
},
|
|
@@ -285,26 +367,26 @@ export function ScheduleActivityDialog({
|
|
|
285
367
|
return (
|
|
286
368
|
<Dialog open={open} onOpenChange={(o) => { if (!o) void guardedClose() }}>
|
|
287
369
|
{ConfirmDialogElement}
|
|
288
|
-
<DialogContent className="flex max-h-[90vh] flex-col overflow-hidden border-border p-0 shadow-xl sm:max-w-[
|
|
370
|
+
<DialogContent className="flex max-h-[90vh] flex-col overflow-hidden border-border p-0 shadow-xl sm:max-w-[760px] sm:rounded-xl [&>[data-dialog-close]]:hidden" onKeyDown={handleKeyDown} aria-describedby={undefined}>
|
|
289
371
|
<VisuallyHidden>
|
|
290
|
-
<DialogTitle>{
|
|
372
|
+
<DialogTitle>{isEditing ? t('customers.schedule.editTitle', 'Edit activity') : t(chrome.titleKey, chrome.titleFallback)}</DialogTitle>
|
|
291
373
|
</VisuallyHidden>
|
|
292
374
|
|
|
293
375
|
{/* Header */}
|
|
294
376
|
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-border bg-background px-6 py-5">
|
|
295
|
-
<div className="flex flex-col gap-1
|
|
296
|
-
<h2 className="text-lg font-
|
|
297
|
-
{
|
|
377
|
+
<div className="flex flex-col gap-1">
|
|
378
|
+
<h2 className="text-lg font-semibold leading-tight tracking-tight text-foreground">
|
|
379
|
+
{isEditing ? t('customers.schedule.editTitle', 'Edit activity') : t(chrome.titleKey, chrome.titleFallback)}
|
|
298
380
|
</h2>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
</
|
|
307
|
-
)}
|
|
381
|
+
<p className="text-sm text-muted-foreground">
|
|
382
|
+
{t(chrome.subtitleKey, chrome.subtitleFallback)}
|
|
383
|
+
</p>
|
|
384
|
+
{entityName ? (
|
|
385
|
+
<p className="mt-0.5 text-xs text-muted-foreground/80">
|
|
386
|
+
{t('customers.schedule.context', 'On timeline: {{name}}', { name: entityName })}
|
|
387
|
+
{companyName ? ` · ${companyName}` : ''}
|
|
388
|
+
</p>
|
|
389
|
+
) : null}
|
|
308
390
|
</div>
|
|
309
391
|
<IconButton type="button" variant="ghost" size="sm" onClick={() => { void guardedClose() }} className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-background" aria-label={t('customers.schedule.cancel', 'Cancel')}>
|
|
310
392
|
<X className="size-4 text-muted-foreground" />
|
|
@@ -325,27 +407,26 @@ export function ScheduleActivityDialog({
|
|
|
325
407
|
</Alert>
|
|
326
408
|
)}
|
|
327
409
|
|
|
328
|
-
{/* Type tabs */}
|
|
329
|
-
<div className="
|
|
410
|
+
{/* Type tabs — large rectangular tiles per Figma */}
|
|
411
|
+
<div className="grid grid-cols-4 gap-2">
|
|
330
412
|
{TYPE_TABS.map(({ type, icon: Icon, labelKey, fallback }) => {
|
|
331
413
|
const isActive = state.activityType === type
|
|
332
414
|
return (
|
|
333
|
-
<
|
|
415
|
+
<button
|
|
334
416
|
key={type}
|
|
335
417
|
type="button"
|
|
336
|
-
variant="ghost"
|
|
337
|
-
size="sm"
|
|
338
418
|
onClick={() => state.setActivityType(type)}
|
|
419
|
+
aria-pressed={isActive}
|
|
339
420
|
className={cn(
|
|
340
|
-
'h-
|
|
421
|
+
'flex h-[80px] flex-col items-center justify-center gap-2 rounded-md border text-[14px] font-semibold transition-colors',
|
|
341
422
|
isActive
|
|
342
|
-
? '
|
|
343
|
-
: '
|
|
423
|
+
? 'border-transparent bg-foreground text-background'
|
|
424
|
+
: 'border-border bg-card text-muted-foreground hover:border-foreground/40 hover:text-foreground',
|
|
344
425
|
)}
|
|
345
426
|
>
|
|
346
|
-
<Icon className="size-
|
|
427
|
+
<Icon className="size-[18px]" />
|
|
347
428
|
{t(labelKey, fallback)}
|
|
348
|
-
</
|
|
429
|
+
</button>
|
|
349
430
|
)
|
|
350
431
|
})}
|
|
351
432
|
</div>
|
|
@@ -359,13 +440,18 @@ export function ScheduleActivityDialog({
|
|
|
359
440
|
type="text"
|
|
360
441
|
value={state.title}
|
|
361
442
|
onChange={(e) => state.setTitle(e.target.value)}
|
|
362
|
-
placeholder={
|
|
443
|
+
placeholder={
|
|
444
|
+
state.activityType === 'email'
|
|
445
|
+
? t('customers.schedule.subjectPlaceholder', 'Subject...')
|
|
446
|
+
: t('customers.schedule.titlePlaceholder', 'Activity title...')
|
|
447
|
+
}
|
|
363
448
|
className="w-full rounded-md border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none focus:border-foreground"
|
|
364
449
|
autoFocus
|
|
365
450
|
/>
|
|
366
451
|
</div>
|
|
367
452
|
|
|
368
|
-
{/* Date/Time/Duration
|
|
453
|
+
{/* Date/Time/Duration — placed before per-type chip rows so the call/task
|
|
454
|
+
workflows match Figma 829:50 / 790:280 (date row first, then status chips). */}
|
|
369
455
|
<DateTimeFields
|
|
370
456
|
visible={visibleFields}
|
|
371
457
|
activityType={state.activityType}
|
|
@@ -389,6 +475,97 @@ export function ScheduleActivityDialog({
|
|
|
389
475
|
setRecurrenceEndDate={state.setRecurrenceEndDate}
|
|
390
476
|
/>
|
|
391
477
|
|
|
478
|
+
{/* Call: Direction + Outcome chips */}
|
|
479
|
+
{state.activityType === 'call' && (
|
|
480
|
+
<div className="flex flex-col gap-3">
|
|
481
|
+
<div>
|
|
482
|
+
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
483
|
+
{t('customers.schedule.call.directionLabel', 'Direction')}
|
|
484
|
+
</label>
|
|
485
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
486
|
+
{CALL_DIRECTIONS.map((opt) => {
|
|
487
|
+
const isActive = callDirection === opt.key
|
|
488
|
+
return (
|
|
489
|
+
<button
|
|
490
|
+
key={opt.key}
|
|
491
|
+
type="button"
|
|
492
|
+
aria-pressed={isActive}
|
|
493
|
+
onClick={() => setCallDirection(opt.key)}
|
|
494
|
+
className={cn(
|
|
495
|
+
'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors',
|
|
496
|
+
isActive
|
|
497
|
+
? 'border-transparent bg-foreground text-background'
|
|
498
|
+
: 'border-border bg-card text-muted-foreground hover:border-foreground/40',
|
|
499
|
+
)}
|
|
500
|
+
>
|
|
501
|
+
<span className={cn('inline-block size-1.5 rounded-full', opt.dot)} aria-hidden />
|
|
502
|
+
{t(opt.labelKey, opt.labelFallback)}
|
|
503
|
+
</button>
|
|
504
|
+
)
|
|
505
|
+
})}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
<div>
|
|
509
|
+
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
510
|
+
{t('customers.schedule.call.outcomeLabel', 'Outcome')}
|
|
511
|
+
</label>
|
|
512
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
513
|
+
{CALL_OUTCOMES.map((opt) => {
|
|
514
|
+
const isActive = callOutcome === opt.key
|
|
515
|
+
return (
|
|
516
|
+
<button
|
|
517
|
+
key={opt.key}
|
|
518
|
+
type="button"
|
|
519
|
+
aria-pressed={isActive}
|
|
520
|
+
onClick={() => setCallOutcome(isActive ? null : opt.key)}
|
|
521
|
+
className={cn(
|
|
522
|
+
'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors',
|
|
523
|
+
isActive
|
|
524
|
+
? 'border-transparent bg-foreground text-background'
|
|
525
|
+
: 'border-border bg-card text-muted-foreground hover:border-foreground/40',
|
|
526
|
+
)}
|
|
527
|
+
>
|
|
528
|
+
<span className={cn('inline-block size-1.5 rounded-full', opt.dot)} aria-hidden />
|
|
529
|
+
{t(opt.labelKey, opt.labelFallback)}
|
|
530
|
+
</button>
|
|
531
|
+
)
|
|
532
|
+
})}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
{/* Task: Priority chips */}
|
|
539
|
+
{state.activityType === 'task' && (
|
|
540
|
+
<div>
|
|
541
|
+
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
542
|
+
{t('customers.schedule.task.priorityLabel', 'Priority')}
|
|
543
|
+
</label>
|
|
544
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
545
|
+
{TASK_PRIORITIES.map((opt) => {
|
|
546
|
+
const isActive = taskPriority === opt.key
|
|
547
|
+
return (
|
|
548
|
+
<button
|
|
549
|
+
key={opt.key}
|
|
550
|
+
type="button"
|
|
551
|
+
aria-pressed={isActive}
|
|
552
|
+
onClick={() => setTaskPriority(opt.key)}
|
|
553
|
+
className={cn(
|
|
554
|
+
'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors',
|
|
555
|
+
isActive
|
|
556
|
+
? 'border-transparent bg-foreground text-background'
|
|
557
|
+
: 'border-border bg-card text-muted-foreground hover:border-foreground/40',
|
|
558
|
+
)}
|
|
559
|
+
>
|
|
560
|
+
<span className={cn('inline-block size-1.5 rounded-full', opt.dot)} aria-hidden />
|
|
561
|
+
{t(opt.labelKey, opt.labelFallback)}
|
|
562
|
+
</button>
|
|
563
|
+
)
|
|
564
|
+
})}
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
)}
|
|
568
|
+
|
|
392
569
|
{/* Participants */}
|
|
393
570
|
<ParticipantsField
|
|
394
571
|
visible={visibleFields}
|
|
@@ -400,13 +577,28 @@ export function ScheduleActivityDialog({
|
|
|
400
577
|
setGuestPermissions={state.setGuestPermissions}
|
|
401
578
|
/>
|
|
402
579
|
|
|
403
|
-
{/* Location */}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
580
|
+
{/* Location (or phone number for calls) */}
|
|
581
|
+
{state.activityType === 'call' ? (
|
|
582
|
+
<div className="flex flex-col gap-1.5">
|
|
583
|
+
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
584
|
+
{t('customers.schedule.call.phoneLabel', 'Phone number')}
|
|
585
|
+
</label>
|
|
586
|
+
<input
|
|
587
|
+
type="tel"
|
|
588
|
+
value={callPhoneNumber}
|
|
589
|
+
onChange={(e) => setCallPhoneNumber(e.target.value)}
|
|
590
|
+
placeholder={t('customers.schedule.call.phonePlaceholder', '+1 555 000 0000')}
|
|
591
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none focus:border-foreground"
|
|
592
|
+
/>
|
|
593
|
+
</div>
|
|
594
|
+
) : (
|
|
595
|
+
<LocationField
|
|
596
|
+
visible={visibleFields}
|
|
597
|
+
activityType={state.activityType}
|
|
598
|
+
location={state.location}
|
|
599
|
+
setLocation={state.setLocation}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
410
602
|
|
|
411
603
|
{/* Linked Entities */}
|
|
412
604
|
<LinkedEntitiesField
|
|
@@ -450,13 +642,13 @@ export function ScheduleActivityDialog({
|
|
|
450
642
|
<Button type="button" variant="outline" onClick={() => { void guardedClose() }} className="rounded-md border border-input bg-background px-5 py-3 text-sm font-semibold text-foreground">
|
|
451
643
|
{t('customers.schedule.cancel', 'Cancel')}
|
|
452
644
|
</Button>
|
|
453
|
-
<Button type="button" onClick={handleSave} disabled={state.saving || !state.title.trim()} className="flex items-center gap-2 rounded-md bg-
|
|
454
|
-
<
|
|
645
|
+
<Button type="button" onClick={handleSave} disabled={state.saving || !state.title.trim()} className="flex items-center gap-2 rounded-md bg-foreground px-5 py-3 text-sm font-semibold text-background hover:bg-foreground/90 disabled:opacity-50">
|
|
646
|
+
<SaveIcon className="size-3.5" />
|
|
455
647
|
{state.saving
|
|
456
648
|
? t('customers.schedule.saving', 'Saving...')
|
|
457
|
-
:
|
|
649
|
+
: isEditing
|
|
458
650
|
? t('customers.schedule.update', 'Update activity')
|
|
459
|
-
: t(
|
|
651
|
+
: t(chrome.saveKey, chrome.saveFallback)}
|
|
460
652
|
</Button>
|
|
461
653
|
</div>
|
|
462
654
|
</DialogContent>
|
|
@@ -80,7 +80,9 @@ export function DateTimeFields({
|
|
|
80
80
|
</div>
|
|
81
81
|
{showStartTime && (
|
|
82
82
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
83
|
-
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
83
|
+
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
84
|
+
{getFieldLabel(activityType, 'startTime', t, 'customers.schedule.start', 'Start')}
|
|
85
|
+
</label>
|
|
84
86
|
<div className="flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2.5">
|
|
85
87
|
<Clock className="size-3.5 text-muted-foreground" />
|
|
86
88
|
<input type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} disabled={allDay} className="flex-1 bg-transparent text-sm text-foreground focus:outline-none disabled:opacity-50" />
|
|
@@ -89,7 +91,9 @@ export function DateTimeFields({
|
|
|
89
91
|
)}
|
|
90
92
|
{showDuration && (
|
|
91
93
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
92
|
-
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
94
|
+
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
95
|
+
{getFieldLabel(activityType, 'duration', t, 'customers.schedule.duration', 'Duration')}
|
|
96
|
+
</label>
|
|
93
97
|
<div className="flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2.5">
|
|
94
98
|
<select
|
|
95
99
|
value={duration}
|
|
@@ -5,7 +5,27 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
5
5
|
import type { ActivityType, ScheduleFieldId } from './fieldConfig'
|
|
6
6
|
import { isVisible, getFieldLabel } from './fieldConfig'
|
|
7
7
|
|
|
8
|
-
const REMINDER_OPTIONS = [0, 5, 10, 15, 30, 60]
|
|
8
|
+
const REMINDER_OPTIONS = [0, 5, 10, 15, 30, 60, 240, 1440]
|
|
9
|
+
|
|
10
|
+
function formatReminderLabel(
|
|
11
|
+
minutes: number,
|
|
12
|
+
t: (key: string, fallback: string, params?: Record<string, string | number>) => string,
|
|
13
|
+
): string {
|
|
14
|
+
if (minutes === 0) return t('customers.schedule.reminder.none', 'None')
|
|
15
|
+
if (minutes >= 1440) {
|
|
16
|
+
const days = Math.round(minutes / 1440)
|
|
17
|
+
return days === 1
|
|
18
|
+
? t('customers.schedule.reminder.dayBefore', '1 day before')
|
|
19
|
+
: t('customers.schedule.reminder.daysBefore', '{days} days before', { days })
|
|
20
|
+
}
|
|
21
|
+
if (minutes >= 60) {
|
|
22
|
+
const hours = Math.round(minutes / 60)
|
|
23
|
+
return hours === 1
|
|
24
|
+
? t('customers.schedule.reminder.hourBefore', '1 hour before')
|
|
25
|
+
: t('customers.schedule.reminder.hoursBefore', '{hours} hours before', { hours })
|
|
26
|
+
}
|
|
27
|
+
return t('customers.schedule.reminder.minutesBefore', '{minutes} min before', { minutes })
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
interface FooterFieldsProps {
|
|
11
31
|
visible: Set<ScheduleFieldId>
|
|
@@ -47,7 +67,7 @@ export function FooterFields({
|
|
|
47
67
|
>
|
|
48
68
|
{REMINDER_OPTIONS.map((m) => (
|
|
49
69
|
<option key={m} value={m}>
|
|
50
|
-
{m
|
|
70
|
+
{formatReminderLabel(m, t)}
|
|
51
71
|
</option>
|
|
52
72
|
))}
|
|
53
73
|
</select>
|
|
@@ -9,7 +9,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
9
9
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
10
10
|
import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
|
|
11
11
|
import type { ActivityType, ScheduleFieldId } from './fieldConfig'
|
|
12
|
-
import { isVisible } from './fieldConfig'
|
|
12
|
+
import { isVisible, getFieldLabel } from './fieldConfig'
|
|
13
13
|
import type { LinkedEntity } from './useScheduleFormState'
|
|
14
14
|
|
|
15
15
|
const ENTITY_LINK_TYPES = ['company', 'deal', 'offer'] as const
|
|
@@ -242,10 +242,18 @@ export function LinkedEntitiesField({
|
|
|
242
242
|
|
|
243
243
|
if (!isVisible(activityType, 'linkedEntities')) return null
|
|
244
244
|
|
|
245
|
+
const sectionLabel = getFieldLabel(
|
|
246
|
+
activityType,
|
|
247
|
+
'linkedEntities',
|
|
248
|
+
t,
|
|
249
|
+
'customers.schedule.linkedEntities',
|
|
250
|
+
'Linked entities',
|
|
251
|
+
)
|
|
252
|
+
|
|
245
253
|
return (
|
|
246
254
|
<div>
|
|
247
255
|
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
248
|
-
{
|
|
256
|
+
{sectionLabel}
|
|
249
257
|
</label>
|
|
250
258
|
<div className="mt-2.5 flex flex-wrap content-center items-center gap-2">
|
|
251
259
|
{linkedEntities.map((entity) => (
|
|
@@ -9,7 +9,7 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
|
|
10
10
|
import { fetchAssignableStaffMembersPage } from '../assignableStaff'
|
|
11
11
|
import type { ActivityType, ScheduleFieldId } from './fieldConfig'
|
|
12
|
-
import { isVisible } from './fieldConfig'
|
|
12
|
+
import { isVisible, getFieldLabel } from './fieldConfig'
|
|
13
13
|
import type { Participant, RsvpStatus } from './useScheduleFormState'
|
|
14
14
|
import { PARTICIPANT_COLORS } from './useScheduleFormState'
|
|
15
15
|
|
|
@@ -186,10 +186,18 @@ export function ParticipantsField({
|
|
|
186
186
|
|
|
187
187
|
if (!isVisible(activityType, 'participants')) return null
|
|
188
188
|
|
|
189
|
+
const sectionLabel = getFieldLabel(
|
|
190
|
+
activityType,
|
|
191
|
+
'participants',
|
|
192
|
+
t,
|
|
193
|
+
'customers.schedule.participants',
|
|
194
|
+
'Participants',
|
|
195
|
+
)
|
|
196
|
+
|
|
189
197
|
return (
|
|
190
198
|
<div>
|
|
191
199
|
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
192
|
-
{
|
|
200
|
+
{sectionLabel}
|
|
193
201
|
</label>
|
|
194
202
|
<div className="mt-2.5 flex flex-wrap content-center items-center gap-2 rounded-lg border border-border bg-background px-3 py-2.5">
|
|
195
203
|
{participants.map((p) => (
|
|
@@ -27,29 +27,49 @@ export const FIELD_VISIBILITY: Record<ActivityType, Set<ScheduleFieldId>> = {
|
|
|
27
27
|
'participants', 'linkedEntities', 'description',
|
|
28
28
|
'reminder', 'visibility',
|
|
29
29
|
]),
|
|
30
|
+
// Task: now also surfaces Due time (startTime) + Estimate (duration) per Figma 790:280.
|
|
30
31
|
task: new Set([
|
|
31
|
-
'title', 'date',
|
|
32
|
+
'title', 'date', 'startTime', 'duration',
|
|
32
33
|
'linkedEntities', 'description',
|
|
33
34
|
'reminder', 'visibility',
|
|
34
35
|
]),
|
|
36
|
+
// Email: surface participants as TO recipients per Figma 790:510.
|
|
35
37
|
email: new Set([
|
|
36
38
|
'title', 'date', 'startTime',
|
|
37
|
-
'linkedEntities', 'description',
|
|
38
|
-
'visibility',
|
|
39
|
+
'participants', 'linkedEntities', 'description',
|
|
40
|
+
'reminder', 'visibility',
|
|
39
41
|
]),
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
type LabelOverride = { key: string; fallback: string }
|
|
43
45
|
|
|
46
|
+
// Per-type section labels (Figma 784:1255 / 829:50 / 790:280 / 790:510).
|
|
47
|
+
// `participants` / `linkedEntities` / `description` resolve via these overrides
|
|
48
|
+
// when present; otherwise the field components fall back to their generic key.
|
|
44
49
|
export const FIELD_LABEL_OVERRIDES: Partial<
|
|
45
50
|
Record<ActivityType, Partial<Record<ScheduleFieldId, LabelOverride>>>
|
|
46
51
|
> = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
meeting: {
|
|
53
|
+
participants: { key: 'customers.schedule.attendees', fallback: 'Attendees' },
|
|
54
|
+
linkedEntities: { key: 'customers.schedule.connections', fallback: 'Connections' },
|
|
55
|
+
},
|
|
56
|
+
call: {
|
|
57
|
+
participants: { key: 'customers.schedule.contact', fallback: 'Contact' },
|
|
58
|
+
linkedEntities: { key: 'customers.schedule.connections', fallback: 'Connections' },
|
|
59
|
+
description: { key: 'customers.schedule.callNotes', fallback: 'Call notes' },
|
|
50
60
|
},
|
|
51
61
|
task: {
|
|
52
62
|
date: { key: 'customers.schedule.dueDate', fallback: 'Due date' },
|
|
63
|
+
startTime: { key: 'customers.schedule.dueTime', fallback: 'Due time' },
|
|
64
|
+
duration: { key: 'customers.schedule.estimate', fallback: 'Estimate' },
|
|
65
|
+
linkedEntities: { key: 'customers.schedule.connections', fallback: 'Connections' },
|
|
66
|
+
description: { key: 'customers.schedule.details', fallback: 'Details' },
|
|
67
|
+
},
|
|
68
|
+
email: {
|
|
69
|
+
title: { key: 'customers.schedule.subject', fallback: 'Subject' },
|
|
70
|
+
participants: { key: 'customers.schedule.to', fallback: 'To' },
|
|
71
|
+
linkedEntities: { key: 'customers.schedule.connections', fallback: 'Connections' },
|
|
72
|
+
description: { key: 'customers.schedule.message', fallback: 'Message' },
|
|
53
73
|
},
|
|
54
74
|
}
|
|
55
75
|
|
|
@@ -44,6 +44,18 @@ export const PARTICIPANT_COLORS = [
|
|
|
44
44
|
'bg-chart-teal',
|
|
45
45
|
]
|
|
46
46
|
|
|
47
|
+
// Per-Figma defaults for the Reminder dropdown when the user picks an activity
|
|
48
|
+
// type. Meeting/email keep the standard 15 min; tasks default to 1 day (1440 min)
|
|
49
|
+
// because they're plan-ahead artefacts; calls default to 5 min as a stand-in for
|
|
50
|
+
// the Figma "After call ends" treatment (which would need a non-numeric sentinel
|
|
51
|
+
// in the API contract — tracked as a follow-up).
|
|
52
|
+
const DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {
|
|
53
|
+
meeting: 15,
|
|
54
|
+
call: 5,
|
|
55
|
+
task: 1440,
|
|
56
|
+
email: 15,
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
interface UseScheduleFormStateParams {
|
|
48
60
|
open: boolean
|
|
49
61
|
editData: ScheduleActivityEditData | null | undefined
|
|
@@ -76,7 +88,8 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
76
88
|
if (open) {
|
|
77
89
|
if (editData) {
|
|
78
90
|
// Edit mode: populate from existing interaction
|
|
79
|
-
|
|
91
|
+
const resolvedType = (editData.interactionType as ActivityType) ?? 'meeting'
|
|
92
|
+
setActivityType(resolvedType)
|
|
80
93
|
setTitle(editData.title ?? '')
|
|
81
94
|
const scheduledDate = editData.scheduledAt ? new Date(editData.scheduledAt) : new Date()
|
|
82
95
|
setDate(scheduledDate.toISOString().slice(0, 10))
|
|
@@ -85,7 +98,9 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
85
98
|
setAllDay(editData.allDay ?? false)
|
|
86
99
|
setDescription(editData.body ?? '')
|
|
87
100
|
setLocation(editData.location ?? '')
|
|
88
|
-
|
|
101
|
+
// Use per-type default when the editData omits an explicit reminder
|
|
102
|
+
// (the menu-driven "New X" flow opens the dialog with `reminderMinutes: null`).
|
|
103
|
+
setReminderMinutes(editData.reminderMinutes ?? DEFAULT_REMINDER_MINUTES[resolvedType])
|
|
89
104
|
setVisibility(editData.visibility ?? 'team')
|
|
90
105
|
setParticipants(
|
|
91
106
|
Array.isArray(editData.participants)
|
|
@@ -145,7 +160,7 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
145
160
|
setAllDay(false)
|
|
146
161
|
setDescription('')
|
|
147
162
|
setLocation('')
|
|
148
|
-
setReminderMinutes(
|
|
163
|
+
setReminderMinutes(DEFAULT_REMINDER_MINUTES.meeting)
|
|
149
164
|
setVisibility('team')
|
|
150
165
|
setParticipants([])
|
|
151
166
|
setLinkedEntities([])
|
|
@@ -160,6 +175,20 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
160
175
|
}
|
|
161
176
|
}, [open, editData])
|
|
162
177
|
|
|
178
|
+
// Update the Reminder default when the activity type changes in create mode.
|
|
179
|
+
// Skipped in edit mode (the persisted value wins), and gated by `open` to
|
|
180
|
+
// avoid flipping the default in a closed-but-mounted dialog.
|
|
181
|
+
const lastReminderTypeRef = React.useRef<ActivityType>('meeting')
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (!open || editData) {
|
|
184
|
+
lastReminderTypeRef.current = activityType
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
if (lastReminderTypeRef.current === activityType) return
|
|
188
|
+
lastReminderTypeRef.current = activityType
|
|
189
|
+
setReminderMinutes(DEFAULT_REMINDER_MINUTES[activityType])
|
|
190
|
+
}, [activityType, editData, open])
|
|
191
|
+
|
|
163
192
|
const removeParticipant = React.useCallback((userId: string) => {
|
|
164
193
|
setParticipants((prev) => prev.filter((p) => p.userId !== userId))
|
|
165
194
|
}, [])
|