@open-mercato/core 0.6.1-develop.3081.21270ec58a → 0.6.1-develop.3090.06ab462170
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/audit_logs/services/accessLogService.js +13 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +6 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +1 -52
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +2 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +21 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/data/validators.js +74 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
- package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
- package/dist/modules/integrations/lib/credentials-service.js +12 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +29 -14
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/lib/actions.js +24 -4
- package/dist/modules/messages/lib/actions.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +49 -36
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/audit_logs/services/accessLogService.ts +20 -0
- package/src/modules/audit_logs/services/actionLogService.ts +13 -5
- package/src/modules/customers/api/activities/route.ts +1 -76
- package/src/modules/customers/api/interactions/counts/route.ts +2 -1
- package/src/modules/customers/api/interactions/route.ts +28 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
- package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
- package/src/modules/customers/data/validators.ts +85 -2
- package/src/modules/customers/i18n/de.json +11 -0
- package/src/modules/customers/i18n/en.json +11 -0
- package/src/modules/customers/i18n/es.json +11 -0
- package/src/modules/customers/i18n/pl.json +11 -0
- package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
- package/src/modules/integrations/lib/credentials-service.ts +15 -1
- package/src/modules/messages/commands/actions.ts +28 -13
- package/src/modules/messages/lib/actions.ts +34 -3
- package/src/modules/sales/api/documents/factory.ts +55 -38
|
@@ -2,15 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'
|
|
5
|
+
import { toZonedTime } from 'date-fns-tz'
|
|
5
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
8
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
9
|
+
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
10
|
import { ActivitiesDayStrip } from './ActivitiesDayStrip'
|
|
9
11
|
import { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'
|
|
10
12
|
import type { InteractionSummary } from './types'
|
|
11
13
|
|
|
12
14
|
interface ActivitiesCardProps {
|
|
13
15
|
entityId: string
|
|
16
|
+
/**
|
|
17
|
+
* Initial planned activities (from the parent route's `plannedActivitiesPreview`).
|
|
18
|
+
* Used as the seed value before the broader `/api/customers/interactions` fetch
|
|
19
|
+
* resolves, and as the fallback when the fetch fails. The card always prefers
|
|
20
|
+
* its own fetched window (issue #1809 — fixes E1 status alignment and E2 type
|
|
21
|
+
* coverage by sourcing from the same endpoint as the day strip rather than the
|
|
22
|
+
* 5-item server preview that excluded most types in practice).
|
|
23
|
+
*/
|
|
14
24
|
plannedActivities: InteractionSummary[]
|
|
15
25
|
refreshKey?: number
|
|
16
26
|
onAddNew: (kind: ActivityKind) => void
|
|
@@ -29,6 +39,20 @@ const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> =
|
|
|
29
39
|
note: StickyNote,
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
const USER_TIMEZONE = (() => {
|
|
43
|
+
try {
|
|
44
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
45
|
+
} catch {
|
|
46
|
+
return 'UTC'
|
|
47
|
+
}
|
|
48
|
+
})()
|
|
49
|
+
|
|
50
|
+
// Project a UTC instant to the user's local timezone before extracting day/month/year
|
|
51
|
+
// for "same day" comparisons (issue #1809 — E3 timezone drift).
|
|
52
|
+
function toLocalZonedDate(value: string | Date): Date {
|
|
53
|
+
return toZonedTime(value, USER_TIMEZONE)
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
function startOfDay(date: Date): Date {
|
|
33
57
|
const next = new Date(date)
|
|
34
58
|
next.setHours(0, 0, 0, 0)
|
|
@@ -47,6 +71,11 @@ function isOverdue(activity: InteractionSummary, now: Date): boolean {
|
|
|
47
71
|
return date.getTime() < now.getTime() && activity.status !== 'done'
|
|
48
72
|
}
|
|
49
73
|
|
|
74
|
+
// Visible window for the day-strip + activity list. Mirrors `VISIBLE_DAYS = 5`
|
|
75
|
+
// in ActivitiesDayStrip with extra padding so navigation forward/back doesn't
|
|
76
|
+
// race the fetch.
|
|
77
|
+
const FETCH_WINDOW_DAYS = 31
|
|
78
|
+
|
|
50
79
|
function formatTime(date: Date): string {
|
|
51
80
|
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
52
81
|
}
|
|
@@ -80,26 +109,83 @@ export function ActivitiesCard({
|
|
|
80
109
|
}: ActivitiesCardProps) {
|
|
81
110
|
const t = useT()
|
|
82
111
|
const [selectedDate, setSelectedDate] = React.useState<Date>(() => startOfDay(new Date()))
|
|
112
|
+
// Fetch the same broader window as the day strip via the canonical interactions
|
|
113
|
+
// endpoint. This single source of truth aligns the day-strip count with the
|
|
114
|
+
// visible event list (issue #1809 — E1) and surfaces all interaction types
|
|
115
|
+
// (issue #1809 — E2: the previous reliance on the server-side 5-item preview
|
|
116
|
+
// produced "Person view shows only Calls" because the limit happened to drop
|
|
117
|
+
// every non-call entry from the prefix-window).
|
|
118
|
+
const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[] | null>(null)
|
|
119
|
+
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
if (!entityId) {
|
|
122
|
+
setFetchedEvents(null)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const controller = new AbortController()
|
|
126
|
+
const today = startOfDay(new Date())
|
|
127
|
+
const fromDate = new Date(today)
|
|
128
|
+
fromDate.setDate(today.getDate() - FETCH_WINDOW_DAYS)
|
|
129
|
+
const toDate = new Date(today)
|
|
130
|
+
toDate.setDate(today.getDate() + FETCH_WINDOW_DAYS)
|
|
131
|
+
toDate.setHours(23, 59, 59, 999)
|
|
132
|
+
const params = new URLSearchParams({
|
|
133
|
+
entityId,
|
|
134
|
+
from: fromDate.toISOString(),
|
|
135
|
+
to: toDate.toISOString(),
|
|
136
|
+
// Server caps at 100 (interactions querySchema). 100 is well above what
|
|
137
|
+
// an active CRM record accumulates in a 31-day window of meetings/calls,
|
|
138
|
+
// and the day strip + list naturally degrade to truncation if exceeded.
|
|
139
|
+
limit: '100',
|
|
140
|
+
sortField: 'scheduledAt',
|
|
141
|
+
sortDir: 'asc',
|
|
142
|
+
excludeInteractionType: 'task',
|
|
143
|
+
})
|
|
144
|
+
void (async () => {
|
|
145
|
+
try {
|
|
146
|
+
const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(
|
|
147
|
+
`/api/customers/interactions?${params.toString()}`,
|
|
148
|
+
{ signal: controller.signal },
|
|
149
|
+
)
|
|
150
|
+
setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if ((err as { name?: string } | null)?.name !== 'AbortError') {
|
|
153
|
+
console.warn('[ActivitiesCard] failed to load interactions', err)
|
|
154
|
+
setFetchedEvents(null)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
})()
|
|
158
|
+
return () => controller.abort()
|
|
159
|
+
}, [entityId, refreshKey])
|
|
160
|
+
|
|
161
|
+
// Prefer the broader fetch when it has resolved; fall back to the seed prop
|
|
162
|
+
// (route-supplied preview) only while the fetch is in flight or after a
|
|
163
|
+
// hard failure. This guarantees that the rare prop-only render path keeps
|
|
164
|
+
// backwards-compat with existing unit tests while live UI uses the broader fetch.
|
|
165
|
+
const effectiveEvents: InteractionSummary[] = fetchedEvents ?? plannedActivities
|
|
83
166
|
|
|
84
167
|
const eventsForSelectedDay = React.useMemo(() => {
|
|
85
|
-
const items =
|
|
168
|
+
const items = effectiveEvents.filter((activity) => {
|
|
86
169
|
const scheduled = activity.scheduledAt ?? activity.occurredAt
|
|
87
170
|
if (!scheduled) return false
|
|
88
171
|
const date = new Date(scheduled)
|
|
89
172
|
if (Number.isNaN(date.getTime())) return false
|
|
90
|
-
|
|
173
|
+
// Compare in the user's local timezone so a 23:30 local activity stays
|
|
174
|
+
// on its local-day chip instead of bleeding into the next UTC day
|
|
175
|
+
// (issue #1809 — E3).
|
|
176
|
+
return isSameDay(toLocalZonedDate(scheduled), selectedDate)
|
|
91
177
|
})
|
|
92
178
|
return items.sort((left, right) => {
|
|
93
179
|
const leftTime = new Date(left.scheduledAt ?? left.occurredAt ?? left.createdAt).getTime()
|
|
94
180
|
const rightTime = new Date(right.scheduledAt ?? right.occurredAt ?? right.createdAt).getTime()
|
|
95
181
|
return leftTime - rightTime
|
|
96
182
|
})
|
|
97
|
-
}, [
|
|
183
|
+
}, [effectiveEvents, selectedDate])
|
|
98
184
|
|
|
99
185
|
const overdueCount = React.useMemo(() => {
|
|
100
186
|
const now = new Date()
|
|
101
|
-
return
|
|
102
|
-
}, [
|
|
187
|
+
return effectiveEvents.filter((activity) => isOverdue(activity, now)).length
|
|
188
|
+
}, [effectiveEvents])
|
|
103
189
|
|
|
104
190
|
return (
|
|
105
191
|
<div className="flex flex-col gap-3 rounded-lg border border-border bg-card pt-4 pb-4 px-4">
|
|
@@ -124,6 +210,7 @@ export function ActivitiesCard({
|
|
|
124
210
|
selectedDate={selectedDate}
|
|
125
211
|
onSelectDate={setSelectedDate}
|
|
126
212
|
refreshKey={refreshKey}
|
|
213
|
+
events={fetchedEvents ?? undefined}
|
|
127
214
|
/>
|
|
128
215
|
|
|
129
216
|
{eventsForSelectedDay.length > 0 ? (
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
5
|
+
import { toZonedTime } from 'date-fns-tz'
|
|
5
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
8
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -13,6 +14,29 @@ interface ActivitiesDayStripProps {
|
|
|
13
14
|
selectedDate: Date
|
|
14
15
|
onSelectDate: (date: Date) => void
|
|
15
16
|
refreshKey?: number
|
|
17
|
+
/**
|
|
18
|
+
* Optional pre-fetched events. When provided, the day strip skips its own fetch
|
|
19
|
+
* and uses the supplied list, ensuring its busyness count agrees with the
|
|
20
|
+
* activity list rendered alongside it (issue #1809 — E1 status filter alignment).
|
|
21
|
+
*/
|
|
22
|
+
events?: InteractionSummary[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const USER_TIMEZONE = (() => {
|
|
26
|
+
try {
|
|
27
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
28
|
+
} catch {
|
|
29
|
+
return 'UTC'
|
|
30
|
+
}
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
// Project a UTC ISO timestamp to the user's local timezone before comparing
|
|
34
|
+
// "same day" (issue #1809 — E3). The browser's `new Date(iso)` treats the
|
|
35
|
+
// instant correctly, but `getDate()/getMonth()/getFullYear()` reflect the
|
|
36
|
+
// user's local day, so for activities scheduled at e.g. 23:30 local on a UTC
|
|
37
|
+
// boundary the day-strip and list now agree.
|
|
38
|
+
function toLocalZonedDate(value: string | Date): Date {
|
|
39
|
+
return toZonedTime(value, USER_TIMEZONE)
|
|
16
40
|
}
|
|
17
41
|
|
|
18
42
|
const VISIBLE_DAYS = 5
|
|
@@ -114,7 +138,10 @@ function computeDayBusyness(events: InteractionSummary[], day: Date): DayBusynes
|
|
|
114
138
|
if (!startIso) continue
|
|
115
139
|
const start = new Date(startIso)
|
|
116
140
|
if (Number.isNaN(start.getTime())) continue
|
|
117
|
-
|
|
141
|
+
// Compare in the user's local timezone so an activity at 23:30 local time
|
|
142
|
+
// doesn't bleed into the next UTC day's chip (issue #1809 — E3).
|
|
143
|
+
const localStart = toLocalZonedDate(startIso)
|
|
144
|
+
if (!isSameDay(localStart, day)) continue
|
|
118
145
|
eventCount += 1
|
|
119
146
|
const durationMinutes = typeof event.duration === 'number' && event.duration > 0 ? event.duration : 30
|
|
120
147
|
totalMinutes += durationMinutes
|
|
@@ -171,10 +198,14 @@ function formatDayLabel(date: Date, t: TranslateFn): string {
|
|
|
171
198
|
return entry ? t(entry[1], entry[2]) : ''
|
|
172
199
|
}
|
|
173
200
|
|
|
174
|
-
export function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey = 0 }: ActivitiesDayStripProps) {
|
|
201
|
+
export function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey = 0, events: providedEvents }: ActivitiesDayStripProps) {
|
|
175
202
|
const t = useT()
|
|
176
203
|
const [anchor, setAnchor] = React.useState<Date>(() => anchorCenteredOn(selectedDate))
|
|
177
|
-
const [
|
|
204
|
+
const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[]>([])
|
|
205
|
+
// When the parent supplies `events` (preferred path — keeps day strip and
|
|
206
|
+
// the list in lockstep, fixes #1809 E1), skip the local fetch entirely.
|
|
207
|
+
const useProvidedEvents = providedEvents !== undefined
|
|
208
|
+
const events = useProvidedEvents ? providedEvents : fetchedEvents
|
|
178
209
|
|
|
179
210
|
React.useEffect(() => {
|
|
180
211
|
setAnchor((current) => {
|
|
@@ -189,6 +220,7 @@ export function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refre
|
|
|
189
220
|
const headerLabel = React.useMemo(() => formatMonthLabel(visibleDays[0], t), [visibleDays, t])
|
|
190
221
|
|
|
191
222
|
React.useEffect(() => {
|
|
223
|
+
if (useProvidedEvents) return
|
|
192
224
|
if (!entityId || visibleDays.length === 0) return
|
|
193
225
|
const controller = new AbortController()
|
|
194
226
|
const fromIso = startOfDay(visibleDays[0]).toISOString()
|
|
@@ -208,16 +240,16 @@ export function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refre
|
|
|
208
240
|
`/api/customers/interactions?${params.toString()}`,
|
|
209
241
|
{ signal: controller.signal },
|
|
210
242
|
)
|
|
211
|
-
|
|
243
|
+
setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])
|
|
212
244
|
} catch (err) {
|
|
213
245
|
if ((err as { name?: string } | null)?.name !== 'AbortError') {
|
|
214
246
|
console.warn('[ActivitiesDayStrip] failed to load interactions', err)
|
|
215
247
|
}
|
|
216
|
-
|
|
248
|
+
setFetchedEvents([])
|
|
217
249
|
}
|
|
218
250
|
})()
|
|
219
251
|
return () => controller.abort()
|
|
220
|
-
}, [entityId, visibleDays, refreshKey])
|
|
252
|
+
}, [entityId, visibleDays, refreshKey, useProvidedEvents])
|
|
221
253
|
|
|
222
254
|
const todayDate = React.useMemo(() => startOfDay(new Date()), [])
|
|
223
255
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Clock, Search } from 'lucide-react'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
-
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
8
|
import type { SectionAction, TabEmptyStateConfig } from '@open-mercato/ui/backend/detail'
|
|
9
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -105,6 +105,7 @@ export function ActivitiesSection({
|
|
|
105
105
|
onLoadingChange,
|
|
106
106
|
refreshKey = 0,
|
|
107
107
|
onEditActivity,
|
|
108
|
+
runGuardedMutation,
|
|
108
109
|
}: ActivitiesSectionProps) {
|
|
109
110
|
const t = useT()
|
|
110
111
|
const [filterTypes, setFilterTypes] = React.useState<string[]>([])
|
|
@@ -165,13 +166,17 @@ export function ActivitiesSection({
|
|
|
165
166
|
setLoading(true)
|
|
166
167
|
try {
|
|
167
168
|
// Always fetch canonical interactions (new activities are always created here)
|
|
169
|
+
const taskFilterActive = filterTypes.includes('task')
|
|
168
170
|
const canonicalParams = new URLSearchParams({
|
|
169
171
|
entityId,
|
|
170
172
|
limit: '50',
|
|
171
173
|
sortField: 'occurredAt',
|
|
172
174
|
sortDir: 'desc',
|
|
173
|
-
excludeInteractionType: 'task',
|
|
174
175
|
})
|
|
176
|
+
// Hide tasks from the activity timeline by default — they have their own tab —
|
|
177
|
+
// but lift the exclusion when the user explicitly toggled the Task chip on
|
|
178
|
+
// (mirrors `ActivityHistorySection.tsx` after the #1805 fix).
|
|
179
|
+
if (!taskFilterActive) canonicalParams.set('excludeInteractionType', 'task')
|
|
175
180
|
if (dealId) canonicalParams.set('dealId', dealId)
|
|
176
181
|
if (filterTypes.length > 0) canonicalParams.set('type', filterTypes.join(','))
|
|
177
182
|
if (filterDateFrom) canonicalParams.set('from', filterDateFrom)
|
|
@@ -250,6 +255,31 @@ export function ActivitiesSection({
|
|
|
250
255
|
setLoadedPages(1)
|
|
251
256
|
}, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, useCanonicalInteractions])
|
|
252
257
|
|
|
258
|
+
const handleMarkDone = React.useCallback(async (activityId: string) => {
|
|
259
|
+
try {
|
|
260
|
+
const operation = () =>
|
|
261
|
+
apiCallOrThrow('/api/customers/interactions/complete', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'content-type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({ id: activityId, occurredAt: new Date().toISOString() }),
|
|
265
|
+
})
|
|
266
|
+
if (runGuardedMutation) {
|
|
267
|
+
await runGuardedMutation(operation, {
|
|
268
|
+
id: activityId,
|
|
269
|
+
status: 'done',
|
|
270
|
+
operation: 'completeActivity',
|
|
271
|
+
})
|
|
272
|
+
} else {
|
|
273
|
+
await operation()
|
|
274
|
+
}
|
|
275
|
+
flash(t('customers.activities.actions.markDoneSuccess', 'Activity marked done'), 'success')
|
|
276
|
+
await loadActivities()
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.warn('[customers.activitiesSection] mark done failed', activityId, err)
|
|
279
|
+
flash(t('customers.activities.actions.markDoneError', 'Could not mark activity as done'), 'error')
|
|
280
|
+
}
|
|
281
|
+
}, [loadActivities, runGuardedMutation, t])
|
|
282
|
+
|
|
253
283
|
const resolvedUserIdsRef = React.useRef(new Set<string>())
|
|
254
284
|
|
|
255
285
|
// Resolve missing author names from user IDs
|
|
@@ -353,7 +383,11 @@ export function ActivitiesSection({
|
|
|
353
383
|
</div>
|
|
354
384
|
) : (
|
|
355
385
|
<>
|
|
356
|
-
<ActivityTimeline
|
|
386
|
+
<ActivityTimeline
|
|
387
|
+
activities={visibleActivities}
|
|
388
|
+
onEdit={onEditActivity}
|
|
389
|
+
onMarkDone={handleMarkDone}
|
|
390
|
+
/>
|
|
357
391
|
{totalCount > 0 ? (
|
|
358
392
|
<div className="flex items-center justify-between gap-3 border-t border-border/60 pt-3">
|
|
359
393
|
<span className="text-xs text-muted-foreground">
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { Calendar, ExternalLink, Mail, MoreHorizontal, Phone, StickyNote, Users } from 'lucide-react'
|
|
4
|
+
import { Calendar, Check, ExternalLink, ListTodo, Mail, MoreHorizontal, Phone, StickyNote, Users } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
5
6
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
7
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
|
+
import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
9
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
10
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
8
11
|
import type { InteractionSummary } from './types'
|
|
9
12
|
import { ActivityAiActions } from './ActivityAiActions'
|
|
10
13
|
import { getInitials } from './utils'
|
|
11
14
|
|
|
15
|
+
type GuardedMutationRunner = <T,>(
|
|
16
|
+
operation: () => Promise<T>,
|
|
17
|
+
mutationPayload?: Record<string, unknown>,
|
|
18
|
+
) => Promise<T>
|
|
19
|
+
|
|
12
20
|
type ActivityCardProps = {
|
|
13
21
|
activity: InteractionSummary
|
|
14
22
|
onOpen?: (activity: InteractionSummary) => void
|
|
23
|
+
/** Called after a successful mark-done so the parent can refresh the timeline. */
|
|
24
|
+
onChanged?: () => void
|
|
25
|
+
/**
|
|
26
|
+
* Optional guarded-mutation runner. When provided, mutations route through the parent's
|
|
27
|
+
* `useGuardedMutation` so retry-last-mutation and the global injection contract apply.
|
|
28
|
+
* When omitted, mutations run directly via `apiCallOrThrow` (e.g. read-only contexts
|
|
29
|
+
* or jest unit tests that don't supply a guarded runner).
|
|
30
|
+
*/
|
|
31
|
+
runMutation?: GuardedMutationRunner
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
@@ -19,6 +36,7 @@ const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> =
|
|
|
19
36
|
email: Mail,
|
|
20
37
|
meeting: Users,
|
|
21
38
|
note: StickyNote,
|
|
39
|
+
task: ListTodo,
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
function formatDayLabel(value: string, t: ReturnType<typeof useT>): string {
|
|
@@ -54,7 +72,7 @@ function resolveTarget(activity: InteractionSummary): string | null {
|
|
|
54
72
|
return null
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
export function ActivityCard({ activity, onOpen }: ActivityCardProps) {
|
|
75
|
+
export function ActivityCard({ activity, onOpen, onChanged, runMutation }: ActivityCardProps) {
|
|
58
76
|
const t = useT()
|
|
59
77
|
const timestamp = activity.occurredAt ?? activity.scheduledAt ?? activity.createdAt
|
|
60
78
|
const TypeIcon = TYPE_ICONS[activity.interactionType] ?? StickyNote
|
|
@@ -69,6 +87,36 @@ export function ActivityCard({ activity, onOpen }: ActivityCardProps) {
|
|
|
69
87
|
? t('customers.activityLog.direction.with', 'with')
|
|
70
88
|
: ''
|
|
71
89
|
const showExternalLink = Boolean(activity._integrations && Object.keys(activity._integrations).length > 0)
|
|
90
|
+
const [markingDone, setMarkingDone] = React.useState(false)
|
|
91
|
+
|
|
92
|
+
const handleMarkDone = React.useCallback(async () => {
|
|
93
|
+
if (markingDone) return
|
|
94
|
+
setMarkingDone(true)
|
|
95
|
+
try {
|
|
96
|
+
const operation = () =>
|
|
97
|
+
apiCallOrThrow('/api/customers/interactions/complete', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'content-type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ id: activity.id, occurredAt: new Date().toISOString() }),
|
|
101
|
+
})
|
|
102
|
+
if (runMutation) {
|
|
103
|
+
await runMutation(operation, {
|
|
104
|
+
id: activity.id,
|
|
105
|
+
status: 'done',
|
|
106
|
+
operation: 'completeActivity',
|
|
107
|
+
})
|
|
108
|
+
} else {
|
|
109
|
+
await operation()
|
|
110
|
+
}
|
|
111
|
+
flash(t('customers.activities.actions.markDoneSuccess', 'Activity marked done'), 'success')
|
|
112
|
+
onChanged?.()
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.warn('[customers.activityCard] mark done failed', activity.id, err)
|
|
115
|
+
flash(t('customers.activities.actions.markDoneError', 'Could not mark activity as done'), 'error')
|
|
116
|
+
} finally {
|
|
117
|
+
setMarkingDone(false)
|
|
118
|
+
}
|
|
119
|
+
}, [activity.id, markingDone, onChanged, runMutation, t])
|
|
72
120
|
|
|
73
121
|
return (
|
|
74
122
|
<div
|
|
@@ -111,18 +159,35 @@ export function ActivityCard({ activity, onOpen }: ActivityCardProps) {
|
|
|
111
159
|
) : null}
|
|
112
160
|
</div>
|
|
113
161
|
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
162
|
+
<div className="flex items-center gap-1.5">
|
|
163
|
+
{activity.status === 'planned' ? (
|
|
164
|
+
<Button
|
|
165
|
+
type="button"
|
|
166
|
+
variant="default"
|
|
167
|
+
size="sm"
|
|
168
|
+
disabled={markingDone}
|
|
169
|
+
onClick={(event) => {
|
|
170
|
+
event.stopPropagation()
|
|
171
|
+
void handleMarkDone()
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<Check className="size-3.5" />
|
|
175
|
+
{t('customers.activities.actions.markDone', 'Mark done')}
|
|
176
|
+
</Button>
|
|
177
|
+
) : null}
|
|
178
|
+
<IconButton
|
|
179
|
+
type="button"
|
|
180
|
+
variant="ghost"
|
|
181
|
+
size="sm"
|
|
182
|
+
aria-label={t('customers.timeline.more', 'More')}
|
|
183
|
+
onClick={(event) => {
|
|
184
|
+
event.stopPropagation()
|
|
185
|
+
onOpen?.(activity)
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<MoreHorizontal className="size-4" />
|
|
189
|
+
</IconButton>
|
|
190
|
+
</div>
|
|
126
191
|
</div>
|
|
127
192
|
|
|
128
193
|
<div className="mt-2">
|