@aglyn/plugins-crm 1.0.0-beta.183 → 1.0.0-beta.185
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/companies-section.js +97 -54
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/contacts-section.js +51 -19
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-filter-bar.d.ts +34 -32
- package/src/lib/components/crm-filter-bar.js +34 -235
- package/src/lib/components/crm-filter-bar.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +13 -21
- package/src/lib/components/crm-next-activity-column.js +21 -32
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deals-section.js +65 -54
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/leads-section.js +106 -286
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +139 -34
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/hooks/use-crm-grid-filter.d.ts +77 -0
- package/src/lib/hooks/use-crm-grid-filter.js +143 -0
- package/src/lib/hooks/use-crm-grid-filter.js.map +1 -0
- package/src/lib/model/crm-grid-filter.d.ts +64 -0
- package/src/lib/model/crm-grid-filter.js +144 -0
- package/src/lib/model/crm-grid-filter.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +42 -0
- package/src/lib/model/lead-filters.js +182 -9
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/server/email-send.js +4 -2
- package/src/lib/server/email-send.js.map +1 -1
|
@@ -14,15 +14,10 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
22
|
-
import { listFilterOperatorLabel, listFilterOperators } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
23
|
-
import { Button, Chip, MenuItem, Popover, Stack, TextField, Tooltip } from "@mui/material";
|
|
24
|
-
import { useMemo, useState } from "react";
|
|
25
|
-
/** Operators that carry no value, so the value box is not shown for them. */ const VALUELESS = new Set([
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
import { listFilterOperatorLabel } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
19
|
+
import { Chip, Stack, Tooltip } from "@mui/material";
|
|
20
|
+
/** Operators that carry no value. */ const VALUELESS = new Set([
|
|
26
21
|
'isEmpty',
|
|
27
22
|
'isNotEmpty'
|
|
28
23
|
]);
|
|
@@ -34,84 +29,22 @@ const dayLabel = (raw)=>{
|
|
|
34
29
|
return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString();
|
|
35
30
|
};
|
|
36
31
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* Every clause a CRM list is narrowed by, as chips over the grid (AGL-2617,
|
|
33
|
+
* AGL-3313).
|
|
39
34
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* Adding one is a popover, not a row of controls: field, operator, value,
|
|
48
|
-
* Add. The value control follows the field — a picker where the section
|
|
49
|
-
* supplied choices, a date box for a date, a number box for a number, a
|
|
50
|
-
* text box otherwise — and the operators offered are exactly the ones the
|
|
51
|
-
* grammar declares for the field, so a clause cannot be built that nothing
|
|
52
|
-
* answers.
|
|
35
|
+
* The clauses are ADDED through the grid's own Filters panel; this bar is
|
|
36
|
+
* what shows the SET. The free DataGrid's panel holds one item at a time
|
|
37
|
+
* and a saved view holds several, so without it a view narrowed by lead
|
|
38
|
+
* source and status would show one of the two and hide the other. Each
|
|
39
|
+
* chip reads as a sentence — "Owner is Dana", "Created on or after 1 Jan"
|
|
40
|
+
* — and removes its clause; where some clauses reach the query and others
|
|
41
|
+
* narrow the loaded rows, the served one is marked.
|
|
53
42
|
*/ export function CrmFilterBar(props) {
|
|
54
|
-
|
|
55
|
-
const { fields, headers, clauses, onChange, options = {}, servedField = null, disabled = false, onOpen } = props;
|
|
56
|
-
const [anchor, setAnchor] = useState(null);
|
|
57
|
-
const [draft, setDraft] = useState({
|
|
58
|
-
field: '',
|
|
59
|
-
op: '',
|
|
60
|
-
value: ''
|
|
61
|
-
});
|
|
62
|
-
/** Fields a clause can be added on — those with at least one operator. */ const choosable = useMemo(()=>fields.filter((field)=>listFilterOperators(field).length > 0), [
|
|
63
|
-
fields
|
|
64
|
-
]);
|
|
43
|
+
const { fields, headers, clauses, onChange, options = {}, servedField = null, marksServed = true, disabled = false } = props;
|
|
65
44
|
const header = (column)=>{
|
|
66
45
|
var _headers_column;
|
|
67
46
|
return (_headers_column = headers[column]) != null ? _headers_column : column;
|
|
68
47
|
};
|
|
69
|
-
const draftField = (_choosable_find = choosable.find((field)=>field.column === draft.field)) != null ? _choosable_find : null;
|
|
70
|
-
const operators = draftField ? listFilterOperators(draftField) : [];
|
|
71
|
-
const draftOptions = draft.field ? options[draft.field] : undefined;
|
|
72
|
-
const valueless = VALUELESS.has(draft.op);
|
|
73
|
-
const multi = MULTI.has(draft.op);
|
|
74
|
-
const open = (event)=>{
|
|
75
|
-
var _ref, _firstOps_;
|
|
76
|
-
const first = choosable[0];
|
|
77
|
-
const firstOps = first ? listFilterOperators(first) : [];
|
|
78
|
-
setDraft({
|
|
79
|
-
field: (_ref = first == null ? void 0 : first.column) != null ? _ref : '',
|
|
80
|
-
op: (_firstOps_ = firstOps[0]) != null ? _firstOps_ : '',
|
|
81
|
-
value: ''
|
|
82
|
-
});
|
|
83
|
-
setAnchor(event.currentTarget);
|
|
84
|
-
onOpen == null ? void 0 : onOpen();
|
|
85
|
-
};
|
|
86
|
-
const close = ()=>setAnchor(null);
|
|
87
|
-
const chooseField = (column)=>{
|
|
88
|
-
var _ops_;
|
|
89
|
-
const field = choosable.find((entry)=>entry.column === column);
|
|
90
|
-
const ops = field ? listFilterOperators(field) : [];
|
|
91
|
-
setDraft({
|
|
92
|
-
field: column,
|
|
93
|
-
op: (_ops_ = ops[0]) != null ? _ops_ : '',
|
|
94
|
-
value: ''
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
const canAdd = Boolean(draft.field && draft.op) && (valueless || draft.value.trim() !== '') && clauses.length < CRM_VIEW_MAX_FILTERS;
|
|
98
|
-
const add = ()=>{
|
|
99
|
-
var _draftOptions_find;
|
|
100
|
-
if (!canAdd) return;
|
|
101
|
-
const value = valueless ? '' : draft.value.trim();
|
|
102
|
-
const label = draftOptions && !multi ? (_draftOptions_find = draftOptions.find((option)=>option.value === value)) == null ? void 0 : _draftOptions_find.label : undefined;
|
|
103
|
-
onChange([
|
|
104
|
-
...clauses,
|
|
105
|
-
_extends({
|
|
106
|
-
field: draft.field,
|
|
107
|
-
op: draft.op,
|
|
108
|
-
value
|
|
109
|
-
}, label ? {
|
|
110
|
-
label
|
|
111
|
-
} : {})
|
|
112
|
-
]);
|
|
113
|
-
close();
|
|
114
|
-
};
|
|
115
48
|
/** How a clause reads: its header, its operator, and its value by label. */ const sentence = (clause)=>{
|
|
116
49
|
var _clause_label;
|
|
117
50
|
const field = fields.find((entry)=>entry.column === clause.field);
|
|
@@ -124,7 +57,8 @@ const dayLabel = (raw)=>{
|
|
|
124
57
|
const value = VALUELESS.has(clause.op) ? '' : MULTI.has(clause.op) ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean).map(named).join(', ') : (_clause_label = clause.label) != null ? _clause_label : (field == null ? void 0 : field.kind) === 'date' ? dayLabel(clause.value) : named(clause.value);
|
|
125
58
|
return `${header(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`;
|
|
126
59
|
};
|
|
127
|
-
|
|
60
|
+
if (!clauses.length) return null;
|
|
61
|
+
return /*#__PURE__*/ _jsx(Stack, {
|
|
128
62
|
direction: "row",
|
|
129
63
|
spacing: 1,
|
|
130
64
|
useFlexGap: true,
|
|
@@ -132,160 +66,25 @@ const dayLabel = (raw)=>{
|
|
|
132
66
|
alignItems: 'center',
|
|
133
67
|
flexWrap: 'wrap'
|
|
134
68
|
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
variant: served ? 'filled' : 'outlined',
|
|
143
|
-
color: served ? 'primary' : 'default',
|
|
144
|
-
label: sentence(clause),
|
|
145
|
-
disabled: disabled,
|
|
146
|
-
onDelete: ()=>onChange(clauses.filter((_entry, at)=>at !== index))
|
|
147
|
-
})
|
|
148
|
-
}, `${clause.field}-${clause.op}-${index}`);
|
|
149
|
-
}),
|
|
150
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
69
|
+
role: "list",
|
|
70
|
+
"aria-label": "Filters",
|
|
71
|
+
children: clauses.map((clause, index)=>{
|
|
72
|
+
const served = marksServed && servedField !== null && clause.field === servedField;
|
|
73
|
+
const key = `${clause.field}-${clause.op}-${index}`;
|
|
74
|
+
const chip = /*#__PURE__*/ _jsx(Chip, {
|
|
75
|
+
role: "listitem",
|
|
151
76
|
size: "small",
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
anchorEl: anchor,
|
|
164
|
-
onClose: close,
|
|
165
|
-
anchorOrigin: {
|
|
166
|
-
vertical: 'bottom',
|
|
167
|
-
horizontal: 'left'
|
|
168
|
-
},
|
|
169
|
-
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
170
|
-
spacing: 1.5,
|
|
171
|
-
sx: {
|
|
172
|
-
p: 2,
|
|
173
|
-
minWidth: 280
|
|
174
|
-
},
|
|
175
|
-
role: "dialog",
|
|
176
|
-
"aria-label": "Add filter",
|
|
177
|
-
children: [
|
|
178
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
179
|
-
select: true,
|
|
180
|
-
size: "small",
|
|
181
|
-
label: "Field",
|
|
182
|
-
value: draft.field,
|
|
183
|
-
onChange: (event)=>chooseField(event.target.value),
|
|
184
|
-
children: choosable.map((field)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
185
|
-
value: field.column,
|
|
186
|
-
children: header(field.column)
|
|
187
|
-
}, field.column))
|
|
188
|
-
}),
|
|
189
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
190
|
-
select: true,
|
|
191
|
-
size: "small",
|
|
192
|
-
label: "Condition",
|
|
193
|
-
value: draft.op,
|
|
194
|
-
onChange: (event)=>setDraft((prev)=>_extends({}, prev, {
|
|
195
|
-
op: event.target.value,
|
|
196
|
-
value: ''
|
|
197
|
-
})),
|
|
198
|
-
disabled: !draftField,
|
|
199
|
-
children: operators.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
200
|
-
value: op,
|
|
201
|
-
children: listFilterOperatorLabel(op)
|
|
202
|
-
}, op))
|
|
203
|
-
}),
|
|
204
|
-
valueless ? null : draftOptions ? /*#__PURE__*/ _jsx(TextField, {
|
|
205
|
-
select: true,
|
|
206
|
-
size: "small",
|
|
207
|
-
label: "Value",
|
|
208
|
-
value: multi ? draft.value.split(',').filter(Boolean) : draft.value,
|
|
209
|
-
onChange: (event)=>{
|
|
210
|
-
const next = event.target.value;
|
|
211
|
-
setDraft((prev)=>_extends({}, prev, {
|
|
212
|
-
value: Array.isArray(next) ? next.join(',') : String(next)
|
|
213
|
-
}));
|
|
214
|
-
},
|
|
215
|
-
slotProps: multi ? {
|
|
216
|
-
select: {
|
|
217
|
-
multiple: true
|
|
218
|
-
}
|
|
219
|
-
} : undefined,
|
|
220
|
-
children: draftOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
221
|
-
value: option.value,
|
|
222
|
-
children: option.label
|
|
223
|
-
}, option.value))
|
|
224
|
-
}) : (draftField == null ? void 0 : draftField.kind) === 'boolean' ? /*#__PURE__*/ _jsxs(TextField, {
|
|
225
|
-
select: true,
|
|
226
|
-
size: "small",
|
|
227
|
-
label: "Value",
|
|
228
|
-
value: draft.value,
|
|
229
|
-
onChange: (event)=>setDraft((prev)=>_extends({}, prev, {
|
|
230
|
-
value: event.target.value
|
|
231
|
-
})),
|
|
232
|
-
children: [
|
|
233
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
234
|
-
value: "true",
|
|
235
|
-
children: 'Yes'
|
|
236
|
-
}),
|
|
237
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
238
|
-
value: "false",
|
|
239
|
-
children: 'No'
|
|
240
|
-
})
|
|
241
|
-
]
|
|
242
|
-
}) : /*#__PURE__*/ _jsx(TextField, {
|
|
243
|
-
size: "small",
|
|
244
|
-
label: "Value",
|
|
245
|
-
type: (draftField == null ? void 0 : draftField.kind) === 'date' ? 'date' : (draftField == null ? void 0 : draftField.kind) === 'number' ? 'number' : 'text',
|
|
246
|
-
placeholder: multi ? 'one, two, three' : undefined,
|
|
247
|
-
helperText: multi ? 'Separate several with commas' : undefined,
|
|
248
|
-
value: draft.value,
|
|
249
|
-
onChange: (event)=>setDraft((prev)=>_extends({}, prev, {
|
|
250
|
-
value: event.target.value
|
|
251
|
-
})),
|
|
252
|
-
onKeyDown: (event)=>{
|
|
253
|
-
if (event.key === 'Enter') {
|
|
254
|
-
event.preventDefault();
|
|
255
|
-
add();
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
slotProps: (draftField == null ? void 0 : draftField.kind) === 'date' ? {
|
|
259
|
-
inputLabel: {
|
|
260
|
-
shrink: true
|
|
261
|
-
}
|
|
262
|
-
} : undefined
|
|
263
|
-
}),
|
|
264
|
-
/*#__PURE__*/ _jsxs(Stack, {
|
|
265
|
-
direction: "row",
|
|
266
|
-
spacing: 1,
|
|
267
|
-
sx: {
|
|
268
|
-
justifyContent: 'flex-end'
|
|
269
|
-
},
|
|
270
|
-
children: [
|
|
271
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
272
|
-
size: "small",
|
|
273
|
-
onClick: close,
|
|
274
|
-
children: 'Cancel'
|
|
275
|
-
}),
|
|
276
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
277
|
-
size: "small",
|
|
278
|
-
variant: "contained",
|
|
279
|
-
disabled: !canAdd,
|
|
280
|
-
onClick: add,
|
|
281
|
-
children: 'Add'
|
|
282
|
-
})
|
|
283
|
-
]
|
|
284
|
-
})
|
|
285
|
-
]
|
|
286
|
-
})
|
|
287
|
-
})
|
|
288
|
-
]
|
|
77
|
+
variant: served ? 'filled' : 'outlined',
|
|
78
|
+
color: served ? 'primary' : 'default',
|
|
79
|
+
label: sentence(clause),
|
|
80
|
+
disabled: disabled,
|
|
81
|
+
onDelete: ()=>onChange(clauses.filter((_entry, at)=>at !== index))
|
|
82
|
+
}, key);
|
|
83
|
+
return marksServed ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
84
|
+
title: served ? 'Searched across every record' : 'Narrows the records already loaded',
|
|
85
|
+
children: chip
|
|
86
|
+
}, key) : chip;
|
|
87
|
+
})
|
|
289
88
|
});
|
|
290
89
|
}
|
|
291
90
|
CrmFilterBar.displayName = 'CrmFilterBar';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-filter-bar.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 { CRM_VIEW_MAX_FILTERS, type CrmViewFilterClause } from '@aglyn/aglyn'\nimport { mdiFilterPlusOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n type ListFilterField,\n listFilterOperatorLabel,\n listFilterOperators,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n Button,\n Chip,\n MenuItem,\n Popover,\n Stack,\n TextField,\n Tooltip,\n} from '@mui/material'\nimport { type MouseEvent, useMemo, useState } from 'react'\n\n/** A choice for a field whose value is picked rather than typed. */\nexport interface CrmFilterOption {\n value: string\n label: string\n}\n\nexport interface CrmFilterBarProps {\n /** The list's grammar — every field a clause may name. */\n fields: readonly ListFilterField[]\n /** How a field reads on a chip; a field without one reads as its column. */\n headers: Readonly<Record<string, string>>\n clauses: readonly CrmViewFilterClause[]\n onChange: (clauses: CrmViewFilterClause[]) => void\n /**\n * Choices per field — an owner from the roster, a stage from the fixed\n * list, a source from its labels. A field with choices gets a picker\n * and its chip shows the label; without them a value is typed.\n */\n options?: Readonly<Record<string, readonly CrmFilterOption[]>>\n /** The field the query is serving, marked so the reader knows which reached everything. */\n servedField?: string | null\n disabled?: boolean\n /**\n * The reader reached for a filter. A section that reads its pickers'\n * choices lazily — the roster, the companies — starts those reads here,\n * so a list nobody is narrowing pays for neither.\n */\n onOpen?: () => void\n}\n\n/** Operators that carry no value, so the value box is not shown for them. */\nconst VALUELESS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI = new Set(['isAnyOf'])\n\nconst dayLabel = (raw: string): string => {\n const at = new Date(raw)\n return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString()\n}\n\n/**\n * The chips above a CRM list: every clause the list is narrowed by, and\n * the one control that adds another (AGL-2617).\n *\n * A saved view carries several clauses and the data grid's own filter panel\n * holds one, so the list needs an editor of its own for the set. Each\n * clause is a chip that reads as a sentence — \"Owner is Dana\", \"Created on\n * or after 1 Jan\" — with the clause the query serves marked, because that\n * is the one that reached every contact and the others narrowed the loaded\n * window; the list's caption says the same thing in prose.\n *\n * Adding one is a popover, not a row of controls: field, operator, value,\n * Add. The value control follows the field — a picker where the section\n * supplied choices, a date box for a date, a number box for a number, a\n * text box otherwise — and the operators offered are exactly the ones the\n * grammar declares for the field, so a clause cannot be built that nothing\n * answers.\n */\nexport function CrmFilterBar(props: CrmFilterBarProps) {\n const {\n fields,\n headers,\n clauses,\n onChange,\n options = {},\n servedField = null,\n disabled = false,\n onOpen,\n } = props\n const [anchor, setAnchor] = useState<HTMLElement | null>(null)\n const [draft, setDraft] = useState<{ field: string; op: string; value: string }>({\n field: '',\n op: '',\n value: '',\n })\n\n /** Fields a clause can be added on — those with at least one operator. */\n const choosable = useMemo(\n () => fields.filter((field) => listFilterOperators(field).length > 0),\n [fields],\n )\n const header = (column: string) => headers[column] ?? column\n const draftField = choosable.find((field) => field.column === draft.field) ?? null\n const operators = draftField ? listFilterOperators(draftField) : []\n const draftOptions = draft.field ? options[draft.field] : undefined\n const valueless = VALUELESS.has(draft.op)\n const multi = MULTI.has(draft.op)\n\n const open = (event: MouseEvent<HTMLElement>) => {\n const first = choosable[0]\n const firstOps = first ? listFilterOperators(first) : []\n setDraft({ field: first?.column ?? '', op: firstOps[0] ?? '', value: '' })\n setAnchor(event.currentTarget)\n onOpen?.()\n }\n const close = () => setAnchor(null)\n\n const chooseField = (column: string) => {\n const field = choosable.find((entry) => entry.column === column)\n const ops = field ? listFilterOperators(field) : []\n setDraft({ field: column, op: ops[0] ?? '', value: '' })\n }\n\n const canAdd =\n Boolean(draft.field && draft.op) &&\n (valueless || draft.value.trim() !== '') &&\n clauses.length < CRM_VIEW_MAX_FILTERS\n\n const add = () => {\n if (!canAdd) return\n const value = valueless ? '' : draft.value.trim()\n const label =\n draftOptions && !multi\n ? draftOptions.find((option) => option.value === value)?.label\n : undefined\n onChange([...clauses, { field: draft.field, op: draft.op, value, ...(label ? { label } : {}) }])\n close()\n }\n\n /** How a clause reads: its header, its operator, and its value by label. */\n const sentence = (clause: CrmViewFilterClause): string => {\n const field = fields.find((entry) => entry.column === clause.field)\n const choices = options[clause.field]\n const named = (value: string) =>\n choices?.find((option) => option.value === value)?.label ?? value\n const value = VALUELESS.has(clause.op)\n ? ''\n : MULTI.has(clause.op)\n ? clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(named)\n .join(', ')\n : clause.label ?? (field?.kind === 'date' ? dayLabel(clause.value) : named(clause.value))\n return `${header(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`\n }\n\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n {clauses.map((clause, index) => {\n const served = servedField !== null && clause.field === servedField\n return (\n <Tooltip\n key={`${clause.field}-${clause.op}-${index}`}\n title={\n served\n ? 'Searched across every record'\n : 'Narrows the records already loaded'\n }\n >\n <Chip\n size=\"small\"\n variant={served ? 'filled' : 'outlined'}\n color={served ? 'primary' : 'default'}\n label={sentence(clause)}\n disabled={disabled}\n onDelete={() => onChange(clauses.filter((_entry, at) => at !== index))}\n />\n </Tooltip>\n )\n })}\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiFilterPlusOutline.path} size={0.8} />}\n onClick={open}\n disabled={disabled || !choosable.length || clauses.length >= CRM_VIEW_MAX_FILTERS}\n aria-haspopup=\"dialog\"\n >\n {'Add filter'}\n </Button>\n <Popover\n open={Boolean(anchor)}\n anchorEl={anchor}\n onClose={close}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}\n >\n <Stack spacing={1.5} sx={{ p: 2, minWidth: 280 }} role=\"dialog\" aria-label=\"Add filter\">\n <TextField\n select\n size=\"small\"\n label=\"Field\"\n value={draft.field}\n onChange={(event) => chooseField(event.target.value)}\n >\n {choosable.map((field) => (\n <MenuItem key={field.column} value={field.column}>\n {header(field.column)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Condition\"\n value={draft.op}\n onChange={(event) => setDraft((prev) => ({ ...prev, op: event.target.value, value: '' }))}\n disabled={!draftField}\n >\n {operators.map((op) => (\n <MenuItem key={op} value={op}>\n {listFilterOperatorLabel(op)}\n </MenuItem>\n ))}\n </TextField>\n {valueless ? null : draftOptions ? (\n <TextField\n select\n size=\"small\"\n label=\"Value\"\n value={multi ? draft.value.split(',').filter(Boolean) : draft.value}\n onChange={(event) => {\n const next = event.target.value as unknown\n setDraft((prev) => ({\n ...prev,\n value: Array.isArray(next) ? next.join(',') : String(next),\n }))\n }}\n slotProps={multi ? { select: { multiple: true } } : undefined}\n >\n {draftOptions.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n ) : draftField?.kind === 'boolean' ? (\n <TextField\n select\n size=\"small\"\n label=\"Value\"\n value={draft.value}\n onChange={(event) => setDraft((prev) => ({ ...prev, value: event.target.value }))}\n >\n <MenuItem value=\"true\">{'Yes'}</MenuItem>\n <MenuItem value=\"false\">{'No'}</MenuItem>\n </TextField>\n ) : (\n <TextField\n size=\"small\"\n label=\"Value\"\n type={\n draftField?.kind === 'date'\n ? 'date'\n : draftField?.kind === 'number'\n ? 'number'\n : 'text'\n }\n placeholder={multi ? 'one, two, three' : undefined}\n helperText={multi ? 'Separate several with commas' : undefined}\n value={draft.value}\n onChange={(event) => setDraft((prev) => ({ ...prev, value: event.target.value }))}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n add()\n }\n }}\n slotProps={\n draftField?.kind === 'date' ? { inputLabel: { shrink: true } } : undefined\n }\n />\n )}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button size=\"small\" onClick={close}>\n {'Cancel'}\n </Button>\n <Button size=\"small\" variant=\"contained\" disabled={!canAdd} onClick={add}>\n {'Add'}\n </Button>\n </Stack>\n </Stack>\n </Popover>\n </Stack>\n )\n}\nCrmFilterBar.displayName = 'CrmFilterBar'\n\nexport default CrmFilterBar\n"],"names":["CRM_VIEW_MAX_FILTERS","mdiFilterPlusOutline","MdiIcon","listFilterOperatorLabel","listFilterOperators","Button","Chip","MenuItem","Popover","Stack","TextField","Tooltip","useMemo","useState","VALUELESS","Set","MULTI","dayLabel","raw","at","Date","Number","isNaN","getTime","toLocaleDateString","CrmFilterBar","props","choosable","fields","headers","clauses","onChange","options","servedField","disabled","onOpen","anchor","setAnchor","draft","setDraft","field","op","value","filter","length","header","column","draftField","find","operators","draftOptions","undefined","valueless","has","multi","open","event","firstOps","first","currentTarget","close","chooseField","ops","entry","canAdd","Boolean","trim","add","label","option","sentence","clause","choices","named","split","map","join","kind","direction","spacing","useFlexGap","sx","alignItems","flexWrap","index","served","title","size","variant","color","onDelete","_entry","startIcon","path","onClick","aria-haspopup","anchorEl","onClose","anchorOrigin","vertical","horizontal","p","minWidth","role","aria-label","select","target","prev","next","Array","isArray","String","slotProps","multiple","type","placeholder","helperText","onKeyDown","key","preventDefault","inputLabel","shrink","justifyContent","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-filter-bar.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\n'use client'\n\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport {\n type ListFilterField,\n listFilterOperatorLabel,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { Chip, Stack, Tooltip } from '@mui/material'\nimport type { CrmFilterOption } from '../model/crm-grid-filter'\n\nexport type { CrmFilterOption } from '../model/crm-grid-filter'\n\nexport interface CrmFilterBarProps {\n /** The list's grammar — every field a clause may name. */\n fields: readonly ListFilterField[]\n /** How a field reads on a chip; a field without one reads as its column. */\n headers: Readonly<Record<string, string>>\n clauses: readonly CrmViewFilterClause[]\n onChange: (clauses: CrmViewFilterClause[]) => void\n /** Choices per field, so a chip names a picked value by its label. */\n options?: Readonly<Record<string, readonly CrmFilterOption[]>>\n /**\n * The field the query is serving, marked so the reader knows which reached\n * everything. Omitted on a list whose every clause narrows the same rows.\n */\n servedField?: string | null\n /** Whether chips carry the served / loaded-window distinction at all. */\n marksServed?: boolean\n disabled?: boolean\n}\n\n/** Operators that carry no value. */\nconst VALUELESS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI = new Set(['isAnyOf'])\n\nconst dayLabel = (raw: string): string => {\n const at = new Date(raw)\n return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString()\n}\n\n/**\n * Every clause a CRM list is narrowed by, as chips over the grid (AGL-2617,\n * AGL-3313).\n *\n * The clauses are ADDED through the grid's own Filters panel; this bar is\n * what shows the SET. The free DataGrid's panel holds one item at a time\n * and a saved view holds several, so without it a view narrowed by lead\n * source and status would show one of the two and hide the other. Each\n * chip reads as a sentence — \"Owner is Dana\", \"Created on or after 1 Jan\"\n * — and removes its clause; where some clauses reach the query and others\n * narrow the loaded rows, the served one is marked.\n */\nexport function CrmFilterBar(props: CrmFilterBarProps) {\n const {\n fields,\n headers,\n clauses,\n onChange,\n options = {},\n servedField = null,\n marksServed = true,\n disabled = false,\n } = props\n const header = (column: string) => headers[column] ?? column\n\n /** How a clause reads: its header, its operator, and its value by label. */\n const sentence = (clause: CrmViewFilterClause): string => {\n const field = fields.find((entry) => entry.column === clause.field)\n const choices = options[clause.field]\n const named = (value: string) =>\n choices?.find((option) => option.value === value)?.label ?? value\n const value = VALUELESS.has(clause.op)\n ? ''\n : MULTI.has(clause.op)\n ? clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(named)\n .join(', ')\n : clause.label ?? (field?.kind === 'date' ? dayLabel(clause.value) : named(clause.value))\n return `${header(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`\n }\n\n if (!clauses.length) return null\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n role=\"list\"\n aria-label=\"Filters\"\n >\n {clauses.map((clause, index) => {\n const served = marksServed && servedField !== null && clause.field === servedField\n const key = `${clause.field}-${clause.op}-${index}`\n const chip = (\n <Chip\n key={key}\n role=\"listitem\"\n size=\"small\"\n variant={served ? 'filled' : 'outlined'}\n color={served ? 'primary' : 'default'}\n label={sentence(clause)}\n disabled={disabled}\n onDelete={() => onChange(clauses.filter((_entry, at) => at !== index))}\n />\n )\n return marksServed ? (\n <Tooltip\n key={key}\n title={served ? 'Searched across every record' : 'Narrows the records already loaded'}\n >\n {chip}\n </Tooltip>\n ) : (\n chip\n )\n })}\n </Stack>\n )\n}\nCrmFilterBar.displayName = 'CrmFilterBar'\n\nexport default CrmFilterBar\n"],"names":["listFilterOperatorLabel","Chip","Stack","Tooltip","VALUELESS","Set","MULTI","dayLabel","raw","at","Date","Number","isNaN","getTime","toLocaleDateString","CrmFilterBar","props","fields","headers","clauses","onChange","options","servedField","marksServed","disabled","header","column","sentence","clause","field","find","entry","choices","named","value","option","label","has","op","split","map","trim","filter","Boolean","join","kind","length","direction","spacing","useFlexGap","sx","alignItems","flexWrap","role","aria-label","index","served","key","chip","size","variant","color","onDelete","_entry","title","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAGA,SAEEA,uBAAuB,QAClB,yCAAwC;AAC/C,SAASC,IAAI,EAAEC,KAAK,EAAEC,OAAO,QAAQ,gBAAe;AAwBpD,mCAAmC,GACnC,MAAMC,YAAY,IAAIC,IAAI;IAAC;IAAW;CAAa;AACnD,sFAAsF,GACtF,MAAMC,QAAQ,IAAID,IAAI;IAAC;CAAU;AAEjC,MAAME,WAAW,CAACC;IAChB,MAAMC,KAAK,IAAIC,KAAKF;IACpB,OAAOG,OAAOC,KAAK,CAACH,GAAGI,OAAO,MAAML,MAAMC,GAAGK,kBAAkB;AACjE;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EACJC,MAAM,EACNC,OAAO,EACPC,OAAO,EACPC,QAAQ,EACRC,UAAU,CAAC,CAAC,EACZC,cAAc,IAAI,EAClBC,cAAc,IAAI,EAClBC,WAAW,KAAK,EACjB,GAAGR;IACJ,MAAMS,SAAS,CAACC;YAAmBR;gBAAAA,kBAAAA,OAAO,CAACQ,OAAO,YAAfR,kBAAmBQ;;IAEtD,0EAA0E,GAC1E,MAAMC,WAAW,CAACC;YAcVA;QAbN,MAAMC,QAAQZ,OAAOa,IAAI,CAAC,CAACC,QAAUA,MAAML,MAAM,KAAKE,OAAOC,KAAK;QAClE,MAAMG,UAAUX,OAAO,CAACO,OAAOC,KAAK,CAAC;QACrC,MAAMI,QAAQ,CAACC;;gBACbF;2BAAAA,4BAAAA,gBAAAA,QAASF,IAAI,CAAC,CAACK,SAAWA,OAAOD,KAAK,KAAKA,2BAA3CF,cAAmDI,KAAK,mBAAIF;;QAC9D,MAAMA,QAAQ9B,UAAUiC,GAAG,CAACT,OAAOU,EAAE,IACjC,KACAhC,MAAM+B,GAAG,CAACT,OAAOU,EAAE,IACjBV,OAAOM,KAAK,CACTK,KAAK,CAAC,KACNC,GAAG,CAAC,CAACT,QAAUA,MAAMU,IAAI,IACzBC,MAAM,CAACC,SACPH,GAAG,CAACP,OACJW,IAAI,CAAC,SACRhB,gBAAAA,OAAOQ,KAAK,YAAZR,gBAAiBC,CAAAA,yBAAAA,MAAOgB,IAAI,MAAK,SAAStC,SAASqB,OAAOM,KAAK,IAAID,MAAML,OAAOM,KAAK;QAC3F,OAAO,GAAGT,OAAOG,OAAOC,KAAK,EAAE,CAAC,EAAE7B,wBAAwB4B,OAAOU,EAAE,IAAIJ,QAAQ,CAAC,CAAC,EAAEA,OAAO,GAAG,IAAI;IACnG;IAEA,IAAI,CAACf,QAAQ2B,MAAM,EAAE,OAAO;IAC5B,qBACE,KAAC5C;QACC6C,WAAU;QACVC,SAAS;QACTC,UAAU;QACVC,IAAI;YAAEC,YAAY;YAAUC,UAAU;QAAO;QAC7CC,MAAK;QACLC,cAAW;kBAEVnC,QAAQqB,GAAG,CAAC,CAACZ,QAAQ2B;YACpB,MAAMC,SAASjC,eAAeD,gBAAgB,QAAQM,OAAOC,KAAK,KAAKP;YACvE,MAAMmC,MAAM,GAAG7B,OAAOC,KAAK,CAAC,CAAC,EAAED,OAAOU,EAAE,CAAC,CAAC,EAAEiB,OAAO;YACnD,MAAMG,qBACJ,KAACzD;gBAECoD,MAAK;gBACLM,MAAK;gBACLC,SAASJ,SAAS,WAAW;gBAC7BK,OAAOL,SAAS,YAAY;gBAC5BpB,OAAOT,SAASC;gBAChBJ,UAAUA;gBACVsC,UAAU,IAAM1C,SAASD,QAAQuB,MAAM,CAAC,CAACqB,QAAQtD,KAAOA,OAAO8C;eAP1DE;YAUT,OAAOlC,4BACL,KAACpB;gBAEC6D,OAAOR,SAAS,iCAAiC;0BAEhDE;eAHID,OAMPC;QAEJ;;AAGN;AACA3C,aAAakD,WAAW,GAAG;AAE3B,eAAelD,aAAY"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
2
2
|
import type { GridColDef } from '@mui/x-data-grid';
|
|
3
3
|
/**
|
|
4
4
|
* The "Next activity" column the contacts, companies and deals lists share
|
|
@@ -9,27 +9,19 @@ import type { GridColDef } from '@mui/x-data-grid';
|
|
|
9
9
|
* color, today emphasized — because it IS that date, seen from the record.
|
|
10
10
|
* A dash for a record with nothing scheduled, and the same dash for one
|
|
11
11
|
* written before the field existed: the reader's question is "is anything
|
|
12
|
-
* planned", and to both the answer is no. Sorting
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* what it narrows.
|
|
12
|
+
* planned", and to both the answer is no. Sorting is off for the reason
|
|
13
|
+
* the custom columns' is — the value is on the row, and a sort over the
|
|
14
|
+
* loaded window would look like a sort over the collection. Filtering is
|
|
15
|
+
* the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
|
|
17
16
|
*/
|
|
18
17
|
export declare function nextActivityColumn(nowMs: number): GridColDef;
|
|
19
|
-
export interface NoNextActivityToggleProps {
|
|
20
|
-
/** The view's clauses; the toggle reads its own state off them. */
|
|
21
|
-
filters: readonly CrmViewFilterClause[];
|
|
22
|
-
onChange: (filters: CrmViewFilterClause[]) => void;
|
|
23
|
-
disabled?: boolean;
|
|
24
|
-
}
|
|
25
18
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* reopens
|
|
30
|
-
*
|
|
19
|
+
* "No next activity" as a filter of the grid's own panel (AGL-3313): the
|
|
20
|
+
* Next activity column offers "is empty", which stores the clause
|
|
21
|
+
* `isNoNextActivityClause` names, so a view saved with the old toggle on
|
|
22
|
+
* reopens filtered the same way. Window-only: absence has no index, so it
|
|
23
|
+
* narrows the loaded rows beside whatever the query serves.
|
|
31
24
|
*/
|
|
32
|
-
export declare
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
25
|
+
export declare const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField;
|
|
26
|
+
/** What the clause reads as on a chip. */
|
|
27
|
+
export declare const CRM_NEXT_ACTIVITY_FILTER_HEADER = "Next activity";
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { CRM_NEXT_ACTIVITY_FIELD,
|
|
19
|
-
import {
|
|
18
|
+
import { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from "@aglyn/aglyn";
|
|
19
|
+
import { Tooltip, Typography } from "@mui/material";
|
|
20
20
|
import { TaskDueText } from "./task-cells.js";
|
|
21
21
|
/**
|
|
22
22
|
* The "Next activity" column the contacts, companies and deals lists share
|
|
@@ -27,11 +27,10 @@ import { TaskDueText } from "./task-cells.js";
|
|
|
27
27
|
* color, today emphasized — because it IS that date, seen from the record.
|
|
28
28
|
* A dash for a record with nothing scheduled, and the same dash for one
|
|
29
29
|
* written before the field existed: the reader's question is "is anything
|
|
30
|
-
* planned", and to both the answer is no. Sorting
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* what it narrows.
|
|
30
|
+
* planned", and to both the answer is no. Sorting is off for the reason
|
|
31
|
+
* the custom columns' is — the value is on the row, and a sort over the
|
|
32
|
+
* loaded window would look like a sort over the collection. Filtering is
|
|
33
|
+
* the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
|
|
35
34
|
*/ export function nextActivityColumn(nowMs) {
|
|
36
35
|
return {
|
|
37
36
|
field: CRM_NEXT_ACTIVITY_FIELD,
|
|
@@ -71,30 +70,20 @@ import { TaskDueText } from "./task-cells.js";
|
|
|
71
70
|
};
|
|
72
71
|
}
|
|
73
72
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* reopens
|
|
78
|
-
*
|
|
79
|
-
*/ export
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
label: "No next activity",
|
|
90
|
-
variant: on ? 'filled' : 'outlined',
|
|
91
|
-
color: on ? 'primary' : 'default',
|
|
92
|
-
disabled: disabled,
|
|
93
|
-
onClick: ()=>onChange(withNoNextActivity(filters, !on)),
|
|
94
|
-
"aria-pressed": on
|
|
95
|
-
})
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
NoNextActivityToggle.displayName = 'NoNextActivityToggle';
|
|
73
|
+
* "No next activity" as a filter of the grid's own panel (AGL-3313): the
|
|
74
|
+
* Next activity column offers "is empty", which stores the clause
|
|
75
|
+
* `isNoNextActivityClause` names, so a view saved with the old toggle on
|
|
76
|
+
* reopens filtered the same way. Window-only: absence has no index, so it
|
|
77
|
+
* narrows the loaded rows beside whatever the query serves.
|
|
78
|
+
*/ export const CRM_NEXT_ACTIVITY_FILTER_FIELD = {
|
|
79
|
+
column: CRM_NEXT_ACTIVITY_FIELD,
|
|
80
|
+
kind: 'date',
|
|
81
|
+
path: CRM_NEXT_ACTIVITY_FIELD,
|
|
82
|
+
windowOnly: true,
|
|
83
|
+
operators: [
|
|
84
|
+
'isEmpty'
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
/** What the clause reads as on a chip. */ export const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity';
|
|
99
88
|
|
|
100
89
|
//# sourceMappingURL=crm-next-activity-column.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-next-activity-column.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 {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-next-activity-column.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 { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { TaskDueText } from './task-cells'\n\n/**\n * The \"Next activity\" column the contacts, companies and deals lists share\n * (AGL-2661): when the record's earliest open task is due, off the\n * `nextTaskAtMs` every server-side task writer keeps on the row.\n *\n * Drawn the way the tasks list draws a due date — overdue in the warning\n * color, today emphasized — because it IS that date, seen from the record.\n * A dash for a record with nothing scheduled, and the same dash for one\n * written before the field existed: the reader's question is \"is anything\n * planned\", and to both the answer is no. Sorting is off for the reason\n * the custom columns' is — the value is on the row, and a sort over the\n * loaded window would look like a sort over the collection. Filtering is\n * the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.\n */\nexport function nextActivityColumn(nowMs: number): GridColDef {\n return {\n field: CRM_NEXT_ACTIVITY_FIELD,\n headerName: 'Next activity',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: { nextTaskAtMs?: unknown }) => {\n const at = readNextTaskAtMs(row)\n return at === null ? null : new Date(at)\n },\n renderCell: ({ row }: { row: { nextTaskAtMs?: unknown } }) => {\n const at = readNextTaskAtMs(row)\n if (at === null) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n }\n return (\n <Tooltip title={new Date(at).toLocaleString()}>\n <span>\n <TaskDueText task={{ status: 'open', dueAtMs: at }} nowMs={nowMs} variant=\"caption\" />\n </span>\n </Tooltip>\n )\n },\n }\n}\n\n/**\n * \"No next activity\" as a filter of the grid's own panel (AGL-3313): the\n * Next activity column offers \"is empty\", which stores the clause\n * `isNoNextActivityClause` names, so a view saved with the old toggle on\n * reopens filtered the same way. Window-only: absence has no index, so it\n * narrows the loaded rows beside whatever the query serves.\n */\nexport const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField = {\n column: CRM_NEXT_ACTIVITY_FIELD,\n kind: 'date',\n path: CRM_NEXT_ACTIVITY_FIELD,\n windowOnly: true,\n operators: ['isEmpty'],\n}\n\n/** What the clause reads as on a chip. */\nexport const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity'\n"],"names":["CRM_NEXT_ACTIVITY_FIELD","readNextTaskAtMs","Tooltip","Typography","TaskDueText","nextActivityColumn","nowMs","field","headerName","flex","minWidth","type","sortable","filterable","valueGetter","_value","row","at","Date","renderCell","variant","color","title","toLocaleString","span","task","status","dueAtMs","CRM_NEXT_ACTIVITY_FILTER_FIELD","column","kind","path","windowOnly","operators","CRM_NEXT_ACTIVITY_FILTER_HEADER"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,uBAAuB,EAAEC,gBAAgB,QAAQ,eAAc;AAExE,SAASC,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEnD,SAASC,WAAW,QAAQ,kBAAc;AAE1C;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAO;QACLC,OAAOP;QACPQ,YAAY;QACZC,MAAM;QACNC,UAAU;QACVC,MAAM;QACNC,UAAU;QACVC,YAAY;QACZC,aAAa,CAACC,QAAiBC;YAC7B,MAAMC,KAAKhB,iBAAiBe;YAC5B,OAAOC,OAAO,OAAO,OAAO,IAAIC,KAAKD;QACvC;QACAE,YAAY,CAAC,EAAEH,GAAG,EAAuC;YACvD,MAAMC,KAAKhB,iBAAiBe;YAC5B,IAAIC,OAAO,MAAM;gBACf,qBACE,KAACd;oBAAWiB,SAAQ;oBAAUC,OAAM;8BACjC;;YAGP;YACA,qBACE,KAACnB;gBAAQoB,OAAO,IAAIJ,KAAKD,IAAIM,cAAc;0BACzC,cAAA,KAACC;8BACC,cAAA,KAACpB;wBAAYqB,MAAM;4BAAEC,QAAQ;4BAAQC,SAASV;wBAAG;wBAAGX,OAAOA;wBAAOc,SAAQ;;;;QAIlF;IACF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,MAAMQ,iCAAkD;IAC7DC,QAAQ7B;IACR8B,MAAM;IACNC,MAAM/B;IACNgC,YAAY;IACZC,WAAW;QAAC;KAAU;AACxB,EAAC;AAED,wCAAwC,GACxC,OAAO,MAAMC,kCAAkC,gBAAe"}
|