@aglyn/plugins-crm 1.0.0-beta.200 → 1.0.0-beta.202
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/package.json +13 -13
- package/src/lib/components/activity-list.d.ts +36 -5
- package/src/lib/components/activity-list.js +159 -137
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/activity-timeline.d.ts +84 -0
- package/src/lib/components/activity-timeline.js +224 -0
- package/src/lib/components/activity-timeline.js.map +1 -0
- package/src/lib/components/company-properties-card.js +53 -36
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-associations-card.d.ts +3 -2
- package/src/lib/components/contact-associations-card.js +12 -12
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -3
- package/src/lib/components/contact-detail-page.js +4 -10
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +21 -4
- package/src/lib/components/contact-properties-card.js +129 -29
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.d.ts +12 -6
- package/src/lib/components/contact-timeline-card.js +132 -174
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-section.js +197 -198
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-state-chip.js +4 -1
- package/src/lib/components/crm-email-state-chip.js.map +1 -1
- package/src/lib/components/deal-products-card.js +15 -13
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deal-properties-card.js +7 -0
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.js +31 -29
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-section.js +9 -14
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +4 -0
- package/src/lib/components/fields-section.js +368 -154
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +24 -20
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +1 -1
- package/src/lib/components/lead-campaigns-card.js +32 -35
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +14 -6
- package/src/lib/components/lead-properties-card.js +317 -313
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +13 -2
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +12 -10
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/recent-activity-feed.d.ts +12 -8
- package/src/lib/components/recent-activity-feed.js +54 -29
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/record-activity-card.d.ts +4 -2
- package/src/lib/components/record-activity-card.js +29 -11
- package/src/lib/components/record-activity-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +18 -12
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +47 -37
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +22 -16
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.d.ts +4 -11
- package/src/lib/components/reports/report-export.js +16 -33
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +18 -12
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +35 -25
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/tasks-section.js +23 -23
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/field-list-query.d.ts +23 -0
- package/src/lib/constants/field-list-query.js +80 -0
- package/src/lib/constants/field-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +1 -1
- package/src/lib/server/capture-contact.js +7 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
- package/src/lib/components/contact-custom-fields-card.js +0 -204
- package/src/lib/components/contact-custom-fields-card.js.map +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ListPaginationProps } from '@aglyn/shared-ui-jsx/components/list-pagination.component';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
/** Which entries of one history are open. */
|
|
4
|
+
export interface TimelineExpansion {
|
|
5
|
+
isExpanded: (key: string) => boolean;
|
|
6
|
+
toggle: (key: string) => void;
|
|
7
|
+
/** Every entry is open and none has been closed since. */
|
|
8
|
+
allExpanded: boolean;
|
|
9
|
+
setAllExpanded: (expanded: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The open/closed state of a history's entries, held by the CARD so that
|
|
13
|
+
* the header's "Expand all" and each row's chevron act on one state.
|
|
14
|
+
*
|
|
15
|
+
* Kept as a default plus the keys that differ from it, so "Expand all"
|
|
16
|
+
* reaches the entries of pages the reader has not turned to yet, and a row
|
|
17
|
+
* closed after it stays closed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useTimelineExpansion(): TimelineExpansion;
|
|
20
|
+
/** The card header's "Expand all" / "Collapse all". */
|
|
21
|
+
export declare function TimelineExpandAllButton(props: {
|
|
22
|
+
expansion: TimelineExpansion;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
export declare namespace TimelineExpandAllButton {
|
|
26
|
+
var displayName: string;
|
|
27
|
+
}
|
|
28
|
+
/** The listener window a history pages over. */
|
|
29
|
+
export interface TimelineWindow {
|
|
30
|
+
/** A further page exists past the rows the listener holds. */
|
|
31
|
+
hasMore?: boolean;
|
|
32
|
+
/** Widens the listener by one step. */
|
|
33
|
+
showMore?: () => void;
|
|
34
|
+
/** The listener is reading; the page asked for is held rather than pulled back. */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Newest-first pages of a history, over rows a listener window holds.
|
|
39
|
+
*
|
|
40
|
+
* The window is the query's bound — a hundred activities on a record, a
|
|
41
|
+
* page's worth on the feed — and a page the reader turns to past what it
|
|
42
|
+
* holds widens it, so the footer's Next is live while the probe row says
|
|
43
|
+
* more exists and the count stays "of more than" until it does not. Paged
|
|
44
|
+
* in memory otherwise: the contact timeline merges three sources that share
|
|
45
|
+
* no cursor, so only a slice of the merged stream can be turned.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useTimelinePages(total: number, reader?: TimelineWindow): {
|
|
48
|
+
page: number;
|
|
49
|
+
pageSize: number;
|
|
50
|
+
slice: <T>(rows: readonly T[]) => T[];
|
|
51
|
+
footer: ListPaginationProps;
|
|
52
|
+
};
|
|
53
|
+
export interface TimelineEntryProps {
|
|
54
|
+
/** The glyph for the entry's kind. */
|
|
55
|
+
icon: ReactNode;
|
|
56
|
+
/** The chips that say what the entry is: its kind, its direction, its delivery state. */
|
|
57
|
+
chips?: ReactNode;
|
|
58
|
+
/** The one line a collapsed entry reads as: a subject, a summary, a note's first line. */
|
|
59
|
+
title?: ReactNode;
|
|
60
|
+
/** A subject reads as one; a note's own words as body text. */
|
|
61
|
+
titleVariant?: 'subtitle2' | 'body2';
|
|
62
|
+
/** The line under it: who, to or from whom, when. */
|
|
63
|
+
meta?: ReactNode;
|
|
64
|
+
/** What the entry opens to. Absent, there is nothing more to show and no chevron. */
|
|
65
|
+
body?: ReactNode;
|
|
66
|
+
/** Controls on this one entry — edit, delete. */
|
|
67
|
+
actions?: ReactNode;
|
|
68
|
+
expanded?: boolean;
|
|
69
|
+
onToggle?: () => void;
|
|
70
|
+
/** Names the chevron, "Expand {label}", so a screen reader knows which entry. */
|
|
71
|
+
label?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One entry of a history: collapsed to its heading and caption, opened in
|
|
75
|
+
* place to its body.
|
|
76
|
+
*
|
|
77
|
+
* An entry with no body has no chevron — a spacer keeps its glyph in line
|
|
78
|
+
* with the rows that have one — and its heading wraps rather than
|
|
79
|
+
* truncating, since there is nowhere else to read the rest of it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function TimelineEntry(props: TimelineEntryProps): import("react").JSX.Element;
|
|
82
|
+
export declare namespace TimelineEntry {
|
|
83
|
+
var displayName: string;
|
|
84
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { mdiChevronDown, mdiChevronRight, mdiUnfoldLessHorizontal, mdiUnfoldMoreHorizontal } from "@aglyn/shared-data-mdi";
|
|
19
|
+
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
20
|
+
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
21
|
+
import { Box, Button, Collapse, IconButton, Stack, Typography } from "@mui/material";
|
|
22
|
+
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
23
|
+
/**
|
|
24
|
+
* The open/closed state of a history's entries, held by the CARD so that
|
|
25
|
+
* the header's "Expand all" and each row's chevron act on one state.
|
|
26
|
+
*
|
|
27
|
+
* Kept as a default plus the keys that differ from it, so "Expand all"
|
|
28
|
+
* reaches the entries of pages the reader has not turned to yet, and a row
|
|
29
|
+
* closed after it stays closed.
|
|
30
|
+
*/ export function useTimelineExpansion() {
|
|
31
|
+
const [state, setState] = useState(()=>({
|
|
32
|
+
all: false,
|
|
33
|
+
flipped: new Set()
|
|
34
|
+
}));
|
|
35
|
+
const isExpanded = useCallback((key)=>state.all !== state.flipped.has(key), [
|
|
36
|
+
state
|
|
37
|
+
]);
|
|
38
|
+
const toggle = useCallback((key)=>{
|
|
39
|
+
setState((current)=>{
|
|
40
|
+
const flipped = new Set(current.flipped);
|
|
41
|
+
if (flipped.has(key)) flipped.delete(key);
|
|
42
|
+
else flipped.add(key);
|
|
43
|
+
return {
|
|
44
|
+
all: current.all,
|
|
45
|
+
flipped
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}, []);
|
|
49
|
+
const setAllExpanded = useCallback((all)=>{
|
|
50
|
+
setState({
|
|
51
|
+
all,
|
|
52
|
+
flipped: new Set()
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
return useMemo(()=>({
|
|
56
|
+
isExpanded,
|
|
57
|
+
toggle,
|
|
58
|
+
allExpanded: state.all && state.flipped.size === 0,
|
|
59
|
+
setAllExpanded
|
|
60
|
+
}), [
|
|
61
|
+
isExpanded,
|
|
62
|
+
toggle,
|
|
63
|
+
state,
|
|
64
|
+
setAllExpanded
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
/** The card header's "Expand all" / "Collapse all". */ export function TimelineExpandAllButton(props) {
|
|
68
|
+
const { expansion, disabled } = props;
|
|
69
|
+
const open = expansion.allExpanded;
|
|
70
|
+
return /*#__PURE__*/ _jsx(Button, {
|
|
71
|
+
size: "small",
|
|
72
|
+
disabled: disabled,
|
|
73
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
74
|
+
path: (open ? mdiUnfoldLessHorizontal : mdiUnfoldMoreHorizontal).path,
|
|
75
|
+
size: 0.8
|
|
76
|
+
}),
|
|
77
|
+
onClick: ()=>expansion.setAllExpanded(!open),
|
|
78
|
+
children: open ? 'Collapse all' : 'Expand all'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
TimelineExpandAllButton.displayName = 'TimelineExpandAllButton';
|
|
82
|
+
/**
|
|
83
|
+
* Newest-first pages of a history, over rows a listener window holds.
|
|
84
|
+
*
|
|
85
|
+
* The window is the query's bound — a hundred activities on a record, a
|
|
86
|
+
* page's worth on the feed — and a page the reader turns to past what it
|
|
87
|
+
* holds widens it, so the footer's Next is live while the probe row says
|
|
88
|
+
* more exists and the count stays "of more than" until it does not. Paged
|
|
89
|
+
* in memory otherwise: the contact timeline merges three sources that share
|
|
90
|
+
* no cursor, so only a slice of the merged stream can be turned.
|
|
91
|
+
*/ export function useTimelinePages(total, reader = {}) {
|
|
92
|
+
const { hasMore = false, showMore, loading = false } = reader;
|
|
93
|
+
const [page, setPage] = useState(0);
|
|
94
|
+
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
95
|
+
const lastPage = Math.max(0, Math.ceil(total / pageSize) - 1);
|
|
96
|
+
// A page left empty by a delete shows the last one with rows; while the
|
|
97
|
+
// window is reading, or holds less than exists, the page asked for stands.
|
|
98
|
+
const shown = hasMore || loading ? page : Math.min(page, lastPage);
|
|
99
|
+
const start = shown * pageSize;
|
|
100
|
+
const short = start + pageSize > total;
|
|
101
|
+
useEffect(()=>{
|
|
102
|
+
if (short && hasMore && !loading && showMore) showMore();
|
|
103
|
+
}, [
|
|
104
|
+
short,
|
|
105
|
+
hasMore,
|
|
106
|
+
loading,
|
|
107
|
+
showMore
|
|
108
|
+
]);
|
|
109
|
+
const slice = useCallback((rows)=>rows.slice(start, start + pageSize), [
|
|
110
|
+
start,
|
|
111
|
+
pageSize
|
|
112
|
+
]);
|
|
113
|
+
const footer = {
|
|
114
|
+
page: shown,
|
|
115
|
+
pageSize,
|
|
116
|
+
rowCount: Math.max(0, Math.min(pageSize, total - start)),
|
|
117
|
+
count: hasMore ? undefined : total,
|
|
118
|
+
hasMore,
|
|
119
|
+
onPageChange: setPage,
|
|
120
|
+
onPageSizeChange: setPageSize
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
page: shown,
|
|
124
|
+
pageSize,
|
|
125
|
+
slice,
|
|
126
|
+
footer
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One entry of a history: collapsed to its heading and caption, opened in
|
|
131
|
+
* place to its body.
|
|
132
|
+
*
|
|
133
|
+
* An entry with no body has no chevron — a spacer keeps its glyph in line
|
|
134
|
+
* with the rows that have one — and its heading wraps rather than
|
|
135
|
+
* truncating, since there is nowhere else to read the rest of it.
|
|
136
|
+
*/ export function TimelineEntry(props) {
|
|
137
|
+
const { icon, chips, title, titleVariant = 'body2', meta, body, actions, expanded = false, onToggle, label } = props;
|
|
138
|
+
const bodyId = useId();
|
|
139
|
+
const expandable = Boolean(body) && Boolean(onToggle);
|
|
140
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
141
|
+
direction: "row",
|
|
142
|
+
spacing: 1,
|
|
143
|
+
sx: {
|
|
144
|
+
alignItems: 'flex-start'
|
|
145
|
+
},
|
|
146
|
+
"data-testid": "timeline-entry",
|
|
147
|
+
"data-expanded": expandable ? String(expanded) : undefined,
|
|
148
|
+
children: [
|
|
149
|
+
expandable ? /*#__PURE__*/ _jsx(IconButton, {
|
|
150
|
+
size: "small",
|
|
151
|
+
"aria-expanded": expanded,
|
|
152
|
+
"aria-controls": bodyId,
|
|
153
|
+
"aria-label": `${expanded ? 'Collapse' : 'Expand'} ${label != null ? label : 'entry'}`,
|
|
154
|
+
onClick: onToggle,
|
|
155
|
+
sx: {
|
|
156
|
+
mt: -0.25
|
|
157
|
+
},
|
|
158
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
159
|
+
path: (expanded ? mdiChevronDown : mdiChevronRight).path,
|
|
160
|
+
size: 0.8
|
|
161
|
+
})
|
|
162
|
+
}) : /*#__PURE__*/ _jsx(Box, {
|
|
163
|
+
"aria-hidden": true,
|
|
164
|
+
sx: {
|
|
165
|
+
width: 30,
|
|
166
|
+
flexShrink: 0
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
170
|
+
sx: {
|
|
171
|
+
color: 'text.secondary',
|
|
172
|
+
pt: 0.25,
|
|
173
|
+
fontSize: (theme)=>theme.typography.h6.fontSize
|
|
174
|
+
},
|
|
175
|
+
children: icon
|
|
176
|
+
}),
|
|
177
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
178
|
+
spacing: 0.5,
|
|
179
|
+
sx: {
|
|
180
|
+
flex: 1,
|
|
181
|
+
minWidth: 0
|
|
182
|
+
},
|
|
183
|
+
children: [
|
|
184
|
+
chips ? /*#__PURE__*/ _jsx(Stack, {
|
|
185
|
+
direction: "row",
|
|
186
|
+
spacing: 1,
|
|
187
|
+
sx: {
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
flexWrap: 'wrap',
|
|
190
|
+
rowGap: 0.5
|
|
191
|
+
},
|
|
192
|
+
children: chips
|
|
193
|
+
}) : null,
|
|
194
|
+
title ? /*#__PURE__*/ _jsx(Typography, {
|
|
195
|
+
variant: titleVariant,
|
|
196
|
+
component: "div",
|
|
197
|
+
noWrap: expandable && !expanded,
|
|
198
|
+
sx: {
|
|
199
|
+
overflowWrap: 'anywhere'
|
|
200
|
+
},
|
|
201
|
+
children: title
|
|
202
|
+
}) : null,
|
|
203
|
+
meta,
|
|
204
|
+
expandable ? /*#__PURE__*/ _jsx(Collapse, {
|
|
205
|
+
in: expanded,
|
|
206
|
+
unmountOnExit: true,
|
|
207
|
+
children: /*#__PURE__*/ _jsx(Box, {
|
|
208
|
+
id: bodyId,
|
|
209
|
+
children: body
|
|
210
|
+
})
|
|
211
|
+
}) : body
|
|
212
|
+
]
|
|
213
|
+
}),
|
|
214
|
+
actions ? /*#__PURE__*/ _jsx(Stack, {
|
|
215
|
+
direction: "row",
|
|
216
|
+
spacing: 0.5,
|
|
217
|
+
children: actions
|
|
218
|
+
}) : null
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
TimelineEntry.displayName = 'TimelineEntry';
|
|
223
|
+
|
|
224
|
+
//# sourceMappingURL=activity-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/activity-timeline.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 {\n mdiChevronDown,\n mdiChevronRight,\n mdiUnfoldLessHorizontal,\n mdiUnfoldMoreHorizontal,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { ListPaginationProps } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { Box, Button, Collapse, IconButton, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useEffect, useId, useMemo, useState } from 'react'\n\n/*\n * THE ROW GRAMMAR OF EVERY CRM HISTORY: the Timeline on a contact, the\n * Activity card on a lead, a company and a deal, and the Recent activity\n * feed under the contacts list.\n *\n * Salesforce's activity timeline is the model. Each entry is a collapsed\n * row — a chevron, the kind, a one-line heading and the who/whom/when under\n * it — that opens in place to what it holds: an email's body, a note's whole\n * text. The card header carries \"Expand all\", the list is paged by the\n * console's own footer, and nothing a row showed before is dropped: what\n * does not fit on the collapsed row is in the opened one.\n */\n\n/** Which entries of one history are open. */\nexport interface TimelineExpansion {\n isExpanded: (key: string) => boolean\n toggle: (key: string) => void\n /** Every entry is open and none has been closed since. */\n allExpanded: boolean\n setAllExpanded: (expanded: boolean) => void\n}\n\n/**\n * The open/closed state of a history's entries, held by the CARD so that\n * the header's \"Expand all\" and each row's chevron act on one state.\n *\n * Kept as a default plus the keys that differ from it, so \"Expand all\"\n * reaches the entries of pages the reader has not turned to yet, and a row\n * closed after it stays closed.\n */\nexport function useTimelineExpansion(): TimelineExpansion {\n const [state, setState] = useState<{ all: boolean; flipped: ReadonlySet<string> }>(\n () => ({ all: false, flipped: new Set<string>() }),\n )\n const isExpanded = useCallback(\n (key: string) => state.all !== state.flipped.has(key),\n [state],\n )\n const toggle = useCallback((key: string) => {\n setState((current) => {\n const flipped = new Set(current.flipped)\n if (flipped.has(key)) flipped.delete(key)\n else flipped.add(key)\n return { all: current.all, flipped }\n })\n }, [])\n const setAllExpanded = useCallback((all: boolean) => {\n setState({ all, flipped: new Set<string>() })\n }, [])\n return useMemo(\n () => ({\n isExpanded,\n toggle,\n allExpanded: state.all && state.flipped.size === 0,\n setAllExpanded,\n }),\n [isExpanded, toggle, state, setAllExpanded],\n )\n}\n\n/** The card header's \"Expand all\" / \"Collapse all\". */\nexport function TimelineExpandAllButton(props: {\n expansion: TimelineExpansion\n disabled?: boolean\n}) {\n const { expansion, disabled } = props\n const open = expansion.allExpanded\n return (\n <Button\n size=\"small\"\n disabled={disabled}\n startIcon={\n <MdiIcon\n path={(open ? mdiUnfoldLessHorizontal : mdiUnfoldMoreHorizontal).path}\n size={0.8}\n />\n }\n onClick={() => expansion.setAllExpanded(!open)}\n >\n {open ? 'Collapse all' : 'Expand all'}\n </Button>\n )\n}\nTimelineExpandAllButton.displayName = 'TimelineExpandAllButton'\n\n/** The listener window a history pages over. */\nexport interface TimelineWindow {\n /** A further page exists past the rows the listener holds. */\n hasMore?: boolean\n /** Widens the listener by one step. */\n showMore?: () => void\n /** The listener is reading; the page asked for is held rather than pulled back. */\n loading?: boolean\n}\n\n/**\n * Newest-first pages of a history, over rows a listener window holds.\n *\n * The window is the query's bound — a hundred activities on a record, a\n * page's worth on the feed — and a page the reader turns to past what it\n * holds widens it, so the footer's Next is live while the probe row says\n * more exists and the count stays \"of more than\" until it does not. Paged\n * in memory otherwise: the contact timeline merges three sources that share\n * no cursor, so only a slice of the merged stream can be turned.\n */\nexport function useTimelinePages(total: number, reader: TimelineWindow = {}) {\n const { hasMore = false, showMore, loading = false } = reader\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const lastPage = Math.max(0, Math.ceil(total / pageSize) - 1)\n // A page left empty by a delete shows the last one with rows; while the\n // window is reading, or holds less than exists, the page asked for stands.\n const shown = hasMore || loading ? page : Math.min(page, lastPage)\n const start = shown * pageSize\n const short = start + pageSize > total\n useEffect(() => {\n if (short && hasMore && !loading && showMore) showMore()\n }, [short, hasMore, loading, showMore])\n const slice = useCallback(\n <T,>(rows: readonly T[]): T[] => rows.slice(start, start + pageSize),\n [start, pageSize],\n )\n const footer: ListPaginationProps = {\n page: shown,\n pageSize,\n rowCount: Math.max(0, Math.min(pageSize, total - start)),\n count: hasMore ? undefined : total,\n hasMore,\n onPageChange: setPage,\n onPageSizeChange: setPageSize,\n }\n return { page: shown, pageSize, slice, footer }\n}\n\nexport interface TimelineEntryProps {\n /** The glyph for the entry's kind. */\n icon: ReactNode\n /** The chips that say what the entry is: its kind, its direction, its delivery state. */\n chips?: ReactNode\n /** The one line a collapsed entry reads as: a subject, a summary, a note's first line. */\n title?: ReactNode\n /** A subject reads as one; a note's own words as body text. */\n titleVariant?: 'subtitle2' | 'body2'\n /** The line under it: who, to or from whom, when. */\n meta?: ReactNode\n /** What the entry opens to. Absent, there is nothing more to show and no chevron. */\n body?: ReactNode\n /** Controls on this one entry — edit, delete. */\n actions?: ReactNode\n expanded?: boolean\n onToggle?: () => void\n /** Names the chevron, \"Expand {label}\", so a screen reader knows which entry. */\n label?: string\n}\n\n/**\n * One entry of a history: collapsed to its heading and caption, opened in\n * place to its body.\n *\n * An entry with no body has no chevron — a spacer keeps its glyph in line\n * with the rows that have one — and its heading wraps rather than\n * truncating, since there is nowhere else to read the rest of it.\n */\nexport function TimelineEntry(props: TimelineEntryProps) {\n const {\n icon,\n chips,\n title,\n titleVariant = 'body2',\n meta,\n body,\n actions,\n expanded = false,\n onToggle,\n label,\n } = props\n const bodyId = useId()\n const expandable = Boolean(body) && Boolean(onToggle)\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n data-testid=\"timeline-entry\"\n data-expanded={expandable ? String(expanded) : undefined}\n >\n {expandable ? (\n <IconButton\n size=\"small\"\n aria-expanded={expanded}\n aria-controls={bodyId}\n aria-label={`${expanded ? 'Collapse' : 'Expand'} ${label ?? 'entry'}`}\n onClick={onToggle}\n sx={{ mt: -0.25 }}\n >\n <MdiIcon path={(expanded ? mdiChevronDown : mdiChevronRight).path} size={0.8} />\n </IconButton>\n ) : (\n <Box aria-hidden sx={{ width: 30, flexShrink: 0 }} />\n )}\n <Stack\n sx={{\n color: 'text.secondary',\n pt: 0.25,\n fontSize: (theme) => theme.typography.h6.fontSize,\n }}\n >\n {icon}\n </Stack>\n <Stack spacing={0.5} sx={{ flex: 1, minWidth: 0 }}>\n {chips ? (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {chips}\n </Stack>\n ) : null}\n {title ? (\n <Typography\n variant={titleVariant}\n component=\"div\"\n noWrap={expandable && !expanded}\n sx={{ overflowWrap: 'anywhere' }}\n >\n {title}\n </Typography>\n ) : null}\n {meta}\n {expandable ? (\n <Collapse in={expanded} unmountOnExit>\n <Box id={bodyId}>{body}</Box>\n </Collapse>\n ) : (\n body\n )}\n </Stack>\n {actions ? (\n <Stack direction=\"row\" spacing={0.5}>\n {actions}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nTimelineEntry.displayName = 'TimelineEntry'\n"],"names":["mdiChevronDown","mdiChevronRight","mdiUnfoldLessHorizontal","mdiUnfoldMoreHorizontal","MdiIcon","TABLE_PAGE_SIZE_DEFAULT","Box","Button","Collapse","IconButton","Stack","Typography","useCallback","useEffect","useId","useMemo","useState","useTimelineExpansion","state","setState","all","flipped","Set","isExpanded","key","has","toggle","current","delete","add","setAllExpanded","allExpanded","size","TimelineExpandAllButton","props","expansion","disabled","open","startIcon","path","onClick","displayName","useTimelinePages","total","reader","hasMore","showMore","loading","page","setPage","pageSize","setPageSize","lastPage","Math","max","ceil","shown","min","start","short","slice","rows","footer","rowCount","count","undefined","onPageChange","onPageSizeChange","TimelineEntry","icon","chips","title","titleVariant","meta","body","actions","expanded","onToggle","label","bodyId","expandable","Boolean","direction","spacing","sx","alignItems","data-testid","data-expanded","String","aria-expanded","aria-controls","aria-label","mt","aria-hidden","width","flexShrink","color","pt","fontSize","theme","typography","h6","flex","minWidth","flexWrap","rowGap","variant","component","noWrap","overflowWrap","in","unmountOnExit","id"],"mappings":"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"}
|
|
@@ -166,21 +166,50 @@ import { useCrmApi } from "./use-crm-api.js";
|
|
|
166
166
|
{
|
|
167
167
|
label: 'Notes',
|
|
168
168
|
value: company.notes
|
|
169
|
-
}
|
|
170
|
-
...fields.active.map((definition)=>{
|
|
171
|
-
var _company_custom;
|
|
172
|
-
const text = formatContactCustomValue(definition, (_company_custom = company.custom) == null ? void 0 : _company_custom[definition.key]);
|
|
173
|
-
return {
|
|
174
|
-
label: definition.label || definition.key,
|
|
175
|
-
value: text && definition.type === 'url' ? /*#__PURE__*/ _jsx(Link, {
|
|
176
|
-
href: text,
|
|
177
|
-
target: "_blank",
|
|
178
|
-
rel: "noreferrer noopener",
|
|
179
|
-
children: text
|
|
180
|
-
}) : text
|
|
181
|
-
};
|
|
182
|
-
})
|
|
169
|
+
}
|
|
183
170
|
];
|
|
171
|
+
// The org's own company fields (AGL-2661), after the built-in ones under
|
|
172
|
+
// "More fields", as a record's Details keep them; none defined, none drawn.
|
|
173
|
+
const moreRows = fields.active.map((definition)=>{
|
|
174
|
+
var _company_custom;
|
|
175
|
+
const text = formatContactCustomValue(definition, (_company_custom = company.custom) == null ? void 0 : _company_custom[definition.key]);
|
|
176
|
+
return {
|
|
177
|
+
label: definition.label || definition.key,
|
|
178
|
+
value: text && definition.type === 'url' ? /*#__PURE__*/ _jsx(Link, {
|
|
179
|
+
href: text,
|
|
180
|
+
target: "_blank",
|
|
181
|
+
rel: "noreferrer noopener",
|
|
182
|
+
children: text
|
|
183
|
+
}) : text
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const factRows = (list)=>list.map((row)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
187
|
+
direction: "row",
|
|
188
|
+
spacing: 2,
|
|
189
|
+
sx: {
|
|
190
|
+
alignItems: 'baseline'
|
|
191
|
+
},
|
|
192
|
+
children: [
|
|
193
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
194
|
+
variant: "caption",
|
|
195
|
+
color: "text.secondary",
|
|
196
|
+
sx: {
|
|
197
|
+
minWidth: 96,
|
|
198
|
+
flexShrink: 0
|
|
199
|
+
},
|
|
200
|
+
children: row.label
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
203
|
+
variant: "body2",
|
|
204
|
+
color: row.value ? 'text.primary' : 'text.secondary',
|
|
205
|
+
sx: {
|
|
206
|
+
whiteSpace: 'pre-wrap',
|
|
207
|
+
overflowWrap: 'anywhere'
|
|
208
|
+
},
|
|
209
|
+
children: row.value || '—'
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
}, row.label));
|
|
184
213
|
return /*#__PURE__*/ _jsxs(CrmRecordHeader, {
|
|
185
214
|
kind: "Company",
|
|
186
215
|
title: String(company.name || company.$id),
|
|
@@ -240,35 +269,23 @@ import { useCrmApi } from "./use-crm-api.js";
|
|
|
240
269
|
]
|
|
241
270
|
}),
|
|
242
271
|
children: [
|
|
243
|
-
/*#__PURE__*/
|
|
272
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
244
273
|
spacing: 1,
|
|
245
|
-
children:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
sx: {
|
|
249
|
-
alignItems: 'baseline'
|
|
250
|
-
},
|
|
274
|
+
children: [
|
|
275
|
+
factRows(rows),
|
|
276
|
+
moreRows.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
251
277
|
children: [
|
|
252
278
|
/*#__PURE__*/ _jsx(Typography, {
|
|
253
|
-
variant: "
|
|
254
|
-
color: "text.secondary",
|
|
279
|
+
variant: "subtitle2",
|
|
255
280
|
sx: {
|
|
256
|
-
|
|
257
|
-
flexShrink: 0
|
|
281
|
+
pt: 1
|
|
258
282
|
},
|
|
259
|
-
children:
|
|
283
|
+
children: 'More fields'
|
|
260
284
|
}),
|
|
261
|
-
|
|
262
|
-
variant: "body2",
|
|
263
|
-
color: row.value ? 'text.primary' : 'text.secondary',
|
|
264
|
-
sx: {
|
|
265
|
-
whiteSpace: 'pre-wrap',
|
|
266
|
-
overflowWrap: 'anywhere'
|
|
267
|
-
},
|
|
268
|
-
children: row.value || '—'
|
|
269
|
-
})
|
|
285
|
+
factRows(moreRows)
|
|
270
286
|
]
|
|
271
|
-
}
|
|
287
|
+
}) : null
|
|
288
|
+
]
|
|
272
289
|
}),
|
|
273
290
|
editing ? /*#__PURE__*/ _jsx(CompanyEditDrawer, {
|
|
274
291
|
open: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-properties-card.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 {\n type AglynOrgBilling,\n type AglynPostalAddress,\n type CrmCompany,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Link, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport type { CrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { COMPANY_DETACH_LIMIT } from '../model/companies'\nimport {\n companyDeleteFailureMessage,\n deleteCompanyThroughRoute,\n} from '../model/company-delete'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { useCrmApi } from './use-crm-api'\n\nexport interface CompanyPropertiesCardProps {\n company: Partial<CrmCompany> & { $id: string }\n /** Whether the document the card holds is server-confirmed. */\n seed: { fromCache: boolean; unreadable: boolean }\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n crmScope: CrmScope\n members: OrgMemberOptions\n routes: CrmRoutes\n /** Called once the record is gone, so the page can leave it. */\n onDeleted: () => void\n}\n\n/** A postal address on one line, or nothing. */\nfunction formatAddress(address: AglynPostalAddress | null | undefined): string {\n if (!address) return ''\n return [\n address.line1,\n address.line2,\n [address.city, address.state].filter(Boolean).join(', '),\n address.postalCode,\n address.country,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * WHAT A COMPANY IS: its fields, read off the one document the page holds,\n * with the edit and the delete beside them (AGL-2597).\n *\n * ## Edit is the create drawer\n *\n * The same form the list creates with, opened on the record. One form means\n * one set of refusals — a domain that is not a domain is refused the same\n * way on day one and on a rename — and one place to add a field.\n *\n * ## Delete is a DETACH first\n *\n * `crm/company-delete` — the route the bulk bar calls per row as well —\n * unlinks the contacts that name this company before removing it, bounded\n * at {@link COMPANY_DETACH_LIMIT} per pass and honest past it: the company\n * stands, and the person is told more remain. The unlink clears each\n * holder's facet that named the company, which is the server's to write\n * (AGL-2804). What this card adds is the confirm, the sentence for each\n * outcome, and leaving the page once the record is gone.\n */\nexport function CompanyPropertiesCard(props: CompanyPropertiesCardProps) {\n const {\n company,\n seed,\n hostId,\n org,\n crmScope,\n members,\n routes,\n onDeleted,\n } = props\n const { scope } = crmScope\n const callCrm = useCrmApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the company's own site would face\n // a gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's company fields (AGL-2661): one row each under the fixed\n // ones, read by type the way the list's columns read them.\n const fields = useContactFieldDefinitions(crmScope.orgId, 'company')\n\n const handleDelete = useCallback(async () => {\n if (!scope || deleting) return\n const name = String(company.name ?? company.$id)\n const accepted = await confirm({\n title: 'Delete this company?',\n description:\n `\"${name}\" is removed from Companies and unlinked from every ` +\n 'contact at it. The contacts themselves are kept, and so is ' +\n 'anything else filed against the company.',\n confirmationText: 'Delete company',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would make this always return.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setDeleting(true)\n try {\n const outcome = await deleteCompanyThroughRoute(callCrm, company.$id)\n if (!outcome.deleted) {\n enqueueSnackbar(\n `${COMPANY_DETACH_LIMIT.toLocaleString()} contacts were unlinked ` +\n `from \"${name}\" and more remain. Delete again to continue.`,\n { variant: 'warning' },\n )\n return\n }\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed.\n logActivity('Deleted company', { type: 'company', id: company.$id, name })\n enqueueSnackbar(\n outcome.detached\n ? `Company deleted and unlinked from ${outcome.detached.toLocaleString()} ` +\n `contact${outcome.detached === 1 ? '' : 's'}`\n : 'Company deleted',\n { variant: 'success', persist: false },\n )\n onDeleted()\n } catch (error) {\n console.error(error)\n enqueueSnackbar(companyDeleteFailureMessage(error), {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [scope, deleting, company, confirm, callCrm, logActivity, enqueueSnackbar, onDeleted])\n\n const address = formatAddress(company.address)\n // The domain, the industry and the owner read on the header — the\n // subtitle and the chip row — so the rows list what is left.\n const rows: Array<{ label: string; value: ReactNode }> = [\n {\n label: 'Website',\n value: company.website ? (\n <Link href={company.website} target=\"_blank\" rel=\"noreferrer noopener\">\n {company.website}\n </Link>\n ) : null,\n },\n // Callable in one tap (AGL-2661); plain text for a value no dialer takes.\n { label: 'Phone', value: <CrmPhoneLink phone={company.phone} /> },\n { label: 'Address', value: address },\n { label: 'Tags', value: (company.tags ?? []).join(', ') },\n { label: 'Notes', value: company.notes },\n ...fields.active.map((definition) => {\n const text = formatContactCustomValue(definition, company.custom?.[definition.key])\n return {\n label: definition.label || definition.key,\n value:\n text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text\n ),\n }\n }),\n ]\n\n return (\n <CrmRecordHeader\n kind=\"Company\"\n title={String(company.name || company.$id)}\n subtitle={company.domain}\n help={pluginDocsHelp('companies', { anchor: '#a-companys-page' })}\n backHref={routes.section('companies')}\n backLabel=\"Back to companies\"\n actions={\n <>\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ companyId: company.$id }}\n phone={company.phone}\n />\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n </>\n }\n menuItems={[\n {\n key: 'delete',\n label: deleting ? 'Deleting…' : 'Delete company',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !scope || deleting,\n disabledReason: deleting ? 'The company is being deleted' : 'The organization has not loaded',\n onClick: () => void handleDelete(),\n },\n ]}\n chips={\n <>\n <CrmRecordChip label=\"Industry\" value={company.industry} />\n <CrmRecordChip\n label=\"Owner\"\n value={\n company.ownerUid\n ? members.ready\n ? members.labelFor(company.ownerUid)\n : '…'\n : undefined\n }\n />\n </>\n }\n >\n <Stack spacing={1}>\n {rows.map((row) => (\n <Stack\n key={row.label}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline' }}\n >\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ minWidth: 96, flexShrink: 0 }}\n >\n {row.label}\n </Typography>\n <Typography\n variant=\"body2\"\n color={row.value ? 'text.primary' : 'text.secondary'}\n sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}\n >\n {row.value || '—'}\n </Typography>\n </Stack>\n ))}\n </Stack>\n {editing ? (\n <CompanyEditDrawer\n open\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n company={company}\n seed={seed}\n members={members}\n onSaved={() => setEditing(false)}\n />\n ) : null}\n </CrmRecordHeader>\n )\n}\nCompanyPropertiesCard.displayName = 'CompanyPropertiesCard'\n\nexport default CompanyPropertiesCard\n"],"names":["pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","Button","Link","Stack","Typography","useCallback","useState","useContactFieldDefinitions","useCrmActivityLogger","COMPANY_DETACH_LIMIT","companyDeleteFailureMessage","deleteCompanyThroughRoute","CompanyEditDrawer","formatContactCustomValue","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","useCrmApi","formatAddress","address","line1","line2","city","state","filter","Boolean","join","postalCode","country","map","part","String","trim","CompanyPropertiesCard","props","company","seed","hostId","org","crmScope","members","routes","onDeleted","scope","callCrm","confirm","enqueueSnackbar","logActivity","editing","setEditing","deleting","setDeleting","fields","orgId","handleDelete","name","$id","accepted","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","deleted","toLocaleString","variant","type","id","detached","persist","error","console","allowDuplicate","rows","label","value","website","href","target","rel","phone","tags","notes","active","definition","text","custom","key","kind","subtitle","domain","help","anchor","backHref","section","backLabel","actions","link","companyId","size","startIcon","path","onClick","menuItems","icon","destructive","disabled","disabledReason","chips","industry","ownerUid","ready","labelFor","undefined","spacing","row","direction","sx","alignItems","minWidth","flexShrink","whiteSpace","overflowWrap","open","onClose","onSaved","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-properties-card.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 {\n type AglynOrgBilling,\n type AglynPostalAddress,\n type CrmCompany,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Link, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport type { CrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { COMPANY_DETACH_LIMIT } from '../model/companies'\nimport {\n companyDeleteFailureMessage,\n deleteCompanyThroughRoute,\n} from '../model/company-delete'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { useCrmApi } from './use-crm-api'\n\nexport interface CompanyPropertiesCardProps {\n company: Partial<CrmCompany> & { $id: string }\n /** Whether the document the card holds is server-confirmed. */\n seed: { fromCache: boolean; unreadable: boolean }\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n crmScope: CrmScope\n members: OrgMemberOptions\n routes: CrmRoutes\n /** Called once the record is gone, so the page can leave it. */\n onDeleted: () => void\n}\n\n/** A postal address on one line, or nothing. */\nfunction formatAddress(address: AglynPostalAddress | null | undefined): string {\n if (!address) return ''\n return [\n address.line1,\n address.line2,\n [address.city, address.state].filter(Boolean).join(', '),\n address.postalCode,\n address.country,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * WHAT A COMPANY IS: its fields, read off the one document the page holds,\n * with the edit and the delete beside them (AGL-2597).\n *\n * ## Edit is the create drawer\n *\n * The same form the list creates with, opened on the record. One form means\n * one set of refusals — a domain that is not a domain is refused the same\n * way on day one and on a rename — and one place to add a field.\n *\n * ## Delete is a DETACH first\n *\n * `crm/company-delete` — the route the bulk bar calls per row as well —\n * unlinks the contacts that name this company before removing it, bounded\n * at {@link COMPANY_DETACH_LIMIT} per pass and honest past it: the company\n * stands, and the person is told more remain. The unlink clears each\n * holder's facet that named the company, which is the server's to write\n * (AGL-2804). What this card adds is the confirm, the sentence for each\n * outcome, and leaving the page once the record is gone.\n */\nexport function CompanyPropertiesCard(props: CompanyPropertiesCardProps) {\n const {\n company,\n seed,\n hostId,\n org,\n crmScope,\n members,\n routes,\n onDeleted,\n } = props\n const { scope } = crmScope\n const callCrm = useCrmApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the company's own site would face\n // a gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's company fields (AGL-2661): one row each under the fixed\n // ones, read by type the way the list's columns read them.\n const fields = useContactFieldDefinitions(crmScope.orgId, 'company')\n\n const handleDelete = useCallback(async () => {\n if (!scope || deleting) return\n const name = String(company.name ?? company.$id)\n const accepted = await confirm({\n title: 'Delete this company?',\n description:\n `\"${name}\" is removed from Companies and unlinked from every ` +\n 'contact at it. The contacts themselves are kept, and so is ' +\n 'anything else filed against the company.',\n confirmationText: 'Delete company',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would make this always return.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setDeleting(true)\n try {\n const outcome = await deleteCompanyThroughRoute(callCrm, company.$id)\n if (!outcome.deleted) {\n enqueueSnackbar(\n `${COMPANY_DETACH_LIMIT.toLocaleString()} contacts were unlinked ` +\n `from \"${name}\" and more remain. Delete again to continue.`,\n { variant: 'warning' },\n )\n return\n }\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed.\n logActivity('Deleted company', { type: 'company', id: company.$id, name })\n enqueueSnackbar(\n outcome.detached\n ? `Company deleted and unlinked from ${outcome.detached.toLocaleString()} ` +\n `contact${outcome.detached === 1 ? '' : 's'}`\n : 'Company deleted',\n { variant: 'success', persist: false },\n )\n onDeleted()\n } catch (error) {\n console.error(error)\n enqueueSnackbar(companyDeleteFailureMessage(error), {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [scope, deleting, company, confirm, callCrm, logActivity, enqueueSnackbar, onDeleted])\n\n const address = formatAddress(company.address)\n // The domain, the industry and the owner read on the header — the\n // subtitle and the chip row — so the rows list what is left.\n const rows: Array<{ label: string; value: ReactNode }> = [\n {\n label: 'Website',\n value: company.website ? (\n <Link href={company.website} target=\"_blank\" rel=\"noreferrer noopener\">\n {company.website}\n </Link>\n ) : null,\n },\n // Callable in one tap (AGL-2661); plain text for a value no dialer takes.\n { label: 'Phone', value: <CrmPhoneLink phone={company.phone} /> },\n { label: 'Address', value: address },\n { label: 'Tags', value: (company.tags ?? []).join(', ') },\n { label: 'Notes', value: company.notes },\n ]\n // The org's own company fields (AGL-2661), after the built-in ones under\n // \"More fields\", as a record's Details keep them; none defined, none drawn.\n const moreRows: Array<{ label: string; value: ReactNode }> = fields.active.map(\n (definition) => {\n const text = formatContactCustomValue(definition, company.custom?.[definition.key])\n return {\n label: definition.label || definition.key,\n value:\n text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text\n ),\n }\n },\n )\n const factRows = (list: Array<{ label: string; value: ReactNode }>) =>\n list.map((row) => (\n <Stack\n key={row.label}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline' }}\n >\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ minWidth: 96, flexShrink: 0 }}\n >\n {row.label}\n </Typography>\n <Typography\n variant=\"body2\"\n color={row.value ? 'text.primary' : 'text.secondary'}\n sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}\n >\n {row.value || '—'}\n </Typography>\n </Stack>\n ))\n\n return (\n <CrmRecordHeader\n kind=\"Company\"\n title={String(company.name || company.$id)}\n subtitle={company.domain}\n help={pluginDocsHelp('companies', { anchor: '#a-companys-page' })}\n backHref={routes.section('companies')}\n backLabel=\"Back to companies\"\n actions={\n <>\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ companyId: company.$id }}\n phone={company.phone}\n />\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n </>\n }\n menuItems={[\n {\n key: 'delete',\n label: deleting ? 'Deleting…' : 'Delete company',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !scope || deleting,\n disabledReason: deleting ? 'The company is being deleted' : 'The organization has not loaded',\n onClick: () => void handleDelete(),\n },\n ]}\n chips={\n <>\n <CrmRecordChip label=\"Industry\" value={company.industry} />\n <CrmRecordChip\n label=\"Owner\"\n value={\n company.ownerUid\n ? members.ready\n ? members.labelFor(company.ownerUid)\n : '…'\n : undefined\n }\n />\n </>\n }\n >\n <Stack spacing={1}>\n {factRows(rows)}\n {moreRows.length ? (\n <>\n <Typography variant=\"subtitle2\" sx={{ pt: 1 }}>\n {'More fields'}\n </Typography>\n {factRows(moreRows)}\n </>\n ) : null}\n </Stack>\n {editing ? (\n <CompanyEditDrawer\n open\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n company={company}\n seed={seed}\n members={members}\n onSaved={() => setEditing(false)}\n />\n ) : null}\n </CrmRecordHeader>\n )\n}\nCompanyPropertiesCard.displayName = 'CompanyPropertiesCard'\n\nexport default CompanyPropertiesCard\n"],"names":["pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","Button","Link","Stack","Typography","useCallback","useState","useContactFieldDefinitions","useCrmActivityLogger","COMPANY_DETACH_LIMIT","companyDeleteFailureMessage","deleteCompanyThroughRoute","CompanyEditDrawer","formatContactCustomValue","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","useCrmApi","formatAddress","address","line1","line2","city","state","filter","Boolean","join","postalCode","country","map","part","String","trim","CompanyPropertiesCard","props","company","seed","hostId","org","crmScope","members","routes","onDeleted","scope","callCrm","confirm","enqueueSnackbar","logActivity","editing","setEditing","deleting","setDeleting","fields","orgId","handleDelete","name","$id","accepted","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","deleted","toLocaleString","variant","type","id","detached","persist","error","console","allowDuplicate","rows","label","value","website","href","target","rel","phone","tags","notes","moreRows","active","definition","text","custom","key","factRows","list","row","direction","spacing","sx","alignItems","minWidth","flexShrink","whiteSpace","overflowWrap","kind","subtitle","domain","help","anchor","backHref","section","backLabel","actions","link","companyId","size","startIcon","path","onClick","menuItems","icon","destructive","disabled","disabledReason","chips","industry","ownerUid","ready","labelFor","undefined","length","pt","open","onClose","onSaved","displayName"],"mappings":"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"}
|
|
@@ -45,8 +45,9 @@ export interface ContactAssociationsCardProps {
|
|
|
45
45
|
* lists and each email's own picker, and the helper says so where a
|
|
46
46
|
* reader would otherwise assume the opposite.
|
|
47
47
|
*
|
|
48
|
-
* The filing is the one editable thing here and has its own Save
|
|
49
|
-
* the same stale-seed guard the properties card
|
|
48
|
+
* The filing is the one editable thing here and has its own **Save filing**
|
|
49
|
+
* in the card header, through the same stale-seed guard the properties card
|
|
50
|
+
* uses: a campaign membership
|
|
50
51
|
* written over a cached read could revert somebody else's filing. The save
|
|
51
52
|
* goes to `crm/contact-update` (AGL-2804), because the filing is this
|
|
52
53
|
* holder's facet and a facet is the server's to write; like every field that
|
|
@@ -54,8 +54,9 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
54
54
|
* lists and each email's own picker, and the helper says so where a
|
|
55
55
|
* reader would otherwise assume the opposite.
|
|
56
56
|
*
|
|
57
|
-
* The filing is the one editable thing here and has its own Save
|
|
58
|
-
* the same stale-seed guard the properties card
|
|
57
|
+
* The filing is the one editable thing here and has its own **Save filing**
|
|
58
|
+
* in the card header, through the same stale-seed guard the properties card
|
|
59
|
+
* uses: a campaign membership
|
|
59
60
|
* written over a cached read could revert somebody else's filing. The save
|
|
60
61
|
* goes to `crm/contact-update` (AGL-2804), because the filing is this
|
|
61
62
|
* holder's facet and a facet is the server's to write; like every field that
|
|
@@ -181,6 +182,15 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
181
182
|
}),
|
|
182
183
|
contentGutterX: true,
|
|
183
184
|
contentGutterY: true,
|
|
185
|
+
HeaderProps: {
|
|
186
|
+
action: /*#__PURE__*/ _jsx(Button, {
|
|
187
|
+
size: "small",
|
|
188
|
+
variant: "outlined",
|
|
189
|
+
disabled: saving || siteCampaigns.ready && !siteCampaigns.options.length,
|
|
190
|
+
onClick: ()=>void handleSaveFiling(),
|
|
191
|
+
children: saving ? 'Saving…' : 'Save filing'
|
|
192
|
+
})
|
|
193
|
+
},
|
|
184
194
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
185
195
|
spacing: 2,
|
|
186
196
|
children: [
|
|
@@ -296,16 +306,6 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
|
|
|
296
306
|
helperText: "Your own filing. It never adds anyone to a send — a campaign mails its lists.",
|
|
297
307
|
empty: siteCampaigns.ready && !siteCampaigns.options.length,
|
|
298
308
|
emptyText: hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'
|
|
299
|
-
}),
|
|
300
|
-
/*#__PURE__*/ _jsx(Stack, {
|
|
301
|
-
direction: "row",
|
|
302
|
-
children: /*#__PURE__*/ _jsx(Button, {
|
|
303
|
-
size: "small",
|
|
304
|
-
variant: "outlined",
|
|
305
|
-
disabled: saving || siteCampaigns.ready && !siteCampaigns.options.length,
|
|
306
|
-
onClick: ()=>void handleSaveFiling(),
|
|
307
|
-
children: saving ? 'Saving…' : 'Save filing'
|
|
308
|
-
})
|
|
309
309
|
})
|
|
310
310
|
]
|
|
311
311
|
})
|