@open-mercato/core 0.5.1-develop.2996.ce62fd491c → 0.5.1-develop.3036.f02c281f23

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 (81) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/sidebar/preferences/route.js +2 -2
  3. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  4. package/dist/modules/auth/api/sidebar/variants/[id]/route.js +2 -2
  5. package/dist/modules/auth/api/sidebar/variants/[id]/route.js.map +2 -2
  6. package/dist/modules/auth/api/sidebar/variants/route.js +1 -1
  7. package/dist/modules/auth/api/sidebar/variants/route.js.map +2 -2
  8. package/dist/modules/auth/backend/sidebar-customization/page.meta.js +1 -0
  9. package/dist/modules/auth/backend/sidebar-customization/page.meta.js.map +2 -2
  10. package/dist/modules/customers/api/companies/[id]/route.js +30 -20
  11. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  12. package/dist/modules/customers/api/companies/route.js +12 -7
  13. package/dist/modules/customers/api/companies/route.js.map +2 -2
  14. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +12 -7
  15. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  16. package/dist/modules/customers/api/people/route.js +12 -7
  17. package/dist/modules/customers/api/people/route.js.map +2 -2
  18. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +21 -0
  19. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +27 -30
  21. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +56 -0
  23. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +7 -0
  24. package/dist/modules/customers/components/detail/ActivitiesCard.js +175 -0
  25. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +7 -0
  26. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +324 -0
  27. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +7 -0
  28. package/dist/modules/customers/components/detail/ActivitiesSection.js +62 -13
  29. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  30. package/dist/modules/customers/components/detail/ActivityLogTab.js +14 -23
  31. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
  32. package/dist/modules/customers/components/detail/ActivityTimeline.js +13 -13
  33. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  34. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +35 -22
  35. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
  36. package/dist/modules/customers/components/detail/AiActionChips.js +15 -22
  37. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  38. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +196 -28
  39. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  40. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -2
  41. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  42. package/dist/modules/customers/components/detail/schedule/FooterFields.js +14 -2
  43. package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +2 -2
  44. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +9 -2
  45. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
  46. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +9 -2
  47. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  48. package/dist/modules/customers/components/detail/schedule/fieldConfig.js +25 -4
  49. package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
  50. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +20 -3
  51. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  52. package/package.json +3 -3
  53. package/src/modules/auth/api/sidebar/preferences/route.ts +2 -2
  54. package/src/modules/auth/api/sidebar/variants/[id]/route.ts +2 -2
  55. package/src/modules/auth/api/sidebar/variants/route.ts +1 -1
  56. package/src/modules/auth/backend/sidebar-customization/page.meta.ts +1 -8
  57. package/src/modules/customers/api/companies/[id]/route.ts +30 -20
  58. package/src/modules/customers/api/companies/route.ts +12 -7
  59. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +12 -7
  60. package/src/modules/customers/api/people/route.ts +12 -7
  61. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +22 -0
  62. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +28 -21
  63. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +67 -0
  64. package/src/modules/customers/components/detail/ActivitiesCard.tsx +231 -0
  65. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +390 -0
  66. package/src/modules/customers/components/detail/ActivitiesSection.tsx +91 -40
  67. package/src/modules/customers/components/detail/ActivityLogTab.tsx +25 -23
  68. package/src/modules/customers/components/detail/ActivityTimeline.tsx +15 -19
  69. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +36 -29
  70. package/src/modules/customers/components/detail/AiActionChips.tsx +17 -23
  71. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +233 -41
  72. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +6 -2
  73. package/src/modules/customers/components/detail/schedule/FooterFields.tsx +22 -2
  74. package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +10 -2
  75. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +10 -2
  76. package/src/modules/customers/components/detail/schedule/fieldConfig.ts +26 -6
  77. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +32 -3
  78. package/src/modules/customers/i18n/de.json +69 -2
  79. package/src/modules/customers/i18n/en.json +69 -2
  80. package/src/modules/customers/i18n/es.json +69 -2
  81. package/src/modules/customers/i18n/pl.json +68 -1
@@ -80,52 +80,48 @@ function TimelineEntry({
80
80
 
81
81
  return (
82
82
  <div
83
- className={`px-5 py-4 ${withBorder ? 'border-b border-border/60' : ''} ${onEdit ? 'cursor-pointer hover:bg-accent/50 transition-colors' : ''}`}
83
+ className={`py-2.5 ${withBorder ? 'border-b border-border/60' : ''} ${onEdit ? 'cursor-pointer hover:bg-accent/40 transition-colors' : ''}`}
84
84
  onClick={() => onEdit?.(activity)}
85
85
  role={onEdit ? 'button' : undefined}
86
86
  tabIndex={onEdit ? 0 : undefined}
87
87
  onKeyDown={onEdit ? (e) => { if (e.key === 'Enter') onEdit(activity) } : undefined}
88
88
  >
89
- <div className="grid items-start gap-3" style={{ gridTemplateColumns: '72px 40px 1fr' }}>
89
+ <div className="grid items-start gap-3" style={{ gridTemplateColumns: '75px 32px 1fr' }}>
90
90
  {/* Column 1: Date */}
91
- <div className="shrink-0 pt-0.5">
92
- <span className="block text-xs font-bold leading-tight">
91
+ <div className="shrink-0">
92
+ <span className="block text-[11px] font-semibold leading-tight text-foreground">
93
93
  {formatRelativeDate(dateStr, t)}
94
94
  </span>
95
- <span className="block text-xs leading-tight text-muted-foreground">
95
+ <span className="block text-[10px] leading-tight text-muted-foreground">
96
96
  {formatTime(dateStr)}
97
97
  </span>
98
98
  </div>
99
99
 
100
100
  {/* Column 2: Type icon */}
101
- <div className="flex size-10 items-center justify-center rounded-lg bg-muted shrink-0">
102
- {TypeIcon ? <TypeIcon className="size-4 text-muted-foreground" /> : null}
101
+ <div className="flex size-8 items-center justify-center rounded-md bg-muted shrink-0">
102
+ {TypeIcon ? <TypeIcon className="size-3.5 text-muted-foreground" /> : null}
103
103
  </div>
104
104
 
105
105
  {/* Column 3: Content */}
106
- <div className="min-w-0">
107
- <div className="flex items-center gap-1.5">
108
- <span className="text-sm font-semibold leading-5">
109
- {title}{duration}
110
- </span>
111
- </div>
106
+ <div className="min-w-0 space-y-1.5">
107
+ <span className="block text-[12px] font-semibold leading-tight text-foreground">
108
+ {title}{duration}
109
+ </span>
112
110
 
113
111
  {activity.body && activity.title && (
114
- <p className="mt-1 text-sm text-muted-foreground">
112
+ <p className="text-[11px] leading-snug text-muted-foreground">
115
113
  {activity.body}
116
114
  </p>
117
115
  )}
118
116
 
119
117
  {activity.authorName && (
120
- <div className="mt-1.5 flex items-center gap-1 text-xs text-muted-foreground">
121
- <User className="size-3 shrink-0" />
118
+ <div className="flex items-center gap-1 text-[10px] text-muted-foreground">
119
+ <User className="size-2.5 shrink-0" />
122
120
  <span>{t('customers.timeline.author', 'by {{name}}', { name: activity.authorName })}</span>
123
121
  </div>
124
122
  )}
125
123
 
126
- <div className="mt-2">
127
- <AiActionChips activityType={activity.interactionType} />
128
- </div>
124
+ <AiActionChips activityType={activity.interactionType} />
129
125
  </div>
130
126
  </div>
131
127
  </div>
@@ -4,14 +4,15 @@ import { Phone, Mail, Users, StickyNote, SlidersHorizontal } from 'lucide-react'
4
4
  import { cn } from '@open-mercato/shared/lib/utils'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
7
8
  import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
8
9
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
9
10
 
10
11
  const FILTER_TYPES = [
12
+ { type: 'note', icon: StickyNote },
11
13
  { type: 'call', icon: Phone },
12
- { type: 'email', icon: Mail },
13
14
  { type: 'meeting', icon: Users },
14
- { type: 'note', icon: StickyNote },
15
+ { type: 'email', icon: Mail },
15
16
  ] as const
16
17
 
17
18
  type InteractionCounts = {
@@ -33,6 +34,10 @@ interface ActivityTimelineFiltersProps {
33
34
  onReset: () => void
34
35
  }
35
36
 
37
+ const CHIP_BASE = 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors'
38
+ const CHIP_INACTIVE = 'border border-border bg-card text-muted-foreground hover:bg-accent/40'
39
+ const CHIP_ACTIVE = 'border border-status-info-border bg-status-info-bg text-status-info-text'
40
+
36
41
  export function ActivityTimelineFilters({
37
42
  entityId,
38
43
  activeTypes,
@@ -45,6 +50,7 @@ export function ActivityTimelineFilters({
45
50
  }: ActivityTimelineFiltersProps) {
46
51
  const t = useT()
47
52
  const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo
53
+ const allActive = activeTypes.length === 0
48
54
  const [counts, setCounts] = React.useState<InteractionCounts | null>(null)
49
55
 
50
56
  React.useEffect(() => {
@@ -72,54 +78,55 @@ export function ActivityTimelineFilters({
72
78
  }
73
79
  }, [activeTypes, onTypesChange])
74
80
 
81
+ const handleSelectAll = React.useCallback(() => {
82
+ onTypesChange([])
83
+ }, [onTypesChange])
84
+
75
85
  return (
76
- <div className="flex flex-col gap-3 border-b border-border/60 pb-4 md:flex-row md:items-center md:justify-between">
77
- <div className="flex flex-wrap items-center gap-2">
78
- <span className="text-overline font-bold uppercase tracking-wider text-muted-foreground">
79
- {t('customers.people.detail.activities.filterLabel', 'FILTER:')}
80
- </span>
86
+ <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
87
+ <div className="flex flex-wrap items-center gap-2.5">
88
+ <button
89
+ type="button"
90
+ onClick={handleSelectAll}
91
+ aria-pressed={allActive}
92
+ className={cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE)}
93
+ >
94
+ <span>{t('customers.timeline.filter.all', 'All Activities')}</span>
95
+ </button>
81
96
 
82
97
  {FILTER_TYPES.map(({ type, icon: Icon }) => {
83
98
  const isActive = activeTypes.includes(type)
84
99
  const count = counts?.[type as keyof InteractionCounts]
100
+ const hasCount = typeof count === 'number' && count > 0
85
101
  return (
86
- <Button
102
+ <button
87
103
  key={type}
88
104
  type="button"
89
- variant="outline"
90
- size="sm"
91
105
  onClick={() => handleTypeToggle(type)}
92
- className={cn(
93
- 'h-7 rounded-full px-2.5 text-xs gap-1.5',
94
- isActive
95
- ? 'border-foreground bg-background text-foreground'
96
- : 'border-border bg-background text-muted-foreground',
97
- )}
98
106
  aria-pressed={isActive}
107
+ className={cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE)}
99
108
  >
100
- <Icon className="size-2.5" />
101
- <span className="font-semibold">{t(`customers.timeline.filter.${type}`, type)}</span>
102
- {typeof count === 'number' && count > 0 ? (
103
- <span className="rounded-full bg-muted px-1 text-overline leading-4 text-muted-foreground">
104
- {count}
105
- </span>
106
- ) : null}
107
- </Button>
109
+ <Icon className="size-[18px] shrink-0" />
110
+ <span>
111
+ {t(`customers.timeline.filter.${type}`, type)}
112
+ {hasCount ? ` ${count}` : ''}
113
+ </span>
114
+ </button>
108
115
  )
109
116
  })}
110
117
  </div>
111
118
 
112
119
  <Popover>
113
120
  <PopoverTrigger asChild>
114
- <Button
121
+ <IconButton
115
122
  type="button"
116
123
  variant="outline"
117
124
  size="sm"
118
- className="h-7 rounded-md px-2.5 text-xs text-muted-foreground"
125
+ className="size-7 rounded-md text-muted-foreground"
126
+ aria-label={t('customers.people.detail.activities.moreFilters', 'More filters')}
119
127
  >
120
- <SlidersHorizontal className="size-2.5" />
121
- {t('customers.people.detail.activities.moreFilters', 'More')}
122
- </Button>
128
+ <SlidersHorizontal className="size-3.5" />
129
+ </IconButton>
123
130
  </PopoverTrigger>
124
131
  <PopoverContent align="end" className="w-72 space-y-3">
125
132
  <div className="space-y-1.5">
@@ -3,7 +3,6 @@
3
3
  import * as React from 'react'
4
4
  import { Sparkles } from 'lucide-react'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
- import { Button } from '@open-mercato/ui/primitives/button'
7
6
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@open-mercato/ui/primitives/tooltip'
8
7
  import { AI_TIMELINE_ACTIONS_BY_TYPE, resolveAiActions } from './aiActionCatalog'
9
8
 
@@ -17,30 +16,25 @@ export function AiActionChips({ activityType }: AiActionChipsProps) {
17
16
 
18
17
  return (
19
18
  <TooltipProvider delayDuration={300}>
20
- <div className="flex items-center gap-0.5">
21
- <span className="mr-1 text-xs text-muted-foreground">
19
+ <div className="flex items-center gap-1.5">
20
+ <span className="text-[9px] font-bold text-muted-foreground/70">
22
21
  {t('customers.ai.prefix', 'AI:')}
23
22
  </span>
24
- {actions.map((action, index) => (
25
- <React.Fragment key={action.key}>
26
- {index > 0 && <span className="text-xs text-muted-foreground/40">|</span>}
27
- <Tooltip>
28
- <TooltipTrigger asChild>
29
- <Button
30
- type="button"
31
- variant="ghost"
32
- size="sm"
33
- className="h-auto inline-flex items-center gap-0.5 px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
34
- >
35
- <Sparkles className="size-2.5" />
36
- {t(action.i18nKey, action.fallback)}
37
- </Button>
38
- </TooltipTrigger>
39
- <TooltipContent side="bottom" className="text-xs">
40
- {t('customers.ai.comingSoon', 'Coming soon')}
41
- </TooltipContent>
42
- </Tooltip>
43
- </React.Fragment>
23
+ {actions.map((action) => (
24
+ <Tooltip key={action.key}>
25
+ <TooltipTrigger asChild>
26
+ <button
27
+ type="button"
28
+ className="inline-flex items-center gap-1 rounded-[4px] border border-dashed border-border bg-card pl-1.5 pr-[7px] py-[3px] text-[9px] font-medium text-muted-foreground/70 transition-colors hover:border-muted-foreground hover:text-foreground"
29
+ >
30
+ <Sparkles className="size-2.5 shrink-0" />
31
+ {t(action.i18nKey, action.fallback)}
32
+ </button>
33
+ </TooltipTrigger>
34
+ <TooltipContent side="bottom" className="text-xs">
35
+ {t('customers.ai.comingSoon', 'Coming soon')}
36
+ </TooltipContent>
37
+ </Tooltip>
44
38
  ))}
45
39
  </div>
46
40
  </TooltipProvider>
@@ -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}