@open-mercato/core 0.5.1-develop.2987.9f8c1e0f68 → 0.5.1-develop.3032.01699048cb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/sidebar_variant/index.js +25 -0
- package/dist/generated/entities/sidebar_variant/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +13 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/authUi.js +1 -1
- package/dist/helpers/integration/authUi.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +4 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js +224 -35
- package/dist/modules/auth/api/sidebar/preferences/route.js.map +3 -3
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js +161 -0
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js.map +7 -0
- package/dist/modules/auth/api/sidebar/variants/route.js +142 -0
- package/dist/modules/auth/api/sidebar/variants/route.js.map +7 -0
- package/dist/modules/auth/backend/sidebar-customization/page.js +16 -0
- package/dist/modules/auth/backend/sidebar-customization/page.js.map +7 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js +28 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js.map +7 -0
- package/dist/modules/auth/data/entities.js +45 -4
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/data/validators.js +63 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260427081815.js +15 -0
- package/dist/modules/auth/migrations/Migration20260427081815.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260427124900.js +15 -0
- package/dist/modules/auth/migrations/Migration20260427124900.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260427143311.js +72 -0
- package/dist/modules/auth/migrations/Migration20260427143311.js.map +7 -0
- package/dist/modules/auth/services/sidebarPreferencesService.js +176 -16
- package/dist/modules/auth/services/sidebarPreferencesService.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/[id]/page.js +3 -1
- package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +25 -2
- 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 +35 -33
- 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/CompanyPeopleSection.js +3 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.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/dist/modules/customers/components/formConfig.js +3 -3
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/displayName.js +12 -0
- package/dist/modules/customers/lib/displayName.js.map +2 -2
- package/dist/modules/entities/cli.js +5 -6
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js +124 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js.map +7 -0
- package/generated/entities/sidebar_variant/index.ts +11 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +13 -0
- package/package.json +6 -6
- package/src/helpers/integration/authUi.ts +1 -1
- package/src/modules/audit_logs/services/actionLogService.ts +5 -6
- package/src/modules/auth/api/sidebar/preferences/route.ts +266 -34
- package/src/modules/auth/api/sidebar/variants/[id]/route.ts +183 -0
- package/src/modules/auth/api/sidebar/variants/route.ts +157 -0
- package/src/modules/auth/backend/sidebar-customization/page.meta.ts +34 -0
- package/src/modules/auth/backend/sidebar-customization/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +48 -2
- package/src/modules/auth/data/validators.ts +70 -0
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +790 -71
- package/src/modules/auth/migrations/Migration20260427081815.ts +16 -0
- package/src/modules/auth/migrations/Migration20260427124900.ts +19 -0
- package/src/modules/auth/migrations/Migration20260427143311.ts +83 -0
- package/src/modules/auth/services/sidebarPreferencesService.ts +243 -18
- 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/[id]/page.tsx +5 -4
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +28 -5
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +41 -30
- 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/CompanyPeopleSection.tsx +3 -2
- 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/components/formConfig.tsx +3 -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
- package/src/modules/customers/lib/displayName.ts +21 -0
- package/src/modules/entities/cli.ts +5 -6
- package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.tsx +168 -0
- package/src/modules/portal/i18n/de.json +20 -0
- package/src/modules/portal/i18n/en.json +20 -0
- package/src/modules/portal/i18n/es.json +20 -0
- package/src/modules/portal/i18n/pl.json +20 -0
|
@@ -25,6 +25,7 @@ import { ActivityLogTab } from '../../../../components/detail/ActivityLogTab'
|
|
|
25
25
|
import { CompanyPeopleSection, type CompanyPersonSummary } from '../../../../components/detail/CompanyPeopleSection'
|
|
26
26
|
import type { TagSummary } from '../../../../components/detail/types'
|
|
27
27
|
import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
28
|
+
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
28
29
|
import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
|
|
29
30
|
import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
|
|
30
31
|
import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
|
|
@@ -103,10 +104,10 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
103
104
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
104
105
|
})
|
|
105
106
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const companyDisplayName = coerceDisplayName(data?.company?.displayName)
|
|
108
|
+
const companyName = companyDisplayName.trim().length
|
|
109
|
+
? companyDisplayName
|
|
110
|
+
: t('customers.companies.list.deleteFallbackName', 'this company')
|
|
110
111
|
|
|
111
112
|
// Data loading
|
|
112
113
|
const initialLoadDoneRef = React.useRef(false)
|
|
@@ -218,6 +219,27 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
218
219
|
setScheduleDialogOpen(true)
|
|
219
220
|
}, [])
|
|
220
221
|
|
|
222
|
+
const handleAddActivity = React.useCallback((kind: 'meeting' | 'call' | 'task' | 'email') => {
|
|
223
|
+
setScheduleEditData({
|
|
224
|
+
id: '',
|
|
225
|
+
interactionType: kind,
|
|
226
|
+
title: null,
|
|
227
|
+
body: null,
|
|
228
|
+
scheduledAt: null,
|
|
229
|
+
durationMinutes: null,
|
|
230
|
+
location: null,
|
|
231
|
+
allDay: null,
|
|
232
|
+
recurrenceRule: null,
|
|
233
|
+
recurrenceEnd: null,
|
|
234
|
+
participants: null,
|
|
235
|
+
reminderMinutes: null,
|
|
236
|
+
visibility: null,
|
|
237
|
+
linkedEntities: null,
|
|
238
|
+
guestPermissions: null,
|
|
239
|
+
})
|
|
240
|
+
setScheduleDialogOpen(true)
|
|
241
|
+
}, [])
|
|
242
|
+
|
|
221
243
|
// Injected tabs from UMES
|
|
222
244
|
const { widgets: injectedTabWidgets } = useInjectionWidgets('detail:customers.company:tabs', {
|
|
223
245
|
context: injectionContext,
|
|
@@ -425,7 +447,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
425
447
|
{activeTab === 'people' && (
|
|
426
448
|
<CompanyPeopleSection
|
|
427
449
|
companyId={companyId}
|
|
428
|
-
companyName={
|
|
450
|
+
companyName={companyDisplayName}
|
|
429
451
|
initialPeople={[]}
|
|
430
452
|
addActionLabel={t('customers.companies.detail.people.add', 'Add person')}
|
|
431
453
|
emptyLabel={t('customers.companies.detail.people.empty', 'No people linked to this company yet.')}
|
|
@@ -472,6 +494,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
472
494
|
plannedActivities={plannedActivities}
|
|
473
495
|
onActivityCreated={handleActivityCreated}
|
|
474
496
|
onScheduleRequested={openNewScheduleDialog}
|
|
497
|
+
onAddActivity={handleAddActivity}
|
|
475
498
|
onMarkDone={handleMarkDone}
|
|
476
499
|
onEditActivity={handleEditActivity}
|
|
477
500
|
onCancelActivity={handleCancelActivity}
|
|
@@ -23,18 +23,17 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
|
|
|
23
23
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
24
24
|
|
|
25
25
|
import { ActivitiesSection } from '../../../../components/detail/ActivitiesSection'
|
|
26
|
+
import { ActivitiesCard } from '../../../../components/detail/ActivitiesCard'
|
|
27
|
+
import type { ActivityKind } from '../../../../components/detail/ActivitiesAddNewMenu'
|
|
26
28
|
import { DealsSection } from '../../../../components/detail/DealsSection'
|
|
27
29
|
import { TasksSection } from '../../../../components/detail/TasksSection'
|
|
28
30
|
import type { TagSummary } from '../../../../components/detail/types'
|
|
29
|
-
import { InlineActivityComposer } from '../../../../components/detail/InlineActivityComposer'
|
|
30
|
-
import { PlannedActivitiesSection } from '../../../../components/detail/PlannedActivitiesSection'
|
|
31
31
|
import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
|
|
32
32
|
import { PersonDetailHeader } from '../../../../components/detail/PersonDetailHeader'
|
|
33
33
|
import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
|
|
34
34
|
import { PersonDetailTabs, resolveLegacyTab, type PersonTabId } from '../../../../components/detail/PersonDetailTabs'
|
|
35
35
|
import { PersonCompaniesSection } from '../../../../components/detail/PersonCompaniesSection'
|
|
36
36
|
import { MobilePersonDetail } from '../../../../components/detail/MobilePersonDetail'
|
|
37
|
-
import { useInteractionMutations } from '../../../../components/detail/hooks/useInteractionMutations'
|
|
38
37
|
import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
39
38
|
import {
|
|
40
39
|
buildPersonEditPayload,
|
|
@@ -45,6 +44,7 @@ import {
|
|
|
45
44
|
type PersonEditFormValues,
|
|
46
45
|
type PersonOverview,
|
|
47
46
|
} from '../../../../components/formConfig'
|
|
47
|
+
import { coerceDisplayName, coerceDisplayNameOrNull } from '../../../../lib/displayName'
|
|
48
48
|
|
|
49
49
|
export default function PersonDetailV2Page({ params }: { params?: { id?: string } }) {
|
|
50
50
|
const id = params?.id
|
|
@@ -95,15 +95,18 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
95
95
|
contextId: mutationContextId,
|
|
96
96
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
97
97
|
})
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const personDisplayName = coerceDisplayName(data?.person?.displayName)
|
|
99
|
+
const personName = personDisplayName.trim().length
|
|
100
|
+
? personDisplayName
|
|
101
|
+
: t('customers.people.list.deleteFallbackName', 'this person')
|
|
102
102
|
|
|
103
|
-
const personDisplayNameForGroups =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
const personDisplayNameForGroups = personDisplayName.trim().length
|
|
104
|
+
? personDisplayName.trim()
|
|
105
|
+
: null
|
|
106
|
+
|
|
107
|
+
const scheduleDialogCompanyName = coerceDisplayNameOrNull(
|
|
108
|
+
data?.company?.displayName ?? data?.companies?.[0]?.displayName ?? null,
|
|
109
|
+
)
|
|
107
110
|
|
|
108
111
|
const groups = React.useMemo(
|
|
109
112
|
() => createPersonPersonalDataGroups(t, { entityName: personDisplayNameForGroups }),
|
|
@@ -186,11 +189,26 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
186
189
|
[injectionContext, runMutation],
|
|
187
190
|
)
|
|
188
191
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
const handleAddActivity = React.useCallback((kind: ActivityKind) => {
|
|
193
|
+
setScheduleEditData({
|
|
194
|
+
id: '',
|
|
195
|
+
interactionType: kind,
|
|
196
|
+
title: null,
|
|
197
|
+
body: null,
|
|
198
|
+
scheduledAt: null,
|
|
199
|
+
durationMinutes: null,
|
|
200
|
+
location: null,
|
|
201
|
+
allDay: null,
|
|
202
|
+
recurrenceRule: null,
|
|
203
|
+
recurrenceEnd: null,
|
|
204
|
+
participants: null,
|
|
205
|
+
reminderMinutes: null,
|
|
206
|
+
visibility: null,
|
|
207
|
+
linkedEntities: null,
|
|
208
|
+
guestPermissions: null,
|
|
209
|
+
})
|
|
210
|
+
setScheduleDialogOpen(true)
|
|
211
|
+
}, [])
|
|
194
212
|
|
|
195
213
|
const handleEditActivity = React.useCallback((activity: { id: string; interactionType?: string; title?: string | null; body?: string | null; scheduledAt?: string | null; [key: string]: unknown }) => {
|
|
196
214
|
const raw = activity as Record<string, unknown>
|
|
@@ -444,20 +462,13 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
444
462
|
if (activeTab === 'activities') {
|
|
445
463
|
return (
|
|
446
464
|
<div className="space-y-4">
|
|
447
|
-
<
|
|
448
|
-
entityType="person"
|
|
465
|
+
<ActivitiesCard
|
|
449
466
|
entityId={personId}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
<PlannedActivitiesSection
|
|
456
|
-
activities={plannedActivities}
|
|
457
|
-
onComplete={handleMarkDone}
|
|
458
|
-
onSchedule={() => { setScheduleEditData(null); setScheduleDialogOpen(true) }}
|
|
459
|
-
onEdit={handleEditActivity}
|
|
460
|
-
onCancel={handleCancelActivity}
|
|
467
|
+
plannedActivities={plannedActivities}
|
|
468
|
+
refreshKey={activityRefreshKey}
|
|
469
|
+
onAddNew={handleAddActivity}
|
|
470
|
+
onEditActivity={handleEditActivity}
|
|
471
|
+
entityCompanyName={data.company?.displayName ?? data.companies?.[0]?.displayName ?? null}
|
|
461
472
|
/>
|
|
462
473
|
<ActivitiesSection
|
|
463
474
|
entityId={personId}
|
|
@@ -579,7 +590,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
579
590
|
onClose={() => { setScheduleDialogOpen(false); setScheduleEditData(null) }}
|
|
580
591
|
entityId={personId}
|
|
581
592
|
entityName={personName}
|
|
582
|
-
companyName={
|
|
593
|
+
companyName={scheduleDialogCompanyName}
|
|
583
594
|
entityType="person"
|
|
584
595
|
onActivityCreated={handleActivityCreated}
|
|
585
596
|
editData={scheduleEditData}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Check, Phone, Mail, Users, CheckSquare } from 'lucide-react'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
|
|
7
|
+
|
|
8
|
+
export type ActivityKind = 'meeting' | 'call' | 'task' | 'email'
|
|
9
|
+
|
|
10
|
+
interface ActivitiesAddNewMenuProps {
|
|
11
|
+
onSelect: (kind: ActivityKind) => void
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MENU_ITEMS: ReadonlyArray<{ kind: ActivityKind; icon: React.ComponentType<{ className?: string }>; key: string; fallback: string }> = [
|
|
16
|
+
{ kind: 'meeting', icon: Users, key: 'customers.activities.add.meeting', fallback: 'New meeting' },
|
|
17
|
+
{ kind: 'call', icon: Phone, key: 'customers.activities.add.call', fallback: 'Log call' },
|
|
18
|
+
{ kind: 'task', icon: CheckSquare, key: 'customers.activities.add.task', fallback: 'New task' },
|
|
19
|
+
{ kind: 'email', icon: Mail, key: 'customers.activities.add.email', fallback: 'Compose email' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export function ActivitiesAddNewMenu({ onSelect, disabled }: ActivitiesAddNewMenuProps) {
|
|
23
|
+
const t = useT()
|
|
24
|
+
const [open, setOpen] = React.useState(false)
|
|
25
|
+
|
|
26
|
+
const handleSelect = React.useCallback(
|
|
27
|
+
(kind: ActivityKind) => {
|
|
28
|
+
setOpen(false)
|
|
29
|
+
onSelect(kind)
|
|
30
|
+
},
|
|
31
|
+
[onSelect],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
36
|
+
<PopoverTrigger asChild>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
disabled={disabled}
|
|
40
|
+
aria-label={t('customers.activities.addNew', 'Add new')}
|
|
41
|
+
className="inline-flex items-center gap-1.5 overflow-hidden rounded-md bg-foreground pl-3 pr-3.5 py-2 text-xs font-semibold text-background transition-colors hover:bg-foreground/90 disabled:opacity-60"
|
|
42
|
+
>
|
|
43
|
+
<Check className="size-3.5" />
|
|
44
|
+
{t('customers.activities.addNew', 'Add new')}
|
|
45
|
+
</button>
|
|
46
|
+
</PopoverTrigger>
|
|
47
|
+
<PopoverContent align="end" className="w-[180px] p-1">
|
|
48
|
+
<ul className="flex flex-col">
|
|
49
|
+
{MENU_ITEMS.map(({ kind, icon: Icon, key, fallback }) => (
|
|
50
|
+
<li key={kind}>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => handleSelect(kind)}
|
|
54
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent/40"
|
|
55
|
+
>
|
|
56
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
57
|
+
{t(key, fallback)}
|
|
58
|
+
</button>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
</PopoverContent>
|
|
63
|
+
</Popover>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default ActivitiesAddNewMenu
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'
|
|
5
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { ActivitiesDayStrip } from './ActivitiesDayStrip'
|
|
9
|
+
import { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'
|
|
10
|
+
import type { InteractionSummary } from './types'
|
|
11
|
+
|
|
12
|
+
interface ActivitiesCardProps {
|
|
13
|
+
entityId: string
|
|
14
|
+
plannedActivities: InteractionSummary[]
|
|
15
|
+
refreshKey?: number
|
|
16
|
+
onAddNew: (kind: ActivityKind) => void
|
|
17
|
+
onEditActivity?: (activity: InteractionSummary) => void
|
|
18
|
+
/**
|
|
19
|
+
* Optional company name for the parent entity. When the planned activity has no `dealTitle`,
|
|
20
|
+
* the row subtitle falls back to "{type} · {company}" to mirror Figma 784:809.
|
|
21
|
+
*/
|
|
22
|
+
entityCompanyName?: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
26
|
+
call: Phone,
|
|
27
|
+
email: Mail,
|
|
28
|
+
meeting: Users,
|
|
29
|
+
note: StickyNote,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startOfDay(date: Date): Date {
|
|
33
|
+
const next = new Date(date)
|
|
34
|
+
next.setHours(0, 0, 0, 0)
|
|
35
|
+
return next
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isSameDay(a: Date, b: Date): boolean {
|
|
39
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isOverdue(activity: InteractionSummary, now: Date): boolean {
|
|
43
|
+
const scheduled = activity.scheduledAt ?? activity.occurredAt
|
|
44
|
+
if (!scheduled) return false
|
|
45
|
+
const date = new Date(scheduled)
|
|
46
|
+
if (Number.isNaN(date.getTime())) return false
|
|
47
|
+
return date.getTime() < now.getTime() && activity.status !== 'done'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatTime(date: Date): string {
|
|
51
|
+
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatRelativeDay(date: Date, t: TranslateFn): string {
|
|
55
|
+
const now = new Date()
|
|
56
|
+
const today = startOfDay(now)
|
|
57
|
+
const target = startOfDay(date)
|
|
58
|
+
const diff = Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
|
59
|
+
if (diff === 0) return t('customers.timeline.date.today', 'today')
|
|
60
|
+
if (diff === 1) return t('customers.timeline.date.tomorrow', 'tomorrow')
|
|
61
|
+
if (diff === -1) return t('customers.timeline.date.yesterday', 'yesterday')
|
|
62
|
+
return target.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatDuration(minutes: number, t: TranslateFn): string {
|
|
66
|
+
if (minutes >= 60) {
|
|
67
|
+
const hours = Math.round((minutes / 60) * 10) / 10
|
|
68
|
+
return t('customers.activities.calendar.hoursShort', '{hours}h', { hours })
|
|
69
|
+
}
|
|
70
|
+
return t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ActivitiesCard({
|
|
74
|
+
entityId,
|
|
75
|
+
plannedActivities,
|
|
76
|
+
refreshKey = 0,
|
|
77
|
+
onAddNew,
|
|
78
|
+
onEditActivity,
|
|
79
|
+
entityCompanyName,
|
|
80
|
+
}: ActivitiesCardProps) {
|
|
81
|
+
const t = useT()
|
|
82
|
+
const [selectedDate, setSelectedDate] = React.useState<Date>(() => startOfDay(new Date()))
|
|
83
|
+
|
|
84
|
+
const eventsForSelectedDay = React.useMemo(() => {
|
|
85
|
+
const items = plannedActivities.filter((activity) => {
|
|
86
|
+
const scheduled = activity.scheduledAt ?? activity.occurredAt
|
|
87
|
+
if (!scheduled) return false
|
|
88
|
+
const date = new Date(scheduled)
|
|
89
|
+
if (Number.isNaN(date.getTime())) return false
|
|
90
|
+
return isSameDay(date, selectedDate)
|
|
91
|
+
})
|
|
92
|
+
return items.sort((left, right) => {
|
|
93
|
+
const leftTime = new Date(left.scheduledAt ?? left.occurredAt ?? left.createdAt).getTime()
|
|
94
|
+
const rightTime = new Date(right.scheduledAt ?? right.occurredAt ?? right.createdAt).getTime()
|
|
95
|
+
return leftTime - rightTime
|
|
96
|
+
})
|
|
97
|
+
}, [plannedActivities, selectedDate])
|
|
98
|
+
|
|
99
|
+
const overdueCount = React.useMemo(() => {
|
|
100
|
+
const now = new Date()
|
|
101
|
+
return plannedActivities.filter((activity) => isOverdue(activity, now)).length
|
|
102
|
+
}, [plannedActivities])
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex flex-col gap-3 rounded-lg border border-border bg-card pt-4 pb-4 px-4">
|
|
106
|
+
<div className="flex items-center justify-between">
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<Calendar className="size-4 text-foreground" />
|
|
109
|
+
<h3 className="text-sm font-semibold leading-none text-foreground">
|
|
110
|
+
{t('customers.activities.card.title', 'Activities')}
|
|
111
|
+
</h3>
|
|
112
|
+
{overdueCount > 0 ? (
|
|
113
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-status-error-bg px-1.5 py-0.5 text-xs font-medium text-status-error-text">
|
|
114
|
+
<CalendarClock className="size-3" />
|
|
115
|
+
{t('customers.activities.card.overdue', '{count} overdue', { count: overdueCount })}
|
|
116
|
+
</span>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
<ActivitiesAddNewMenu onSelect={onAddNew} />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<ActivitiesDayStrip
|
|
123
|
+
entityId={entityId}
|
|
124
|
+
selectedDate={selectedDate}
|
|
125
|
+
onSelectDate={setSelectedDate}
|
|
126
|
+
refreshKey={refreshKey}
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
{eventsForSelectedDay.length > 0 ? (
|
|
130
|
+
<>
|
|
131
|
+
<div className="h-px w-full bg-border" />
|
|
132
|
+
<ul className="flex flex-col">
|
|
133
|
+
{eventsForSelectedDay.map((activity) => (
|
|
134
|
+
<PlannedEventRow
|
|
135
|
+
key={activity.id}
|
|
136
|
+
activity={activity}
|
|
137
|
+
onClick={onEditActivity}
|
|
138
|
+
entityCompanyName={entityCompanyName ?? null}
|
|
139
|
+
t={t}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<div className="h-px w-full bg-border" />
|
|
147
|
+
<p className="px-1 py-2 text-xs text-muted-foreground">
|
|
148
|
+
{t('customers.activities.card.empty', 'Nothing scheduled for this day.')}
|
|
149
|
+
</p>
|
|
150
|
+
</>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface PlannedEventRowProps {
|
|
157
|
+
activity: InteractionSummary
|
|
158
|
+
onClick?: (activity: InteractionSummary) => void
|
|
159
|
+
entityCompanyName: string | null
|
|
160
|
+
t: TranslateFn
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function PlannedEventRow({ activity, onClick, entityCompanyName, t }: PlannedEventRowProps) {
|
|
164
|
+
const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt
|
|
165
|
+
const date = new Date(dateStr)
|
|
166
|
+
const validDate = !Number.isNaN(date.getTime())
|
|
167
|
+
const Icon = TYPE_ICONS[activity.interactionType] ?? Users
|
|
168
|
+
const duration = typeof activity.duration === 'number' && activity.duration > 0 ? activity.duration : null
|
|
169
|
+
const overdue = validDate && date.getTime() < Date.now() && activity.status !== 'done'
|
|
170
|
+
const typeLabel = labelForType(activity.interactionType, t)
|
|
171
|
+
const subtitleSuffix = activity.dealTitle ?? entityCompanyName ?? null
|
|
172
|
+
const subtitle = subtitleSuffix ? `${typeLabel} · ${subtitleSuffix}` : typeLabel
|
|
173
|
+
const interactive = !!onClick
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<li>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={interactive ? () => onClick?.(activity) : undefined}
|
|
180
|
+
disabled={!interactive}
|
|
181
|
+
className={cn(
|
|
182
|
+
'flex w-full items-start gap-[9px] pt-[8px] text-left transition-colors',
|
|
183
|
+
interactive ? 'cursor-pointer rounded-md hover:bg-accent/30 px-1' : 'px-1',
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<div className="flex h-[44px] w-[43px] shrink-0 flex-col gap-[2px] pt-[2px]">
|
|
187
|
+
<span className="text-xs font-semibold leading-none text-foreground">
|
|
188
|
+
{validDate ? formatTime(date) : ''}
|
|
189
|
+
</span>
|
|
190
|
+
<span className="text-[10px] leading-none font-normal text-muted-foreground">
|
|
191
|
+
{validDate ? formatRelativeDay(date, t) : ''}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="flex shrink-0 items-center justify-center rounded-full bg-muted border-4 border-background size-7">
|
|
195
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
196
|
+
</div>
|
|
197
|
+
<div className="min-w-0 flex flex-1 flex-col gap-[4px]">
|
|
198
|
+
<span className="text-sm leading-5 tracking-[-0.084px] text-foreground">
|
|
199
|
+
{activity.title ?? activity.body ?? labelForType(activity.interactionType, t)}
|
|
200
|
+
</span>
|
|
201
|
+
{duration ? (
|
|
202
|
+
<span className={cn(
|
|
203
|
+
'inline-flex w-fit items-center gap-[2px] rounded-full pl-[4px] pr-[8px] py-[2px] text-xs font-medium leading-[16px]',
|
|
204
|
+
overdue
|
|
205
|
+
? 'bg-status-error-bg text-status-error-text'
|
|
206
|
+
: 'bg-status-warning-bg text-status-warning-text',
|
|
207
|
+
)}>
|
|
208
|
+
<Clock className="size-4" />
|
|
209
|
+
{formatDuration(duration, t)}
|
|
210
|
+
</span>
|
|
211
|
+
) : null}
|
|
212
|
+
<span className="text-[11px] font-normal text-muted-foreground">{subtitle}</span>
|
|
213
|
+
</div>
|
|
214
|
+
</button>
|
|
215
|
+
</li>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function labelForType(type: string, t: TranslateFn): string {
|
|
220
|
+
const map: Record<string, [string, string]> = {
|
|
221
|
+
meeting: ['customers.timeline.filter.meeting', 'Meeting'],
|
|
222
|
+
call: ['customers.timeline.filter.call', 'Call'],
|
|
223
|
+
email: ['customers.timeline.filter.email', 'Email'],
|
|
224
|
+
note: ['customers.timeline.filter.note', 'Note'],
|
|
225
|
+
task: ['customers.timeline.filter.task', 'Task'],
|
|
226
|
+
}
|
|
227
|
+
const entry = map[type]
|
|
228
|
+
return entry ? t(entry[0], entry[1]) : type
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default ActivitiesCard
|