@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/sidebar/preferences/route.js +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js +2 -2
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js.map +2 -2
- package/dist/modules/auth/api/sidebar/variants/route.js +1 -1
- package/dist/modules/auth/api/sidebar/variants/route.js.map +2 -2
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js +1 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js.map +2 -2
- 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/auth/api/sidebar/preferences/route.ts +2 -2
- package/src/modules/auth/api/sidebar/variants/[id]/route.ts +2 -2
- package/src/modules/auth/api/sidebar/variants/route.ts +1 -1
- package/src/modules/auth/backend/sidebar-customization/page.meta.ts +1 -8
- 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
|
@@ -80,52 +80,48 @@ function TimelineEntry({
|
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
82
|
<div
|
|
83
|
-
className={`
|
|
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: '
|
|
89
|
+
<div className="grid items-start gap-3" style={{ gridTemplateColumns: '75px 32px 1fr' }}>
|
|
90
90
|
{/* Column 1: Date */}
|
|
91
|
-
<div className="shrink-0
|
|
92
|
-
<span className="block text-
|
|
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-
|
|
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-
|
|
102
|
-
{TypeIcon ? <TypeIcon className="size-
|
|
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
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
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="
|
|
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="
|
|
121
|
-
<User className="size-
|
|
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
|
-
<
|
|
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: '
|
|
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
|
|
77
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
<
|
|
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-
|
|
101
|
-
<span
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
<
|
|
121
|
+
<IconButton
|
|
115
122
|
type="button"
|
|
116
123
|
variant="outline"
|
|
117
124
|
size="sm"
|
|
118
|
-
className="
|
|
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-
|
|
121
|
-
|
|
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-
|
|
21
|
-
<span className="
|
|
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
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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}
|