@aglyn/plugins-crm 1.0.0-beta.183 → 1.0.0-beta.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/companies-section.js +97 -54
  3. package/src/lib/components/companies-section.js.map +1 -1
  4. package/src/lib/components/contacts-section.js +51 -19
  5. package/src/lib/components/contacts-section.js.map +1 -1
  6. package/src/lib/components/crm-filter-bar.d.ts +34 -32
  7. package/src/lib/components/crm-filter-bar.js +34 -235
  8. package/src/lib/components/crm-filter-bar.js.map +1 -1
  9. package/src/lib/components/crm-next-activity-column.d.ts +13 -21
  10. package/src/lib/components/crm-next-activity-column.js +21 -32
  11. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  12. package/src/lib/components/deals-section.js +65 -54
  13. package/src/lib/components/deals-section.js.map +1 -1
  14. package/src/lib/components/leads-section.js +106 -286
  15. package/src/lib/components/leads-section.js.map +1 -1
  16. package/src/lib/components/tasks-section.js +139 -34
  17. package/src/lib/components/tasks-section.js.map +1 -1
  18. package/src/lib/hooks/use-crm-grid-filter.d.ts +77 -0
  19. package/src/lib/hooks/use-crm-grid-filter.js +143 -0
  20. package/src/lib/hooks/use-crm-grid-filter.js.map +1 -0
  21. package/src/lib/model/crm-grid-filter.d.ts +64 -0
  22. package/src/lib/model/crm-grid-filter.js +144 -0
  23. package/src/lib/model/crm-grid-filter.js.map +1 -0
  24. package/src/lib/model/lead-filters.d.ts +42 -0
  25. package/src/lib/model/lead-filters.js +182 -9
  26. package/src/lib/model/lead-filters.js.map +1 -1
  27. package/src/lib/server/email-send.js +4 -2
  28. 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 { _ as _extends } from "@swc/helpers/_/_extends";
18
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { CRM_VIEW_MAX_FILTERS } from "@aglyn/aglyn";
20
- import { mdiFilterPlusOutline } from "@aglyn/shared-data-mdi";
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
- * The chips above a CRM list: every clause the list is narrowed by, and
38
- * the one control that adds another (AGL-2617).
32
+ * Every clause a CRM list is narrowed by, as chips over the grid (AGL-2617,
33
+ * AGL-3313).
39
34
  *
40
- * A saved view carries several clauses and the data grid's own filter panel
41
- * holds one, so the list needs an editor of its own for the set. Each
42
- * clause is a chip that reads as a sentence — "Owner is Dana", "Created on
43
- * or after 1 Jan" — with the clause the query serves marked, because that
44
- * is the one that reached every contact and the others narrowed the loaded
45
- * window; the list's caption says the same thing in prose.
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
- var _choosable_find;
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
- return /*#__PURE__*/ _jsxs(Stack, {
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
- children: [
136
- clauses.map((clause, index)=>{
137
- const served = servedField !== null && clause.field === servedField;
138
- return /*#__PURE__*/ _jsx(Tooltip, {
139
- title: served ? 'Searched across every record' : 'Narrows the records already loaded',
140
- children: /*#__PURE__*/ _jsx(Chip, {
141
- size: "small",
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
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
153
- path: mdiFilterPlusOutline.path,
154
- size: 0.8
155
- }),
156
- onClick: open,
157
- disabled: disabled || !choosable.length || clauses.length >= CRM_VIEW_MAX_FILTERS,
158
- "aria-haspopup": "dialog",
159
- children: 'Add filter'
160
- }),
161
- /*#__PURE__*/ _jsx(Popover, {
162
- open: Boolean(anchor),
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 { type CrmViewFilterClause } from '@aglyn/aglyn';
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 and filtering are off
13
- * for the same reason the custom columns' are — the value is on the row,
14
- * and a sort over the loaded window would look like a sort over the
15
- * collection; the "No next activity" toggle is the filter, and it says
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
- * The "No next activity" filter as one chip beside the view control: on,
27
- * the list keeps only the records with nothing scheduled. It is a clause
28
- * on the SAVED VIEW — `withNoNextActivity` — so a view saved with it on
29
- * reopens with it on, and the clause survives the other filters a list
30
- * sets around it.
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 function NoNextActivityToggle(props: NoNextActivityToggleProps): import("react").JSX.Element;
33
- export declare namespace NoNextActivityToggle {
34
- var displayName: string;
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, isNoNextActivityClause, readNextTaskAtMs, withNoNextActivity } from "@aglyn/aglyn";
19
- import { Chip, Tooltip, Typography } from "@mui/material";
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 and filtering are off
31
- * for the same reason the custom columns' are — the value is on the row,
32
- * and a sort over the loaded window would look like a sort over the
33
- * collection; the "No next activity" toggle is the filter, and it says
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
- * The "No next activity" filter as one chip beside the view control: on,
75
- * the list keeps only the records with nothing scheduled. It is a clause
76
- * on the SAVED VIEW — `withNoNextActivity` — so a view saved with it on
77
- * reopens with it on, and the clause survives the other filters a list
78
- * sets around it.
79
- */ export function NoNextActivityToggle(props) {
80
- const { filters, onChange, disabled } = props;
81
- const on = filters.some(isNoNextActivityClause);
82
- return /*#__PURE__*/ _jsx(Tooltip, {
83
- title: on ? 'Showing records with nothing scheduled' : 'Only records with nothing scheduled',
84
- // Describes rather than names: the chip's label is what a reader — and
85
- // a saved view's caption — calls the filter.
86
- describeChild: true,
87
- children: /*#__PURE__*/ _jsx(Chip, {
88
- size: "small",
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 {\n CRM_NEXT_ACTIVITY_FIELD,\n type CrmViewFilterClause,\n isNoNextActivityClause,\n readNextTaskAtMs,\n withNoNextActivity,\n} from '@aglyn/aglyn'\nimport { Chip, 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 and filtering are off\n * for the same reason the custom columns' are — the value is on the row,\n * and a sort over the loaded window would look like a sort over the\n * collection; the \"No next activity\" toggle is the filter, and it says\n * what it narrows.\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\nexport interface NoNextActivityToggleProps {\n /** The view's clauses; the toggle reads its own state off them. */\n filters: readonly CrmViewFilterClause[]\n onChange: (filters: CrmViewFilterClause[]) => void\n disabled?: boolean\n}\n\n/**\n * The \"No next activity\" filter as one chip beside the view control: on,\n * the list keeps only the records with nothing scheduled. It is a clause\n * on the SAVED VIEW — `withNoNextActivity` — so a view saved with it on\n * reopens with it on, and the clause survives the other filters a list\n * sets around it.\n */\nexport function NoNextActivityToggle(props: NoNextActivityToggleProps) {\n const { filters, onChange, disabled } = props\n const on = filters.some(isNoNextActivityClause)\n return (\n <Tooltip\n title={on ? 'Showing records with nothing scheduled' : 'Only records with nothing scheduled'}\n // Describes rather than names: the chip's label is what a reader — and\n // a saved view's caption — calls the filter.\n describeChild\n >\n <Chip\n size=\"small\"\n label=\"No next activity\"\n variant={on ? 'filled' : 'outlined'}\n color={on ? 'primary' : 'default'}\n disabled={disabled}\n onClick={() => onChange(withNoNextActivity(filters, !on))}\n aria-pressed={on}\n />\n </Tooltip>\n )\n}\nNoNextActivityToggle.displayName = 'NoNextActivityToggle'\n"],"names":["CRM_NEXT_ACTIVITY_FIELD","isNoNextActivityClause","readNextTaskAtMs","withNoNextActivity","Chip","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","NoNextActivityToggle","props","filters","onChange","disabled","on","some","describeChild","size","label","onClick","aria-pressed","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,uBAAuB,EAEvBC,sBAAsB,EACtBC,gBAAgB,EAChBC,kBAAkB,QACb,eAAc;AACrB,SAASC,IAAI,EAAEC,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEzD,SAASC,WAAW,QAAQ,kBAAc;AAE1C;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAO;QACLC,OAAOV;QACPW,YAAY;QACZC,MAAM;QACNC,UAAU;QACVC,MAAM;QACNC,UAAU;QACVC,YAAY;QACZC,aAAa,CAACC,QAAiBC;YAC7B,MAAMC,KAAKlB,iBAAiBiB;YAC5B,OAAOC,OAAO,OAAO,OAAO,IAAIC,KAAKD;QACvC;QACAE,YAAY,CAAC,EAAEH,GAAG,EAAuC;YACvD,MAAMC,KAAKlB,iBAAiBiB;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;AASA;;;;;;CAMC,GACD,OAAO,SAASQ,qBAAqBC,KAAgC;IACnE,MAAM,EAAEC,OAAO,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAGH;IACxC,MAAMI,KAAKH,QAAQI,IAAI,CAACpC;IACxB,qBACE,KAACI;QACCoB,OAAOW,KAAK,2CAA2C;QACvD,uEAAuE;QACvE,6CAA6C;QAC7CE,aAAa;kBAEb,cAAA,KAAClC;YACCmC,MAAK;YACLC,OAAM;YACNjB,SAASa,KAAK,WAAW;YACzBZ,OAAOY,KAAK,YAAY;YACxBD,UAAUA;YACVM,SAAS,IAAMP,SAAS/B,mBAAmB8B,SAAS,CAACG;YACrDM,gBAAcN;;;AAItB;AACAL,qBAAqBY,WAAW,GAAG"}
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"}