@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.
Files changed (69) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/api/companies/[id]/route.js +30 -20
  3. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  4. package/dist/modules/customers/api/companies/route.js +12 -7
  5. package/dist/modules/customers/api/companies/route.js.map +2 -2
  6. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +12 -7
  7. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  8. package/dist/modules/customers/api/people/route.js +12 -7
  9. package/dist/modules/customers/api/people/route.js.map +2 -2
  10. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +21 -0
  11. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  12. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +27 -30
  13. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  14. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +56 -0
  15. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +7 -0
  16. package/dist/modules/customers/components/detail/ActivitiesCard.js +175 -0
  17. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +7 -0
  18. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +324 -0
  19. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +7 -0
  20. package/dist/modules/customers/components/detail/ActivitiesSection.js +62 -13
  21. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivityLogTab.js +14 -23
  23. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
  24. package/dist/modules/customers/components/detail/ActivityTimeline.js +13 -13
  25. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  26. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +35 -22
  27. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
  28. package/dist/modules/customers/components/detail/AiActionChips.js +15 -22
  29. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  30. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +196 -28
  31. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  32. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -2
  33. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  34. package/dist/modules/customers/components/detail/schedule/FooterFields.js +14 -2
  35. package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +2 -2
  36. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +9 -2
  37. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
  38. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +9 -2
  39. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  40. package/dist/modules/customers/components/detail/schedule/fieldConfig.js +25 -4
  41. package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
  42. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +20 -3
  43. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  44. package/package.json +3 -3
  45. package/src/modules/customers/api/companies/[id]/route.ts +30 -20
  46. package/src/modules/customers/api/companies/route.ts +12 -7
  47. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +12 -7
  48. package/src/modules/customers/api/people/route.ts +12 -7
  49. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +22 -0
  50. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +28 -21
  51. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +67 -0
  52. package/src/modules/customers/components/detail/ActivitiesCard.tsx +231 -0
  53. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +390 -0
  54. package/src/modules/customers/components/detail/ActivitiesSection.tsx +91 -40
  55. package/src/modules/customers/components/detail/ActivityLogTab.tsx +25 -23
  56. package/src/modules/customers/components/detail/ActivityTimeline.tsx +15 -19
  57. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +36 -29
  58. package/src/modules/customers/components/detail/AiActionChips.tsx +17 -23
  59. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +233 -41
  60. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +6 -2
  61. package/src/modules/customers/components/detail/schedule/FooterFields.tsx +22 -2
  62. package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +10 -2
  63. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +10 -2
  64. package/src/modules/customers/components/detail/schedule/fieldConfig.ts +26 -6
  65. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +32 -3
  66. package/src/modules/customers/i18n/de.json +69 -2
  67. package/src/modules/customers/i18n/en.json +69 -2
  68. package/src/modules/customers/i18n/es.json +69 -2
  69. 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 isEditing = Boolean(editData?.id)
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
- ...(isEditing ? { id: editData!.id } : {}),
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: isEditing ? 'PUT' : 'POST',
339
+ method: isSaveEdit ? 'PUT' : 'POST',
258
340
  headers: { 'content-type': 'application/json' },
259
341
  body: JSON.stringify(payload),
260
342
  }),
261
343
  {
262
- operation: isEditing ? 'updateActivity' : 'createActivity',
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-[680px] sm:rounded-xl [&>[data-dialog-close]]:hidden" onKeyDown={handleKeyDown} aria-describedby={undefined}>
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>{editData ? t('customers.schedule.editTitle', 'Edit activity') : t('customers.schedule.title', 'Schedule activity')}</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.5">
296
- <h2 className="text-lg font-bold leading-tight text-foreground">
297
- {editData ? t('customers.schedule.editTitle', 'Edit activity') : t('customers.schedule.title', 'Schedule activity')}
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
- {entityName && (
300
- <div className="flex items-center gap-1.5">
301
- <span className="inline-block size-3.5 rounded-full bg-status-success-icon shrink-0" />
302
- <span className="text-xs text-muted-foreground">
303
- {t('customers.schedule.context', 'On timeline: {{name}}', { name: entityName })}
304
- {companyName && ` · ${companyName}`}
305
- </span>
306
- </div>
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="flex gap-0.5 rounded-md border border-border bg-muted p-1">
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
- <Button
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-auto flex items-center gap-2 rounded-md px-3.5 py-2 text-sm transition-colors',
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
- ? 'bg-background font-semibold text-foreground shadow-sm'
343
- : 'bg-transparent font-normal text-muted-foreground',
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-3.5" />
427
+ <Icon className="size-[18px]" />
347
428
  {t(labelKey, fallback)}
348
- </Button>
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={t('customers.schedule.titlePlaceholder', 'Activity title...')}
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
- <LocationField
405
- visible={visibleFields}
406
- activityType={state.activityType}
407
- location={state.location}
408
- setLocation={state.setLocation}
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-primary px-5 py-3 text-sm font-semibold text-primary-foreground disabled:opacity-50">
454
- <Calendar className="size-3.5" />
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
- : editData
649
+ : isEditing
458
650
  ? t('customers.schedule.update', 'Update activity')
459
- : t('customers.schedule.save', 'Save activity')}
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">{t('customers.schedule.start', 'Start')}</label>
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">{t('customers.schedule.duration', 'Duration')}</label>
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 === 0 ? t('customers.schedule.reminder.none', 'None') : `${m} min`}
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
- {t('customers.schedule.linkedEntities', 'Linked entities')}
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
- {t('customers.schedule.participants', 'Participants')}
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
- email: {
48
- title: { key: 'customers.schedule.subject', fallback: 'Subject' },
49
- description: { key: 'customers.schedule.body', fallback: 'Body' },
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
- setActivityType((editData.interactionType as ActivityType) ?? 'meeting')
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
- setReminderMinutes(editData.reminderMinutes ?? 15)
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(15)
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
  }, [])