@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
|
@@ -9,16 +9,9 @@ const sidebarCustomizeIcon = React.createElement(
|
|
|
9
9
|
React.createElement('path', { d: 'M17.5 14v7' }),
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
// Page is reachable by any authenticated user — every staff user has
|
|
13
|
-
// always been able to customize their PERSONAL sidebar (the variants /
|
|
14
|
-
// preferences APIs gate only role-application via `auth.sidebar.manage`).
|
|
15
|
-
// Inside the editor, the "Apply to roles" card and role variants picker are
|
|
16
|
-
// already conditionally hidden via `canApplyToRoles` (server-checked against
|
|
17
|
-
// `auth.sidebar.manage`), so non-admins see only the personal-scope flow,
|
|
18
|
-
// matching the pre-PR inline-editor behavior. Restricting the whole page
|
|
19
|
-
// to `auth.sidebar.manage` would be a stealth regression for non-admins.
|
|
20
12
|
export const metadata = {
|
|
21
13
|
requireAuth: true,
|
|
14
|
+
requireFeatures: ['auth.sidebar.manage'],
|
|
22
15
|
pageTitle: 'Customize sidebar',
|
|
23
16
|
pageTitleKey: 'appShell.customizeSidebar',
|
|
24
17
|
pageGroup: 'Customization',
|
|
@@ -42,7 +42,10 @@ import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
|
42
42
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
43
43
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
44
44
|
import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
filterActivePersonCompanyLinks,
|
|
47
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
48
|
+
} from '../../../lib/personCompanyLinkTable'
|
|
46
49
|
import { normalizeCustomerDetailCustomFields } from '../../detailCustomFields'
|
|
47
50
|
|
|
48
51
|
export const metadata = {
|
|
@@ -703,15 +706,17 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
703
706
|
},
|
|
704
707
|
'customers.companies.GET',
|
|
705
708
|
)
|
|
706
|
-
const companyLinks =
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
709
|
+
const companyLinks = filterActivePersonCompanyLinks(
|
|
710
|
+
await findWithDecryption(
|
|
711
|
+
em,
|
|
712
|
+
CustomerPersonCompanyLink,
|
|
713
|
+
companyLinkWhere,
|
|
714
|
+
{
|
|
715
|
+
populate: ['person', 'person.personProfile'],
|
|
716
|
+
orderBy: { isPrimary: 'desc', createdAt: 'asc' },
|
|
717
|
+
},
|
|
718
|
+
peopleDecryptionScope,
|
|
719
|
+
),
|
|
715
720
|
)
|
|
716
721
|
companyLinks.forEach((link) => {
|
|
717
722
|
const entity = typeof link.person === 'string' ? null : link.person
|
|
@@ -829,18 +834,23 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
829
834
|
})
|
|
830
835
|
const peopleCount = includePeople
|
|
831
836
|
? relatedPeople.length
|
|
832
|
-
:
|
|
833
|
-
|
|
834
|
-
await withActiveCustomerPersonCompanyLinkFilter(
|
|
837
|
+
: filterActivePersonCompanyLinks(
|
|
838
|
+
await findWithDecryption(
|
|
835
839
|
em,
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
840
|
+
CustomerPersonCompanyLink,
|
|
841
|
+
await withActiveCustomerPersonCompanyLinkFilter(
|
|
842
|
+
em,
|
|
843
|
+
{
|
|
844
|
+
company: company.id,
|
|
845
|
+
organizationId: company.organizationId,
|
|
846
|
+
tenantId: company.tenantId,
|
|
847
|
+
},
|
|
848
|
+
'customers.companies.GET',
|
|
849
|
+
),
|
|
850
|
+
{},
|
|
851
|
+
{ tenantId: company.tenantId, organizationId: company.organizationId },
|
|
842
852
|
),
|
|
843
|
-
)
|
|
853
|
+
).length
|
|
844
854
|
const kpiInteractionRows = canonicalActiveInteractions.length
|
|
845
855
|
? canonicalActiveInteractions
|
|
846
856
|
: await findWithDecryption(
|
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
createPagedListResponseSchema,
|
|
34
34
|
defaultOkResponseSchema,
|
|
35
35
|
} from '../openapi'
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
filterActivePersonCompanyLinks,
|
|
38
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
39
|
+
} from '../../lib/personCompanyLinkTable'
|
|
37
40
|
import { normalizeCompanyProfilePayload } from './payload'
|
|
38
41
|
|
|
39
42
|
const rawBodySchema = z.object({}).passthrough()
|
|
@@ -217,12 +220,14 @@ const crud = makeCrudRoute({
|
|
|
217
220
|
{ person: query.excludeLinkedPersonId },
|
|
218
221
|
'customers.companies.GET',
|
|
219
222
|
)
|
|
220
|
-
const links =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
const links = filterActivePersonCompanyLinks(
|
|
224
|
+
await findWithDecryption(
|
|
225
|
+
em,
|
|
226
|
+
CustomerPersonCompanyLink,
|
|
227
|
+
linkWhere,
|
|
228
|
+
{ populate: ['company'] },
|
|
229
|
+
decryptionScope,
|
|
230
|
+
),
|
|
226
231
|
)
|
|
227
232
|
links.forEach((link) => {
|
|
228
233
|
const companyId = link.company?.id
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
CustomerDeal,
|
|
21
21
|
CustomerInteraction,
|
|
22
22
|
} from '../../../../../data/entities'
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
filterActivePersonCompanyLinks,
|
|
25
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
26
|
+
} from '../../../../../lib/personCompanyLinkTable'
|
|
24
27
|
|
|
25
28
|
const paramsSchema = z.object({
|
|
26
29
|
id: z.string().uuid(),
|
|
@@ -187,12 +190,14 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
187
190
|
},
|
|
188
191
|
'customers.people.companiesEnriched.GET',
|
|
189
192
|
)
|
|
190
|
-
const links =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
const links = filterActivePersonCompanyLinks(
|
|
194
|
+
await findWithDecryption(
|
|
195
|
+
em,
|
|
196
|
+
CustomerPersonCompanyLink,
|
|
197
|
+
linkWhere,
|
|
198
|
+
{ populate: ['company'] },
|
|
199
|
+
entityScope,
|
|
200
|
+
),
|
|
196
201
|
)
|
|
197
202
|
|
|
198
203
|
const companyIds = links.map((link) => (link.company as CustomerEntity).id)
|
|
@@ -29,7 +29,10 @@ import {
|
|
|
29
29
|
createPagedListResponseSchema,
|
|
30
30
|
defaultOkResponseSchema,
|
|
31
31
|
} from '../openapi'
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
filterActivePersonCompanyLinks,
|
|
34
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
35
|
+
} from '../../lib/personCompanyLinkTable'
|
|
33
36
|
import { normalizeProfilePayload } from './payload'
|
|
34
37
|
|
|
35
38
|
const rawBodySchema = z.object({}).passthrough()
|
|
@@ -221,12 +224,14 @@ const crud = makeCrudRoute({
|
|
|
221
224
|
{ company: query.excludeLinkedCompanyId },
|
|
222
225
|
'customers.people.GET',
|
|
223
226
|
)
|
|
224
|
-
const links =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
const links = filterActivePersonCompanyLinks(
|
|
228
|
+
await findWithDecryption(
|
|
229
|
+
em,
|
|
230
|
+
CustomerPersonCompanyLink,
|
|
231
|
+
linkWhere,
|
|
232
|
+
{ populate: ['person'] },
|
|
233
|
+
decryptionScope,
|
|
234
|
+
),
|
|
230
235
|
)
|
|
231
236
|
links.forEach((link) => {
|
|
232
237
|
const personId = link.person?.id
|
|
@@ -219,6 +219,27 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
219
219
|
setScheduleDialogOpen(true)
|
|
220
220
|
}, [])
|
|
221
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
|
+
|
|
222
243
|
// Injected tabs from UMES
|
|
223
244
|
const { widgets: injectedTabWidgets } = useInjectionWidgets('detail:customers.company:tabs', {
|
|
224
245
|
context: injectionContext,
|
|
@@ -473,6 +494,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
473
494
|
plannedActivities={plannedActivities}
|
|
474
495
|
onActivityCreated={handleActivityCreated}
|
|
475
496
|
onScheduleRequested={openNewScheduleDialog}
|
|
497
|
+
onAddActivity={handleAddActivity}
|
|
476
498
|
onMarkDone={handleMarkDone}
|
|
477
499
|
onEditActivity={handleEditActivity}
|
|
478
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,
|
|
@@ -190,11 +189,26 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
190
189
|
[injectionContext, runMutation],
|
|
191
190
|
)
|
|
192
191
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}, [])
|
|
198
212
|
|
|
199
213
|
const handleEditActivity = React.useCallback((activity: { id: string; interactionType?: string; title?: string | null; body?: string | null; scheduledAt?: string | null; [key: string]: unknown }) => {
|
|
200
214
|
const raw = activity as Record<string, unknown>
|
|
@@ -448,20 +462,13 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
448
462
|
if (activeTab === 'activities') {
|
|
449
463
|
return (
|
|
450
464
|
<div className="space-y-4">
|
|
451
|
-
<
|
|
452
|
-
entityType="person"
|
|
465
|
+
<ActivitiesCard
|
|
453
466
|
entityId={personId}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
<PlannedActivitiesSection
|
|
460
|
-
activities={plannedActivities}
|
|
461
|
-
onComplete={handleMarkDone}
|
|
462
|
-
onSchedule={() => { setScheduleEditData(null); setScheduleDialogOpen(true) }}
|
|
463
|
-
onEdit={handleEditActivity}
|
|
464
|
-
onCancel={handleCancelActivity}
|
|
467
|
+
plannedActivities={plannedActivities}
|
|
468
|
+
refreshKey={activityRefreshKey}
|
|
469
|
+
onAddNew={handleAddActivity}
|
|
470
|
+
onEditActivity={handleEditActivity}
|
|
471
|
+
entityCompanyName={data.company?.displayName ?? data.companies?.[0]?.displayName ?? null}
|
|
465
472
|
/>
|
|
466
473
|
<ActivitiesSection
|
|
467
474
|
entityId={personId}
|
|
@@ -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
|