@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.
Files changed (143) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/sidebar_variant/index.js +25 -0
  3. package/dist/generated/entities/sidebar_variant/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +1 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +13 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/authUi.js +1 -1
  9. package/dist/helpers/integration/authUi.js.map +2 -2
  10. package/dist/modules/audit_logs/services/actionLogService.js +4 -5
  11. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  12. package/dist/modules/auth/api/sidebar/preferences/route.js +224 -35
  13. package/dist/modules/auth/api/sidebar/preferences/route.js.map +3 -3
  14. package/dist/modules/auth/api/sidebar/variants/[id]/route.js +161 -0
  15. package/dist/modules/auth/api/sidebar/variants/[id]/route.js.map +7 -0
  16. package/dist/modules/auth/api/sidebar/variants/route.js +142 -0
  17. package/dist/modules/auth/api/sidebar/variants/route.js.map +7 -0
  18. package/dist/modules/auth/backend/sidebar-customization/page.js +16 -0
  19. package/dist/modules/auth/backend/sidebar-customization/page.js.map +7 -0
  20. package/dist/modules/auth/backend/sidebar-customization/page.meta.js +28 -0
  21. package/dist/modules/auth/backend/sidebar-customization/page.meta.js.map +7 -0
  22. package/dist/modules/auth/data/entities.js +45 -4
  23. package/dist/modules/auth/data/entities.js.map +2 -2
  24. package/dist/modules/auth/data/validators.js +63 -1
  25. package/dist/modules/auth/data/validators.js.map +2 -2
  26. package/dist/modules/auth/migrations/Migration20260427081815.js +15 -0
  27. package/dist/modules/auth/migrations/Migration20260427081815.js.map +7 -0
  28. package/dist/modules/auth/migrations/Migration20260427124900.js +15 -0
  29. package/dist/modules/auth/migrations/Migration20260427124900.js.map +7 -0
  30. package/dist/modules/auth/migrations/Migration20260427143311.js +72 -0
  31. package/dist/modules/auth/migrations/Migration20260427143311.js.map +7 -0
  32. package/dist/modules/auth/services/sidebarPreferencesService.js +176 -16
  33. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  34. package/dist/modules/customers/api/companies/[id]/route.js +30 -20
  35. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  36. package/dist/modules/customers/api/companies/route.js +12 -7
  37. package/dist/modules/customers/api/companies/route.js.map +2 -2
  38. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +12 -7
  39. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  40. package/dist/modules/customers/api/people/route.js +12 -7
  41. package/dist/modules/customers/api/people/route.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/companies/[id]/page.js +3 -1
  43. package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +25 -2
  45. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +35 -33
  47. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  48. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +56 -0
  49. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +7 -0
  50. package/dist/modules/customers/components/detail/ActivitiesCard.js +175 -0
  51. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +7 -0
  52. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +324 -0
  53. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +7 -0
  54. package/dist/modules/customers/components/detail/ActivitiesSection.js +62 -13
  55. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  56. package/dist/modules/customers/components/detail/ActivityLogTab.js +14 -23
  57. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
  58. package/dist/modules/customers/components/detail/ActivityTimeline.js +13 -13
  59. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  60. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +35 -22
  61. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
  62. package/dist/modules/customers/components/detail/AiActionChips.js +15 -22
  63. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  64. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +3 -2
  65. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  66. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +196 -28
  67. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  68. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -2
  69. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  70. package/dist/modules/customers/components/detail/schedule/FooterFields.js +14 -2
  71. package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +2 -2
  72. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +9 -2
  73. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
  74. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +9 -2
  75. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  76. package/dist/modules/customers/components/detail/schedule/fieldConfig.js +25 -4
  77. package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
  78. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +20 -3
  79. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  80. package/dist/modules/customers/components/formConfig.js +3 -3
  81. package/dist/modules/customers/components/formConfig.js.map +2 -2
  82. package/dist/modules/customers/lib/displayName.js +12 -0
  83. package/dist/modules/customers/lib/displayName.js.map +2 -2
  84. package/dist/modules/entities/cli.js +5 -6
  85. package/dist/modules/entities/cli.js.map +2 -2
  86. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js +124 -0
  87. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js.map +7 -0
  88. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js +11 -0
  89. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js.map +7 -0
  90. package/generated/entities/sidebar_variant/index.ts +11 -0
  91. package/generated/entities.ids.generated.ts +1 -0
  92. package/generated/entity-fields-registry.ts +13 -0
  93. package/package.json +6 -6
  94. package/src/helpers/integration/authUi.ts +1 -1
  95. package/src/modules/audit_logs/services/actionLogService.ts +5 -6
  96. package/src/modules/auth/api/sidebar/preferences/route.ts +266 -34
  97. package/src/modules/auth/api/sidebar/variants/[id]/route.ts +183 -0
  98. package/src/modules/auth/api/sidebar/variants/route.ts +157 -0
  99. package/src/modules/auth/backend/sidebar-customization/page.meta.ts +34 -0
  100. package/src/modules/auth/backend/sidebar-customization/page.tsx +17 -0
  101. package/src/modules/auth/data/entities.ts +48 -2
  102. package/src/modules/auth/data/validators.ts +70 -0
  103. package/src/modules/auth/migrations/.snapshot-open-mercato.json +790 -71
  104. package/src/modules/auth/migrations/Migration20260427081815.ts +16 -0
  105. package/src/modules/auth/migrations/Migration20260427124900.ts +19 -0
  106. package/src/modules/auth/migrations/Migration20260427143311.ts +83 -0
  107. package/src/modules/auth/services/sidebarPreferencesService.ts +243 -18
  108. package/src/modules/customers/api/companies/[id]/route.ts +30 -20
  109. package/src/modules/customers/api/companies/route.ts +12 -7
  110. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +12 -7
  111. package/src/modules/customers/api/people/route.ts +12 -7
  112. package/src/modules/customers/backend/customers/companies/[id]/page.tsx +5 -4
  113. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +28 -5
  114. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +41 -30
  115. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +67 -0
  116. package/src/modules/customers/components/detail/ActivitiesCard.tsx +231 -0
  117. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +390 -0
  118. package/src/modules/customers/components/detail/ActivitiesSection.tsx +91 -40
  119. package/src/modules/customers/components/detail/ActivityLogTab.tsx +25 -23
  120. package/src/modules/customers/components/detail/ActivityTimeline.tsx +15 -19
  121. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +36 -29
  122. package/src/modules/customers/components/detail/AiActionChips.tsx +17 -23
  123. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +3 -2
  124. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +233 -41
  125. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +6 -2
  126. package/src/modules/customers/components/detail/schedule/FooterFields.tsx +22 -2
  127. package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +10 -2
  128. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +10 -2
  129. package/src/modules/customers/components/detail/schedule/fieldConfig.ts +26 -6
  130. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +32 -3
  131. package/src/modules/customers/components/formConfig.tsx +3 -3
  132. package/src/modules/customers/i18n/de.json +69 -2
  133. package/src/modules/customers/i18n/en.json +69 -2
  134. package/src/modules/customers/i18n/es.json +69 -2
  135. package/src/modules/customers/i18n/pl.json +68 -1
  136. package/src/modules/customers/lib/displayName.ts +21 -0
  137. package/src/modules/entities/cli.ts +5 -6
  138. package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.ts +9 -0
  139. package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.tsx +168 -0
  140. package/src/modules/portal/i18n/de.json +20 -0
  141. package/src/modules/portal/i18n/en.json +20 -0
  142. package/src/modules/portal/i18n/es.json +20 -0
  143. 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 companyName =
107
- data?.company?.displayName && data.company.displayName.trim().length
108
- ? data.company.displayName
109
- : t('customers.companies.list.deleteFallbackName', 'this company')
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={data.company?.displayName ?? ''}
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 personName =
99
- data?.person?.displayName && data.person.displayName.trim().length
100
- ? data.person.displayName
101
- : t('customers.people.list.deleteFallbackName', 'this person')
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
- typeof data?.person?.displayName === 'string' && data.person.displayName.trim().length
105
- ? data.person.displayName.trim()
106
- : null
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 { completeInteraction: handleMarkDone, cancelInteraction: handleCancelActivity } = useInteractionMutations({
190
- runMutationWithContext,
191
- onAfterChange: handleActivityCreated,
192
- logContext: 'customers.people-v2',
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
- <InlineActivityComposer
448
- entityType="person"
465
+ <ActivitiesCard
449
466
  entityId={personId}
450
- onActivityCreated={handleActivityCreated}
451
- runGuardedMutation={runMutationWithContext}
452
- onScheduleRequested={() => { setScheduleEditData(null); setScheduleDialogOpen(true) }}
453
- useCanonicalInteractions={useCanonicalInteractions}
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={data.company?.displayName ?? data.companies?.[0]?.displayName ?? null}
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