@aglyn/plugins-crm 1.0.0-beta.200 → 1.0.0-beta.201

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 (78) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/company-properties-card.js +53 -36
  9. package/src/lib/components/company-properties-card.js.map +1 -1
  10. package/src/lib/components/contact-associations-card.d.ts +3 -2
  11. package/src/lib/components/contact-associations-card.js +12 -12
  12. package/src/lib/components/contact-associations-card.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -3
  14. package/src/lib/components/contact-detail-page.js +4 -10
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-properties-card.d.ts +21 -4
  17. package/src/lib/components/contact-properties-card.js +129 -29
  18. package/src/lib/components/contact-properties-card.js.map +1 -1
  19. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  20. package/src/lib/components/contact-timeline-card.js +132 -174
  21. package/src/lib/components/contact-timeline-card.js.map +1 -1
  22. package/src/lib/components/contacts-section.js +197 -198
  23. package/src/lib/components/contacts-section.js.map +1 -1
  24. package/src/lib/components/crm-email-state-chip.js +4 -1
  25. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  26. package/src/lib/components/deal-products-card.js +15 -13
  27. package/src/lib/components/deal-products-card.js.map +1 -1
  28. package/src/lib/components/deal-properties-card.js +7 -0
  29. package/src/lib/components/deal-properties-card.js.map +1 -1
  30. package/src/lib/components/deal-stage-card.js +31 -29
  31. package/src/lib/components/deal-stage-card.js.map +1 -1
  32. package/src/lib/components/deals-section.js +9 -14
  33. package/src/lib/components/deals-section.js.map +1 -1
  34. package/src/lib/components/fields-section.d.ts +4 -0
  35. package/src/lib/components/fields-section.js +368 -154
  36. package/src/lib/components/fields-section.js.map +1 -1
  37. package/src/lib/components/form-contact-fields-card.js +24 -20
  38. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  39. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +32 -35
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-properties-card.d.ts +14 -6
  43. package/src/lib/components/lead-properties-card.js +317 -313
  44. package/src/lib/components/lead-properties-card.js.map +1 -1
  45. package/src/lib/components/lead-surfaces-note.js +13 -2
  46. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  47. package/src/lib/components/linked-deals-card.js +12 -10
  48. package/src/lib/components/linked-deals-card.js.map +1 -1
  49. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  50. package/src/lib/components/recent-activity-feed.js +54 -29
  51. package/src/lib/components/recent-activity-feed.js.map +1 -1
  52. package/src/lib/components/record-activity-card.d.ts +4 -2
  53. package/src/lib/components/record-activity-card.js +29 -11
  54. package/src/lib/components/record-activity-card.js.map +1 -1
  55. package/src/lib/components/reports/activity-card.js +18 -12
  56. package/src/lib/components/reports/activity-card.js.map +1 -1
  57. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  58. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  59. package/src/lib/components/reports/pipeline-card.js +22 -16
  60. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  61. package/src/lib/components/reports/report-export.d.ts +4 -11
  62. package/src/lib/components/reports/report-export.js +16 -33
  63. package/src/lib/components/reports/report-export.js.map +1 -1
  64. package/src/lib/components/reports/source-conversion-card.js +18 -12
  65. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  66. package/src/lib/components/reports/tasks-card.js +35 -25
  67. package/src/lib/components/reports/tasks-card.js.map +1 -1
  68. package/src/lib/components/tasks-section.js +23 -23
  69. package/src/lib/components/tasks-section.js.map +1 -1
  70. package/src/lib/constants/field-list-query.d.ts +23 -0
  71. package/src/lib/constants/field-list-query.js +80 -0
  72. package/src/lib/constants/field-list-query.js.map +1 -0
  73. package/src/lib/model/lead-filters.d.ts +1 -1
  74. package/src/lib/server/capture-contact.js +7 -5
  75. package/src/lib/server/capture-contact.js.map +1 -1
  76. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  77. package/src/lib/components/contact-custom-fields-card.js +0 -204
  78. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.200",
3
+ "version": "1.0.0-beta.201",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.200",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.200",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.200",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.200",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.200",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.200",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.200",
35
- "@aglyn/shared-util-email": "1.0.0-beta.200",
36
- "@aglyn/shared-util-http": "1.0.0-beta.200",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.200",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.200",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.200",
28
+ "@aglyn/aglyn": "1.0.0-beta.201",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.201",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.201",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.201",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.201",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.201",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.201",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.201",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.201",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.201",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.201",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.201",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -1,6 +1,7 @@
1
1
  import type { CrmActivityRow } from '@aglyn/aglyn';
2
2
  import { type ReactNode } from 'react';
3
3
  import { type ActivityScope } from './activity-queries';
4
+ import { type TimelineExpansion } from './activity-timeline';
4
5
  /** The icon for a kind — a stored kind outside the union draws as `other`. */
5
6
  export declare function ActivityKindIcon(props: {
6
7
  kind: string;
@@ -20,6 +21,17 @@ export declare namespace ActivityKindIcon {
20
21
  * team member rather than as a uid.
21
22
  */
22
23
  export declare function useActivityAuthorName(): (activity: Pick<CrmActivityRow, 'byUid' | 'byName'>) => string;
24
+ /**
25
+ * What a collapsed activity reads as, and whether opening it shows more.
26
+ *
27
+ * A subject is the heading and the body is what the row opens to. With no
28
+ * subject the body's first line is the heading, and a body that runs past
29
+ * that line, or past what one line holds, opens to the whole text.
30
+ */
31
+ export declare function activityHeading(activity: Pick<CrmActivityRow, 'subject' | 'body'>): {
32
+ heading: string;
33
+ opens: boolean;
34
+ };
23
35
  export interface ActivityRowProps {
24
36
  activity: CrmActivityRow;
25
37
  /** Where the delete is written, from `useActivityScope`. */
@@ -40,12 +52,23 @@ export interface ActivityRowProps {
40
52
  * itself would read the member document once per row.
41
53
  */
42
54
  editable?: boolean;
55
+ /**
56
+ * Whether the row is open to its body. The list holds it so the card's
57
+ * "Expand all" reaches every row; a row drawn alone holds its own.
58
+ */
59
+ expanded?: boolean;
60
+ onToggle?: () => void;
43
61
  }
44
62
  /**
45
63
  * One logged activity: the kind's icon and label, what was said, how it
46
64
  * went, who logged it and how long ago — with edit and delete for whoever
47
65
  * may (AGL-2600).
48
66
  *
67
+ * Collapsed, the row is its chips, its heading and its caption: the kind,
68
+ * the direction and delivery state, how a call went, the subject or the
69
+ * note's first line, and who, to or from whom, and when. Opened, it shows
70
+ * the body — an email's whole text, a note's every line.
71
+ *
49
72
  * Edit and delete appear only when `editable` says this reader may — the
50
73
  * author or an org-wide member. That is the console's verdict and not the
51
74
  * rules' — see `useCanEditActivity` for why the rules admit more — so the
@@ -65,9 +88,11 @@ export interface ActivityListProps {
65
88
  emptyText?: string;
66
89
  /** The way out of an empty list — the record's "Log activity" button. */
67
90
  emptyAction?: ReactNode;
68
- /** A further page exists; `onShowMore` widens the window. */
91
+ /** The listener holds less than exists; `onShowMore` widens it. */
69
92
  hasMore?: boolean;
70
93
  onShowMore?: () => void;
94
+ /** The listener is reading, so an empty window is not yet an empty log. */
95
+ loading?: boolean;
71
96
  /** The record each row is about, for a list that spans records. */
72
97
  subjectFor?: (activity: CrmActivityRow) => ReactNode;
73
98
  /**
@@ -77,15 +102,21 @@ export interface ActivityListProps {
77
102
  * corrected, beside everything else about it.
78
103
  */
79
104
  readOnly?: boolean;
105
+ /**
106
+ * Which rows are open, held by the card so its header's "Expand all"
107
+ * reaches them. A list with no header control holds its own.
108
+ */
109
+ expansion?: TimelineExpansion;
80
110
  }
81
111
  /**
82
- * A newest-first list of logged activities with a "show more" foot
83
- * (AGL-2600).
112
+ * A newest-first list of logged activities, collapsed to one entry each and
113
+ * paged by the console's shared footer (AGL-2600).
84
114
  *
85
115
  * The rows arrive ordered — the query is `orderBy('atMs', 'desc')` — and the
86
116
  * list does not sort them again; a second sort here is a second place for
87
- * the order to be defined. The foot appears only while the probe row says
88
- * more exists, so it never leads nowhere.
117
+ * the order to be defined. The footer pages the window the listener holds,
118
+ * and a page turned past it widens the listener while the probe row says
119
+ * more exists (`useTimelinePages`).
89
120
  *
90
121
  * Who may edit is decided here, once, and handed to every row: the verdict
91
122
  * reads the member document, and the list is the one place that can ask
@@ -14,17 +14,20 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
19
  import * as Aglyn from "@aglyn/aglyn";
19
20
  import { mdiAccountGroupOutline, mdiDeleteOutline, mdiDotsHorizontalCircleOutline, mdiEmailOutline, mdiEmailReceiveOutline, mdiNoteTextOutline, mdiPencilOutline, mdiPhoneOutline } from "@aglyn/shared-data-mdi";
20
21
  import { MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
21
22
  import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
23
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
22
24
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
25
  import { useUser, useUserName } from "@aglyn/tenant-feature-instance";
24
- import { Button, Chip, IconButton, Stack, Tooltip, Typography } from "@mui/material";
26
+ import { Chip, IconButton, Stack, Tooltip, Typography } from "@mui/material";
25
27
  import { deleteDoc, doc } from "firebase/firestore";
26
- import { useCallback } from "react";
28
+ import { useCallback, useState } from "react";
27
29
  import { useCanEditActivity } from "./activity-queries.js";
30
+ import { TimelineEntry, useTimelineExpansion, useTimelinePages } from "./activity-timeline.js";
28
31
  /**
29
32
  * One glyph per kind. Typed against the union so a kind added to the list
30
33
  * cannot render with no icon — `MdiIcon` would fall back to its default
@@ -68,6 +71,31 @@ ActivityKindIcon.displayName = 'ActivityKindIcon';
68
71
  ownName
69
72
  ]);
70
73
  }
74
+ /**
75
+ * How long a note's own words may run on a collapsed row before it opens to
76
+ * the rest; a single line under this reads whole, with nothing to open.
77
+ */ const ACTIVITY_HEADING_MAX = 80;
78
+ /**
79
+ * What a collapsed activity reads as, and whether opening it shows more.
80
+ *
81
+ * A subject is the heading and the body is what the row opens to. With no
82
+ * subject the body's first line is the heading, and a body that runs past
83
+ * that line, or past what one line holds, opens to the whole text.
84
+ */ export function activityHeading(activity) {
85
+ var _ref;
86
+ var _body_split_find;
87
+ const body = typeof activity.body === 'string' ? activity.body.trim() : '';
88
+ const subject = typeof activity.subject === 'string' ? activity.subject.trim() : '';
89
+ if (subject) return {
90
+ heading: subject,
91
+ opens: body !== ''
92
+ };
93
+ const firstLine = (_ref = (_body_split_find = body.split('\n').find((line)=>line.trim() !== '')) == null ? void 0 : _body_split_find.trim()) != null ? _ref : '';
94
+ return {
95
+ heading: firstLine,
96
+ opens: body !== firstLine || firstLine.length > ACTIVITY_HEADING_MAX
97
+ };
98
+ }
71
99
  /** "45 min" or "2 h 05 min" — a duration a manager reads at a glance. */ function durationLabel(minutes) {
72
100
  if (minutes < 60) return `${minutes} min`;
73
101
  const hours = Math.floor(minutes / 60);
@@ -79,13 +107,22 @@ ActivityKindIcon.displayName = 'ActivityKindIcon';
79
107
  * went, who logged it and how long ago — with edit and delete for whoever
80
108
  * may (AGL-2600).
81
109
  *
110
+ * Collapsed, the row is its chips, its heading and its caption: the kind,
111
+ * the direction and delivery state, how a call went, the subject or the
112
+ * note's first line, and who, to or from whom, and when. Opened, it shows
113
+ * the body — an email's whole text, a note's every line.
114
+ *
82
115
  * Edit and delete appear only when `editable` says this reader may — the
83
116
  * author or an org-wide member. That is the console's verdict and not the
84
117
  * rules' — see `useCanEditActivity` for why the rules admit more — so the
85
118
  * controls are hidden rather than disabled: a disabled button asks "why
86
119
  * not?", and the honest answer would be "you could, through the API".
87
120
  */ export function ActivityRow(props) {
121
+ var _props_expanded, _props_onToggle;
88
122
  const { activity, scope, onEdit, subject, nowMs, editable } = props;
123
+ const [ownExpanded, setOwnExpanded] = useState(false);
124
+ const expanded = (_props_expanded = props.expanded) != null ? _props_expanded : ownExpanded;
125
+ const onToggle = (_props_onToggle = props.onToggle) != null ? _props_onToggle : ()=>setOwnExpanded((open)=>!open);
89
126
  const { firestore, dataScope } = scope;
90
127
  const authorName = useActivityAuthorName();
91
128
  const { confirm } = useConfirmationContext();
@@ -145,149 +182,138 @@ ActivityKindIcon.displayName = 'ActivityKindIcon';
145
182
  activity.outcome ? activity.outcome : null,
146
183
  typeof activity.durationMinutes === 'number' && activity.durationMinutes > 0 ? durationLabel(activity.durationMinutes) : null
147
184
  ].filter(Boolean);
148
- return /*#__PURE__*/ _jsxs(Stack, {
149
- direction: "row",
150
- spacing: 1.5,
151
- sx: {
152
- alignItems: 'flex-start'
153
- },
154
- children: [
155
- /*#__PURE__*/ _jsx(Stack, {
156
- sx: {
157
- color: 'text.secondary',
158
- pt: 0.25,
159
- fontSize: (theme)=>theme.typography.h6.fontSize
160
- },
161
- children: received ? /*#__PURE__*/ _jsx(MdiIcon, {
162
- path: mdiEmailReceiveOutline.path
163
- }) : /*#__PURE__*/ _jsx(ActivityKindIcon, {
164
- kind: activity.kind
165
- })
166
- }),
167
- /*#__PURE__*/ _jsxs(Stack, {
168
- spacing: 0.5,
185
+ const { heading, opens } = activityHeading(activity);
186
+ const hasSubject = typeof activity.subject === 'string' && activity.subject.trim() !== '';
187
+ return /*#__PURE__*/ _jsx(TimelineEntry, {
188
+ icon: received ? /*#__PURE__*/ _jsx(MdiIcon, {
189
+ path: mdiEmailReceiveOutline.path
190
+ }) : /*#__PURE__*/ _jsx(ActivityKindIcon, {
191
+ kind: activity.kind
192
+ }),
193
+ label: heading || label.toLowerCase(),
194
+ expanded: expanded,
195
+ onToggle: onToggle,
196
+ chips: /*#__PURE__*/ _jsxs(_Fragment, {
197
+ children: [
198
+ /*#__PURE__*/ _jsx(Chip, {
199
+ label: label,
200
+ size: "small"
201
+ }),
202
+ received ? /*#__PURE__*/ _jsx(Chip, {
203
+ label: "Received",
204
+ size: "small",
205
+ variant: "outlined",
206
+ color: "info",
207
+ "data-testid": "activity-direction"
208
+ }) : captured ? /*#__PURE__*/ _jsx(Chip, {
209
+ label: "Sent",
210
+ size: "small",
211
+ variant: "outlined",
212
+ "data-testid": "activity-direction"
213
+ }) : null,
214
+ deliveryState ? /*#__PURE__*/ _jsx(Tooltip, {
215
+ title: typeof activity.deliveryAtMs === 'number' ? new Date(activity.deliveryAtMs).toLocaleString() : '',
216
+ children: /*#__PURE__*/ _jsx(Chip, {
217
+ label: Aglyn.CRM_EMAIL_DELIVERY_STATE_LABELS[deliveryState],
218
+ size: "small",
219
+ variant: "outlined",
220
+ color: Aglyn.isCrmEmailDeliveryFailure(deliveryState) ? 'error' : 'default',
221
+ "data-testid": "activity-delivery-state"
222
+ })
223
+ }) : null,
224
+ captured ? null : subject,
225
+ detail.length ? /*#__PURE__*/ _jsx(Typography, {
226
+ variant: "caption",
227
+ color: "text.secondary",
228
+ children: detail.join(' · ')
229
+ }) : null
230
+ ]
231
+ }),
232
+ // With no subject the heading IS the body's first line, so an open
233
+ // row shows the body alone rather than its first line twice.
234
+ title: hasSubject || !opens || !expanded ? heading : null,
235
+ titleVariant: hasSubject ? 'subtitle2' : 'body2',
236
+ meta: /*#__PURE__*/ _jsx(Tooltip, {
237
+ title: when.toLocaleString(),
238
+ children: /*#__PURE__*/ _jsx(Typography, {
239
+ variant: "caption",
240
+ color: "text.secondary",
169
241
  sx: {
170
- flex: 1,
171
- minWidth: 0
242
+ alignSelf: 'flex-start'
172
243
  },
173
244
  children: [
174
- /*#__PURE__*/ _jsxs(Stack, {
175
- direction: "row",
176
- spacing: 1,
177
- sx: {
178
- alignItems: 'center',
179
- flexWrap: 'wrap',
180
- rowGap: 0.5
181
- },
182
- children: [
183
- /*#__PURE__*/ _jsx(Chip, {
184
- label: label,
185
- size: "small"
186
- }),
187
- received ? /*#__PURE__*/ _jsx(Chip, {
188
- label: "Received",
189
- size: "small",
190
- variant: "outlined",
191
- color: "info",
192
- "data-testid": "activity-direction"
193
- }) : captured ? /*#__PURE__*/ _jsx(Chip, {
194
- label: "Sent",
195
- size: "small",
196
- variant: "outlined",
197
- "data-testid": "activity-direction"
198
- }) : null,
199
- deliveryState ? /*#__PURE__*/ _jsx(Tooltip, {
200
- title: typeof activity.deliveryAtMs === 'number' ? new Date(activity.deliveryAtMs).toLocaleString() : '',
201
- children: /*#__PURE__*/ _jsx(Chip, {
202
- label: Aglyn.CRM_EMAIL_DELIVERY_STATE_LABELS[deliveryState],
203
- size: "small",
204
- variant: "outlined",
205
- color: Aglyn.isCrmEmailDeliveryFailure(deliveryState) ? 'error' : 'default',
206
- "data-testid": "activity-delivery-state"
207
- })
208
- }) : null,
209
- captured ? null : subject,
210
- detail.length ? /*#__PURE__*/ _jsx(Typography, {
211
- variant: "caption",
212
- color: "text.secondary",
213
- children: detail.join(' · ')
214
- }) : null
215
- ]
216
- }),
217
- activity.subject ? /*#__PURE__*/ _jsx(Typography, {
218
- variant: "subtitle2",
219
- children: activity.subject
220
- }) : null,
221
- /*#__PURE__*/ _jsx(Typography, {
222
- variant: "body2",
223
- sx: {
224
- whiteSpace: 'pre-wrap'
225
- },
226
- children: activity.body
227
- }),
228
- /*#__PURE__*/ _jsx(Tooltip, {
229
- title: when.toLocaleString(),
230
- children: /*#__PURE__*/ _jsx(Typography, {
231
- variant: "caption",
232
- color: "text.secondary",
233
- sx: {
234
- alignSelf: 'flex-start'
235
- },
236
- children: [
237
- received ? null : authorName(activity),
238
- received && activity.from ? `from ${activity.from}` : null,
239
- sent && activity.to ? `to ${activity.to}` : null,
240
- Aglyn.activityTimeLabel(activity.atMs, nowMs != null ? nowMs : Date.now())
241
- ].filter(Boolean).join(' · ')
242
- })
245
+ received ? null : authorName(activity),
246
+ received && activity.from ? `from ${activity.from}` : null,
247
+ sent && activity.to ? `to ${activity.to}` : null,
248
+ Aglyn.activityTimeLabel(activity.atMs, nowMs != null ? nowMs : Date.now())
249
+ ].filter(Boolean).join(' · ')
250
+ })
251
+ }),
252
+ body: opens ? /*#__PURE__*/ _jsx(Typography, {
253
+ variant: "body2",
254
+ sx: {
255
+ whiteSpace: 'pre-wrap',
256
+ overflowWrap: 'anywhere'
257
+ },
258
+ children: activity.body
259
+ }) : undefined,
260
+ actions: editable ? /*#__PURE__*/ _jsxs(_Fragment, {
261
+ children: [
262
+ onEdit && !sent && !received ? /*#__PURE__*/ _jsx(IconButton, {
263
+ size: "small",
264
+ "aria-label": "Edit activity",
265
+ onClick: ()=>onEdit(activity),
266
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
267
+ path: mdiPencilOutline.path,
268
+ size: 0.8
243
269
  })
244
- ]
245
- }),
246
- editable ? /*#__PURE__*/ _jsxs(Stack, {
247
- direction: "row",
248
- spacing: 0.5,
249
- children: [
250
- onEdit && !sent && !received ? /*#__PURE__*/ _jsx(IconButton, {
251
- size: "small",
252
- "aria-label": "Edit activity",
253
- onClick: ()=>onEdit(activity),
254
- children: /*#__PURE__*/ _jsx(MdiIcon, {
255
- path: mdiPencilOutline.path,
256
- size: 0.8
257
- })
258
- }) : null,
259
- /*#__PURE__*/ _jsx(IconButton, {
260
- size: "small",
261
- "aria-label": "Delete activity",
262
- onClick: handleDelete,
263
- children: /*#__PURE__*/ _jsx(MdiIcon, {
264
- path: mdiDeleteOutline.path,
265
- size: 0.8
266
- })
270
+ }) : null,
271
+ /*#__PURE__*/ _jsx(IconButton, {
272
+ size: "small",
273
+ "aria-label": "Delete activity",
274
+ onClick: handleDelete,
275
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
276
+ path: mdiDeleteOutline.path,
277
+ size: 0.8
267
278
  })
268
- ]
269
- }) : null
270
- ]
279
+ })
280
+ ]
281
+ }) : null
271
282
  });
272
283
  }
273
284
  ActivityRow.displayName = 'ActivityRow';
274
285
  /**
275
- * A newest-first list of logged activities with a "show more" foot
276
- * (AGL-2600).
286
+ * A newest-first list of logged activities, collapsed to one entry each and
287
+ * paged by the console's shared footer (AGL-2600).
277
288
  *
278
289
  * The rows arrive ordered — the query is `orderBy('atMs', 'desc')` — and the
279
290
  * list does not sort them again; a second sort here is a second place for
280
- * the order to be defined. The foot appears only while the probe row says
281
- * more exists, so it never leads nowhere.
291
+ * the order to be defined. The footer pages the window the listener holds,
292
+ * and a page turned past it widens the listener while the probe row says
293
+ * more exists (`useTimelinePages`).
282
294
  *
283
295
  * Who may edit is decided here, once, and handed to every row: the verdict
284
296
  * reads the member document, and the list is the one place that can ask
285
297
  * for it a single time however many rows it draws.
286
298
  */ export function ActivityList(props) {
287
- const { rows, scope, onEdit, emptyText = 'Nothing logged yet.', emptyAction, hasMore, onShowMore, subjectFor, readOnly } = props;
299
+ var _props_expansion;
300
+ const { rows, scope, onEdit, emptyText = 'Nothing logged yet.', emptyAction, hasMore, onShowMore, loading, subjectFor, readOnly } = props;
288
301
  const canEdit = useCanEditActivity(scope.orgId, !readOnly);
302
+ const ownExpansion = useTimelineExpansion();
303
+ const expansion = (_props_expansion = props.expansion) != null ? _props_expansion : ownExpansion;
304
+ const pages = useTimelinePages(rows.length, {
305
+ hasMore,
306
+ showMore: onShowMore,
307
+ loading
308
+ });
309
+ // One clock for every row of one paint.
310
+ const nowMs = Date.now();
289
311
  if (!rows.length) {
290
- return /*#__PURE__*/ _jsx(EmptyStateComponent, {
312
+ return loading ? /*#__PURE__*/ _jsx(Typography, {
313
+ variant: "body2",
314
+ color: "text.secondary",
315
+ children: 'Loading…'
316
+ }) : /*#__PURE__*/ _jsx(EmptyStateComponent, {
291
317
  compact: true,
292
318
  label: 'Nothing logged yet',
293
319
  description: emptyText,
@@ -297,21 +323,17 @@ ActivityRow.displayName = 'ActivityRow';
297
323
  return /*#__PURE__*/ _jsxs(Stack, {
298
324
  spacing: 2,
299
325
  children: [
300
- rows.map((activity)=>/*#__PURE__*/ _jsx(ActivityRow, {
326
+ pages.slice(rows).map((activity)=>/*#__PURE__*/ _jsx(ActivityRow, {
301
327
  activity: activity,
302
328
  scope: scope,
303
329
  onEdit: onEdit,
304
330
  subject: subjectFor == null ? void 0 : subjectFor(activity),
305
- editable: canEdit(activity)
331
+ nowMs: nowMs,
332
+ editable: canEdit(activity),
333
+ expanded: expansion.isExpanded(activity.$id),
334
+ onToggle: ()=>expansion.toggle(activity.$id)
306
335
  }, activity.$id)),
307
- hasMore && onShowMore ? /*#__PURE__*/ _jsx(Button, {
308
- size: "small",
309
- onClick: onShowMore,
310
- sx: {
311
- alignSelf: 'flex-start'
312
- },
313
- children: 'Show more'
314
- }) : null
336
+ /*#__PURE__*/ _jsx(ListPagination, _extends({}, pages.footer))
315
337
  ]
316
338
  });
317
339
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/activity-list.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { CrmActivityKind, CrmActivityRow } from '@aglyn/aglyn'\nimport {\n mdiAccountGroupOutline,\n mdiDeleteOutline,\n mdiDotsHorizontalCircleOutline,\n mdiEmailOutline,\n mdiEmailReceiveOutline,\n mdiNoteTextOutline,\n mdiPencilOutline,\n mdiPhoneOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n IconButton,\n Stack,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteDoc, doc } from 'firebase/firestore'\nimport { type ReactNode, useCallback } from 'react'\nimport { type ActivityScope, useCanEditActivity } from './activity-queries'\n\n/**\n * One glyph per kind. Typed against the union so a kind added to the list\n * cannot render with no icon — `MdiIcon` would fall back to its default\n * glyph silently, and a log where every note looks like a placeholder is\n * worse than a build error.\n */\nconst KIND_ICONS: Record<CrmActivityKind, { path: string }> = {\n call: mdiPhoneOutline,\n email: mdiEmailOutline,\n meeting: mdiAccountGroupOutline,\n note: mdiNoteTextOutline,\n other: mdiDotsHorizontalCircleOutline,\n}\n\n/** The icon for a kind — a stored kind outside the union draws as `other`. */\nexport function ActivityKindIcon(props: { kind: string; size?: number }) {\n const { kind, size = 1 } = props\n const icon = Aglyn.isCrmActivityKind(kind) ? KIND_ICONS[kind] : KIND_ICONS.other\n return <MdiIcon path={icon.path} size={size} />\n}\nActivityKindIcon.displayName = 'ActivityKindIcon'\n\n/**\n * The author's name for a row (AGL-2600).\n *\n * The denormalized `byName` leads: it is the only name a scoped colleague\n * can read, since a member document is readable by its subject and by\n * org-wide members alone. The signed-in user's OWN rows fall back to their\n * live name — the one case where a lookup is possible — and a row that\n * carries neither (written by a door that did not stamp one) reads as a\n * team member rather than as a uid.\n */\nexport function useActivityAuthorName(): (\n activity: Pick<CrmActivityRow, 'byUid' | 'byName'>,\n) => string {\n const { data: user } = useUser()\n const ownName = useUserName()\n const uid = user?.uid\n return useCallback(\n (activity) => {\n if (activity.byName) return activity.byName\n if (uid && activity.byUid === uid && ownName) return ownName\n return 'A team member'\n },\n [uid, ownName],\n )\n}\n\n/** \"45 min\" or \"2 h 05 min\" — a duration a manager reads at a glance. */\nfunction durationLabel(minutes: number): string {\n if (minutes < 60) return `${minutes} min`\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n return rest ? `${hours} h ${String(rest).padStart(2, '0')} min` : `${hours} h`\n}\n\nexport interface ActivityRowProps {\n activity: CrmActivityRow\n /** Where the delete is written, from `useActivityScope`. */\n scope: ActivityScope\n /** Opens the dialog on this activity. Absent, the row offers no edit. */\n onEdit?: (activity: CrmActivityRow) => void\n /**\n * What sits after the kind, before the body — a feed puts the record the\n * activity is about here, a record page needs nothing.\n */\n subject?: ReactNode\n /** The clock the relative time is read against; `Date.now()` when absent. */\n nowMs?: number\n /**\n * Whether THIS reader may edit and delete the row — the author, or an\n * org-wide member. Decided by whoever draws the rows, through\n * `useCanEditActivity`, once for the whole list: a row that asked for\n * itself would read the member document once per row.\n */\n editable?: boolean\n}\n\n/**\n * One logged activity: the kind's icon and label, what was said, how it\n * went, who logged it and how long ago — with edit and delete for whoever\n * may (AGL-2600).\n *\n * Edit and delete appear only when `editable` says this reader may — the\n * author or an org-wide member. That is the console's verdict and not the\n * rules' — see `useCanEditActivity` for why the rules admit more — so the\n * controls are hidden rather than disabled: a disabled button asks \"why\n * not?\", and the honest answer would be \"you could, through the API\".\n */\nexport function ActivityRow(props: ActivityRowProps) {\n const { activity, scope, onEdit, subject, nowMs, editable } = props\n const { firestore, dataScope } = scope\n const authorName = useActivityAuthorName()\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n\n const handleDelete = useCallback(async () => {\n if (!dataScope) return\n const confirmed = await confirm({\n title: 'Delete this activity?',\n description:\n `This ${Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ].toLowerCase()} is removed from the record it was logged against. ` +\n 'Nothing the platform captured — a form, an order, a booking — is ' +\n 'affected.',\n confirmationText: 'Delete activity',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n activity.$id,\n ),\n )\n enqueueSnackbar('Activity deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [dataScope, confirm, activity, firestore, enqueueSnackbar])\n\n const label =\n Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ]\n /*\n * A message the platform SENT (AGL-2615) carries a subject, an address\n * and a delivery state the webhook advances; the state is the chip beside\n * the kind, red once the message did not land. What was sent is a record\n * of a fact and offers no edit — a rewritten body would misstate what\n * left — though whoever may delete a row may still delete this one.\n */\n const sent = activity.direction === 'outbound'\n /*\n * A message CAPTURED from a mailbox (AGL-2657) carries the provider's\n * Message-ID and, when a correspondent wrote it, the `inbound` direction.\n * It is drawn as its own thing — \"Received\" beside the kind, the sender\n * where a sent row shows the recipient, no author because nobody on the\n * team logged it — and a member's copied send reads \"Sent\" without a\n * delivery chip, since the platform did not carry it. Neither takes the\n * caller's \"Logged\" chip: nobody logged them.\n */\n const received = activity.direction === 'inbound'\n const captured = typeof activity.messageId === 'string' && activity.messageId !== ''\n const deliveryState = Aglyn.isCrmEmailDeliveryState(activity.deliveryState)\n ? activity.deliveryState\n : null\n const when = new Date(activity.atMs)\n const detail = [\n activity.outcome ? activity.outcome : null,\n typeof activity.durationMinutes === 'number' && activity.durationMinutes > 0\n ? durationLabel(activity.durationMinutes)\n : null,\n ].filter(Boolean)\n\n return (\n <Stack direction=\"row\" spacing={1.5} sx={{ alignItems: 'flex-start' }}>\n <Stack\n sx={{\n color: 'text.secondary',\n pt: 0.25,\n fontSize: (theme) => theme.typography.h6.fontSize,\n }}\n >\n {received ? (\n <MdiIcon path={mdiEmailReceiveOutline.path} />\n ) : (\n <ActivityKindIcon kind={activity.kind} />\n )}\n </Stack>\n <Stack spacing={0.5} sx={{ flex: 1, minWidth: 0 }}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Chip label={label} size=\"small\" />\n {received ? (\n <Chip\n label=\"Received\"\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n data-testid=\"activity-direction\"\n />\n ) : captured ? (\n <Chip\n label=\"Sent\"\n size=\"small\"\n variant=\"outlined\"\n data-testid=\"activity-direction\"\n />\n ) : null}\n {deliveryState ? (\n <Tooltip\n title={\n typeof activity.deliveryAtMs === 'number'\n ? new Date(activity.deliveryAtMs).toLocaleString()\n : ''\n }\n >\n <Chip\n label={Aglyn.CRM_EMAIL_DELIVERY_STATE_LABELS[deliveryState]}\n size=\"small\"\n variant=\"outlined\"\n color={Aglyn.isCrmEmailDeliveryFailure(deliveryState) ? 'error' : 'default'}\n data-testid=\"activity-delivery-state\"\n />\n </Tooltip>\n ) : null}\n {captured ? null : subject}\n {detail.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {detail.join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n {activity.subject ? (\n <Typography variant=\"subtitle2\">{activity.subject}</Typography>\n ) : null}\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {activity.body}\n </Typography>\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {[\n received ? null : authorName(activity),\n received && activity.from ? `from ${activity.from}` : null,\n sent && activity.to ? `to ${activity.to}` : null,\n Aglyn.activityTimeLabel(activity.atMs, nowMs ?? Date.now()),\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n </Tooltip>\n </Stack>\n {editable ? (\n <Stack direction=\"row\" spacing={0.5}>\n {onEdit && !sent && !received ? (\n <IconButton\n size=\"small\"\n aria-label=\"Edit activity\"\n onClick={() => onEdit(activity)}\n >\n <MdiIcon path={mdiPencilOutline.path} size={0.8} />\n </IconButton>\n ) : null}\n <IconButton\n size=\"small\"\n aria-label=\"Delete activity\"\n onClick={handleDelete}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </Stack>\n ) : null}\n </Stack>\n )\n}\nActivityRow.displayName = 'ActivityRow'\n\nexport interface ActivityListProps {\n /** Newest-first, as the listener hands them back. */\n rows: readonly CrmActivityRow[]\n scope: ActivityScope\n onEdit?: (activity: CrmActivityRow) => void\n /** What the list says when there is nothing in it. */\n emptyText?: string\n /** The way out of an empty list — the record's \"Log activity\" button. */\n emptyAction?: ReactNode\n /** A further page exists; `onShowMore` widens the window. */\n hasMore?: boolean\n onShowMore?: () => void\n /** The record each row is about, for a list that spans records. */\n subjectFor?: (activity: CrmActivityRow) => ReactNode\n /**\n * No controls on any row, whoever is reading — and no member read to\n * decide them. A feed that spans records is a place to see what happened,\n * not to rewrite it; the record's own page is where an activity is\n * corrected, beside everything else about it.\n */\n readOnly?: boolean\n}\n\n/**\n * A newest-first list of logged activities with a \"show more\" foot\n * (AGL-2600).\n *\n * The rows arrive ordered — the query is `orderBy('atMs', 'desc')` — and the\n * list does not sort them again; a second sort here is a second place for\n * the order to be defined. The foot appears only while the probe row says\n * more exists, so it never leads nowhere.\n *\n * Who may edit is decided here, once, and handed to every row: the verdict\n * reads the member document, and the list is the one place that can ask\n * for it a single time however many rows it draws.\n */\nexport function ActivityList(props: ActivityListProps) {\n const {\n rows,\n scope,\n onEdit,\n emptyText = 'Nothing logged yet.',\n emptyAction,\n hasMore,\n onShowMore,\n subjectFor,\n readOnly,\n } = props\n const canEdit = useCanEditActivity(scope.orgId, !readOnly)\n if (!rows.length) {\n return (\n <EmptyStateComponent\n compact\n label={'Nothing logged yet'}\n description={emptyText}\n action={emptyAction}\n />\n )\n }\n return (\n <Stack spacing={2}>\n {rows.map((activity) => (\n <ActivityRow\n key={activity.$id}\n activity={activity}\n scope={scope}\n onEdit={onEdit}\n subject={subjectFor?.(activity)}\n editable={canEdit(activity)}\n />\n ))}\n {hasMore && onShowMore ? (\n <Button size=\"small\" onClick={onShowMore} sx={{ alignSelf: 'flex-start' }}>\n {'Show more'}\n </Button>\n ) : null}\n </Stack>\n )\n}\nActivityList.displayName = 'ActivityList'\n\nexport default ActivityList\n"],"names":["Aglyn","mdiAccountGroupOutline","mdiDeleteOutline","mdiDotsHorizontalCircleOutline","mdiEmailOutline","mdiEmailReceiveOutline","mdiNoteTextOutline","mdiPencilOutline","mdiPhoneOutline","MdiIcon","useConfirmationContext","EmptyStateComponent","useSnackbar","useUser","useUserName","Button","Chip","IconButton","Stack","Tooltip","Typography","deleteDoc","doc","useCallback","useCanEditActivity","KIND_ICONS","call","email","meeting","note","other","ActivityKindIcon","props","kind","size","icon","isCrmActivityKind","path","displayName","useActivityAuthorName","data","user","ownName","uid","activity","byName","byUid","durationLabel","minutes","hours","Math","floor","rest","String","padStart","ActivityRow","scope","onEdit","subject","nowMs","editable","firestore","dataScope","authorName","confirm","enqueueSnackbar","handleDelete","confirmed","title","description","CRM_ACTIVITY_KIND_LABELS","toLowerCase","confirmationText","confirmationButtonProps","color","then","catch","CRM_COLLECTIONS","activities","$id","variant","persist","error","console","allowDuplicate","label","sent","direction","received","captured","messageId","deliveryState","isCrmEmailDeliveryState","when","Date","atMs","detail","outcome","durationMinutes","filter","Boolean","spacing","sx","alignItems","pt","fontSize","theme","typography","h6","flex","minWidth","flexWrap","rowGap","data-testid","deliveryAtMs","toLocaleString","CRM_EMAIL_DELIVERY_STATE_LABELS","isCrmEmailDeliveryFailure","length","join","whiteSpace","body","alignSelf","from","to","activityTimeLabel","now","aria-label","onClick","ActivityList","rows","emptyText","emptyAction","hasMore","onShowMore","subjectFor","readOnly","canEdit","orgId","compact","action","map"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/activity-list.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { CrmActivityKind, CrmActivityRow } from '@aglyn/aglyn'\nimport {\n mdiAccountGroupOutline,\n mdiDeleteOutline,\n mdiDotsHorizontalCircleOutline,\n mdiEmailOutline,\n mdiEmailReceiveOutline,\n mdiNoteTextOutline,\n mdiPencilOutline,\n mdiPhoneOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport { Chip, IconButton, Stack, Tooltip, Typography } from '@mui/material'\nimport { deleteDoc, doc } from 'firebase/firestore'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { type ActivityScope, useCanEditActivity } from './activity-queries'\nimport {\n TimelineEntry,\n type TimelineExpansion,\n useTimelineExpansion,\n useTimelinePages,\n} from './activity-timeline'\n\n/**\n * One glyph per kind. Typed against the union so a kind added to the list\n * cannot render with no icon — `MdiIcon` would fall back to its default\n * glyph silently, and a log where every note looks like a placeholder is\n * worse than a build error.\n */\nconst KIND_ICONS: Record<CrmActivityKind, { path: string }> = {\n call: mdiPhoneOutline,\n email: mdiEmailOutline,\n meeting: mdiAccountGroupOutline,\n note: mdiNoteTextOutline,\n other: mdiDotsHorizontalCircleOutline,\n}\n\n/** The icon for a kind — a stored kind outside the union draws as `other`. */\nexport function ActivityKindIcon(props: { kind: string; size?: number }) {\n const { kind, size = 1 } = props\n const icon = Aglyn.isCrmActivityKind(kind) ? KIND_ICONS[kind] : KIND_ICONS.other\n return <MdiIcon path={icon.path} size={size} />\n}\nActivityKindIcon.displayName = 'ActivityKindIcon'\n\n/**\n * The author's name for a row (AGL-2600).\n *\n * The denormalized `byName` leads: it is the only name a scoped colleague\n * can read, since a member document is readable by its subject and by\n * org-wide members alone. The signed-in user's OWN rows fall back to their\n * live name — the one case where a lookup is possible — and a row that\n * carries neither (written by a door that did not stamp one) reads as a\n * team member rather than as a uid.\n */\nexport function useActivityAuthorName(): (\n activity: Pick<CrmActivityRow, 'byUid' | 'byName'>,\n) => string {\n const { data: user } = useUser()\n const ownName = useUserName()\n const uid = user?.uid\n return useCallback(\n (activity) => {\n if (activity.byName) return activity.byName\n if (uid && activity.byUid === uid && ownName) return ownName\n return 'A team member'\n },\n [uid, ownName],\n )\n}\n\n/**\n * How long a note's own words may run on a collapsed row before it opens to\n * the rest; a single line under this reads whole, with nothing to open.\n */\nconst ACTIVITY_HEADING_MAX = 80\n\n/**\n * What a collapsed activity reads as, and whether opening it shows more.\n *\n * A subject is the heading and the body is what the row opens to. With no\n * subject the body's first line is the heading, and a body that runs past\n * that line, or past what one line holds, opens to the whole text.\n */\nexport function activityHeading(activity: Pick<CrmActivityRow, 'subject' | 'body'>): {\n heading: string\n opens: boolean\n} {\n const body = typeof activity.body === 'string' ? activity.body.trim() : ''\n const subject = typeof activity.subject === 'string' ? activity.subject.trim() : ''\n if (subject) return { heading: subject, opens: body !== '' }\n const firstLine = body.split('\\n').find((line) => line.trim() !== '')?.trim() ?? ''\n return {\n heading: firstLine,\n opens: body !== firstLine || firstLine.length > ACTIVITY_HEADING_MAX,\n }\n}\n\n/** \"45 min\" or \"2 h 05 min\" — a duration a manager reads at a glance. */\nfunction durationLabel(minutes: number): string {\n if (minutes < 60) return `${minutes} min`\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n return rest ? `${hours} h ${String(rest).padStart(2, '0')} min` : `${hours} h`\n}\n\nexport interface ActivityRowProps {\n activity: CrmActivityRow\n /** Where the delete is written, from `useActivityScope`. */\n scope: ActivityScope\n /** Opens the dialog on this activity. Absent, the row offers no edit. */\n onEdit?: (activity: CrmActivityRow) => void\n /**\n * What sits after the kind, before the body — a feed puts the record the\n * activity is about here, a record page needs nothing.\n */\n subject?: ReactNode\n /** The clock the relative time is read against; `Date.now()` when absent. */\n nowMs?: number\n /**\n * Whether THIS reader may edit and delete the row — the author, or an\n * org-wide member. Decided by whoever draws the rows, through\n * `useCanEditActivity`, once for the whole list: a row that asked for\n * itself would read the member document once per row.\n */\n editable?: boolean\n /**\n * Whether the row is open to its body. The list holds it so the card's\n * \"Expand all\" reaches every row; a row drawn alone holds its own.\n */\n expanded?: boolean\n onToggle?: () => void\n}\n\n/**\n * One logged activity: the kind's icon and label, what was said, how it\n * went, who logged it and how long ago — with edit and delete for whoever\n * may (AGL-2600).\n *\n * Collapsed, the row is its chips, its heading and its caption: the kind,\n * the direction and delivery state, how a call went, the subject or the\n * note's first line, and who, to or from whom, and when. Opened, it shows\n * the body — an email's whole text, a note's every line.\n *\n * Edit and delete appear only when `editable` says this reader may — the\n * author or an org-wide member. That is the console's verdict and not the\n * rules' — see `useCanEditActivity` for why the rules admit more — so the\n * controls are hidden rather than disabled: a disabled button asks \"why\n * not?\", and the honest answer would be \"you could, through the API\".\n */\nexport function ActivityRow(props: ActivityRowProps) {\n const { activity, scope, onEdit, subject, nowMs, editable } = props\n const [ownExpanded, setOwnExpanded] = useState(false)\n const expanded = props.expanded ?? ownExpanded\n const onToggle = props.onToggle ?? (() => setOwnExpanded((open) => !open))\n const { firestore, dataScope } = scope\n const authorName = useActivityAuthorName()\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n\n const handleDelete = useCallback(async () => {\n if (!dataScope) return\n const confirmed = await confirm({\n title: 'Delete this activity?',\n description:\n `This ${Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ].toLowerCase()} is removed from the record it was logged against. ` +\n 'Nothing the platform captured — a form, an order, a booking — is ' +\n 'affected.',\n confirmationText: 'Delete activity',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n Aglyn.CRM_COLLECTIONS.activities,\n activity.$id,\n ),\n )\n enqueueSnackbar('Activity deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [dataScope, confirm, activity, firestore, enqueueSnackbar])\n\n const label =\n Aglyn.CRM_ACTIVITY_KIND_LABELS[\n Aglyn.isCrmActivityKind(activity.kind) ? activity.kind : 'other'\n ]\n /*\n * A message the platform SENT (AGL-2615) carries a subject, an address\n * and a delivery state the webhook advances; the state is the chip beside\n * the kind, red once the message did not land. What was sent is a record\n * of a fact and offers no edit — a rewritten body would misstate what\n * left — though whoever may delete a row may still delete this one.\n */\n const sent = activity.direction === 'outbound'\n /*\n * A message CAPTURED from a mailbox (AGL-2657) carries the provider's\n * Message-ID and, when a correspondent wrote it, the `inbound` direction.\n * It is drawn as its own thing — \"Received\" beside the kind, the sender\n * where a sent row shows the recipient, no author because nobody on the\n * team logged it — and a member's copied send reads \"Sent\" without a\n * delivery chip, since the platform did not carry it. Neither takes the\n * caller's \"Logged\" chip: nobody logged them.\n */\n const received = activity.direction === 'inbound'\n const captured = typeof activity.messageId === 'string' && activity.messageId !== ''\n const deliveryState = Aglyn.isCrmEmailDeliveryState(activity.deliveryState)\n ? activity.deliveryState\n : null\n const when = new Date(activity.atMs)\n const detail = [\n activity.outcome ? activity.outcome : null,\n typeof activity.durationMinutes === 'number' && activity.durationMinutes > 0\n ? durationLabel(activity.durationMinutes)\n : null,\n ].filter(Boolean)\n\n const { heading, opens } = activityHeading(activity)\n const hasSubject = typeof activity.subject === 'string' && activity.subject.trim() !== ''\n\n return (\n <TimelineEntry\n icon={\n received ? (\n <MdiIcon path={mdiEmailReceiveOutline.path} />\n ) : (\n <ActivityKindIcon kind={activity.kind} />\n )\n }\n label={heading || label.toLowerCase()}\n expanded={expanded}\n onToggle={onToggle}\n chips={\n <>\n <Chip label={label} size=\"small\" />\n {received ? (\n <Chip\n label=\"Received\"\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n data-testid=\"activity-direction\"\n />\n ) : captured ? (\n <Chip\n label=\"Sent\"\n size=\"small\"\n variant=\"outlined\"\n data-testid=\"activity-direction\"\n />\n ) : null}\n {deliveryState ? (\n <Tooltip\n title={\n typeof activity.deliveryAtMs === 'number'\n ? new Date(activity.deliveryAtMs).toLocaleString()\n : ''\n }\n >\n <Chip\n label={Aglyn.CRM_EMAIL_DELIVERY_STATE_LABELS[deliveryState]}\n size=\"small\"\n variant=\"outlined\"\n color={Aglyn.isCrmEmailDeliveryFailure(deliveryState) ? 'error' : 'default'}\n data-testid=\"activity-delivery-state\"\n />\n </Tooltip>\n ) : null}\n {captured ? null : subject}\n {detail.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {detail.join(' · ')}\n </Typography>\n ) : null}\n </>\n }\n // With no subject the heading IS the body's first line, so an open\n // row shows the body alone rather than its first line twice.\n title={hasSubject || !opens || !expanded ? heading : null}\n titleVariant={hasSubject ? 'subtitle2' : 'body2'}\n meta={\n <Tooltip title={when.toLocaleString()}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ alignSelf: 'flex-start' }}\n >\n {[\n received ? null : authorName(activity),\n received && activity.from ? `from ${activity.from}` : null,\n sent && activity.to ? `to ${activity.to}` : null,\n Aglyn.activityTimeLabel(activity.atMs, nowMs ?? Date.now()),\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n </Tooltip>\n }\n body={\n opens ? (\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>\n {activity.body}\n </Typography>\n ) : undefined\n }\n actions={\n editable ? (\n <>\n {onEdit && !sent && !received ? (\n <IconButton\n size=\"small\"\n aria-label=\"Edit activity\"\n onClick={() => onEdit(activity)}\n >\n <MdiIcon path={mdiPencilOutline.path} size={0.8} />\n </IconButton>\n ) : null}\n <IconButton\n size=\"small\"\n aria-label=\"Delete activity\"\n onClick={handleDelete}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </>\n ) : null\n }\n />\n )\n}\nActivityRow.displayName = 'ActivityRow'\n\nexport interface ActivityListProps {\n /** Newest-first, as the listener hands them back. */\n rows: readonly CrmActivityRow[]\n scope: ActivityScope\n onEdit?: (activity: CrmActivityRow) => void\n /** What the list says when there is nothing in it. */\n emptyText?: string\n /** The way out of an empty list — the record's \"Log activity\" button. */\n emptyAction?: ReactNode\n /** The listener holds less than exists; `onShowMore` widens it. */\n hasMore?: boolean\n onShowMore?: () => void\n /** The listener is reading, so an empty window is not yet an empty log. */\n loading?: boolean\n /** The record each row is about, for a list that spans records. */\n subjectFor?: (activity: CrmActivityRow) => ReactNode\n /**\n * No controls on any row, whoever is reading — and no member read to\n * decide them. A feed that spans records is a place to see what happened,\n * not to rewrite it; the record's own page is where an activity is\n * corrected, beside everything else about it.\n */\n readOnly?: boolean\n /**\n * Which rows are open, held by the card so its header's \"Expand all\"\n * reaches them. A list with no header control holds its own.\n */\n expansion?: TimelineExpansion\n}\n\n/**\n * A newest-first list of logged activities, collapsed to one entry each and\n * paged by the console's shared footer (AGL-2600).\n *\n * The rows arrive ordered — the query is `orderBy('atMs', 'desc')` — and the\n * list does not sort them again; a second sort here is a second place for\n * the order to be defined. The footer pages the window the listener holds,\n * and a page turned past it widens the listener while the probe row says\n * more exists (`useTimelinePages`).\n *\n * Who may edit is decided here, once, and handed to every row: the verdict\n * reads the member document, and the list is the one place that can ask\n * for it a single time however many rows it draws.\n */\nexport function ActivityList(props: ActivityListProps) {\n const {\n rows,\n scope,\n onEdit,\n emptyText = 'Nothing logged yet.',\n emptyAction,\n hasMore,\n onShowMore,\n loading,\n subjectFor,\n readOnly,\n } = props\n const canEdit = useCanEditActivity(scope.orgId, !readOnly)\n const ownExpansion = useTimelineExpansion()\n const expansion = props.expansion ?? ownExpansion\n const pages = useTimelinePages(rows.length, { hasMore, showMore: onShowMore, loading })\n // One clock for every row of one paint.\n const nowMs = Date.now()\n if (!rows.length) {\n return loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : (\n <EmptyStateComponent\n compact\n label={'Nothing logged yet'}\n description={emptyText}\n action={emptyAction}\n />\n )\n }\n return (\n <Stack spacing={2}>\n {pages.slice(rows).map((activity) => (\n <ActivityRow\n key={activity.$id}\n activity={activity}\n scope={scope}\n onEdit={onEdit}\n subject={subjectFor?.(activity)}\n nowMs={nowMs}\n editable={canEdit(activity)}\n expanded={expansion.isExpanded(activity.$id)}\n onToggle={() => expansion.toggle(activity.$id)}\n />\n ))}\n <ListPagination {...pages.footer} />\n </Stack>\n )\n}\nActivityList.displayName = 'ActivityList'\n\nexport default ActivityList\n"],"names":["Aglyn","mdiAccountGroupOutline","mdiDeleteOutline","mdiDotsHorizontalCircleOutline","mdiEmailOutline","mdiEmailReceiveOutline","mdiNoteTextOutline","mdiPencilOutline","mdiPhoneOutline","MdiIcon","useConfirmationContext","EmptyStateComponent","ListPagination","useSnackbar","useUser","useUserName","Chip","IconButton","Stack","Tooltip","Typography","deleteDoc","doc","useCallback","useState","useCanEditActivity","TimelineEntry","useTimelineExpansion","useTimelinePages","KIND_ICONS","call","email","meeting","note","other","ActivityKindIcon","props","kind","size","icon","isCrmActivityKind","path","displayName","useActivityAuthorName","data","user","ownName","uid","activity","byName","byUid","ACTIVITY_HEADING_MAX","activityHeading","body","trim","subject","heading","opens","firstLine","split","find","line","length","durationLabel","minutes","hours","Math","floor","rest","String","padStart","ActivityRow","scope","onEdit","nowMs","editable","ownExpanded","setOwnExpanded","expanded","onToggle","open","firestore","dataScope","authorName","confirm","enqueueSnackbar","handleDelete","confirmed","title","description","CRM_ACTIVITY_KIND_LABELS","toLowerCase","confirmationText","confirmationButtonProps","color","then","catch","CRM_COLLECTIONS","activities","$id","variant","persist","error","console","allowDuplicate","label","sent","direction","received","captured","messageId","deliveryState","isCrmEmailDeliveryState","when","Date","atMs","detail","outcome","durationMinutes","filter","Boolean","hasSubject","chips","data-testid","deliveryAtMs","toLocaleString","CRM_EMAIL_DELIVERY_STATE_LABELS","isCrmEmailDeliveryFailure","join","titleVariant","meta","sx","alignSelf","from","to","activityTimeLabel","now","whiteSpace","overflowWrap","undefined","actions","aria-label","onClick","ActivityList","rows","emptyText","emptyAction","hasMore","onShowMore","loading","subjectFor","readOnly","canEdit","orgId","ownExpansion","expansion","pages","showMore","compact","action","spacing","slice","map","isExpanded","toggle","footer"],"mappings":"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"}