@aglyn/plugins-crm 1.0.0-beta.229 → 1.0.0-beta.231

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 (52) hide show
  1. package/package.json +14 -13
  2. package/src/lib/components/crm-next-activity-column.d.ts +1 -13
  3. package/src/lib/components/crm-next-activity-column.js +1 -19
  4. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  5. package/src/lib/components/lead-status-options.d.ts +2 -14
  6. package/src/lib/components/lead-status-options.js +1 -13
  7. package/src/lib/components/lead-status-options.js.map +1 -1
  8. package/src/lib/components/lead-unqualify-dialog.d.ts +1 -2
  9. package/src/lib/components/lead-unqualify-dialog.js +2 -1
  10. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  11. package/src/lib/components/settings-section.d.ts +18 -0
  12. package/src/lib/components/settings-section.js +97 -1
  13. package/src/lib/components/settings-section.js.map +1 -1
  14. package/src/lib/constants/company-filters.d.ts +2 -2
  15. package/src/lib/constants/company-filters.js +1 -1
  16. package/src/lib/constants/company-filters.js.map +1 -1
  17. package/src/lib/constants/contact-filters.d.ts +3 -3
  18. package/src/lib/constants/contact-filters.js +2 -1
  19. package/src/lib/constants/contact-filters.js.map +1 -1
  20. package/src/lib/constants/deal-filters.d.ts +3 -3
  21. package/src/lib/constants/deal-filters.js +1 -1
  22. package/src/lib/constants/deal-filters.js.map +1 -1
  23. package/src/lib/constants/field-list-query.d.ts +1 -1
  24. package/src/lib/constants/field-list-query.js +1 -1
  25. package/src/lib/constants/field-list-query.js.map +1 -1
  26. package/src/lib/declarations.server.js +9 -0
  27. package/src/lib/declarations.server.js.map +1 -1
  28. package/src/lib/model/contacts-list-seed.d.ts +1 -1
  29. package/src/lib/model/contacts-list-seed.js.map +1 -1
  30. package/src/lib/model/crm-list-query.d.ts +4 -4
  31. package/src/lib/model/crm-list-query.js +2 -2
  32. package/src/lib/model/crm-list-query.js.map +1 -1
  33. package/src/lib/model/crm-next-activity.d.ts +13 -0
  34. package/src/lib/model/crm-next-activity.js +19 -0
  35. package/src/lib/model/crm-next-activity.js.map +1 -1
  36. package/src/lib/model/lead-filters.d.ts +6 -5
  37. package/src/lib/model/lead-filters.js +3 -2
  38. package/src/lib/model/lead-filters.js.map +1 -1
  39. package/src/lib/model/lead-status-choices.d.ts +32 -0
  40. package/src/lib/model/lead-status-choices.js +31 -0
  41. package/src/lib/model/lead-status-choices.js.map +1 -0
  42. package/src/lib/model/task-views.d.ts +2 -2
  43. package/src/lib/model/task-views.js.map +1 -1
  44. package/src/lib/server/deal-figures.d.ts +47 -0
  45. package/src/lib/server/deal-figures.js +275 -0
  46. package/src/lib/server/deal-figures.js.map +1 -0
  47. package/src/lib/server/person-records.d.ts +27 -0
  48. package/src/lib/server/person-records.js +127 -4
  49. package/src/lib/server/person-records.js.map +1 -1
  50. package/src/lib/server.d.ts +0 -38
  51. package/src/lib/server.js +5 -1
  52. package/src/lib/server.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.229",
3
+ "version": "1.0.0-beta.231",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.229",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.229",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.229",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.229",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.229",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.229",
34
- "@aglyn/shared-util-email": "1.0.0-beta.229",
35
- "@aglyn/shared-util-http": "1.0.0-beta.229",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.229",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.229",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.229",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.229",
28
+ "@aglyn/aglyn": "1.0.0-beta.231",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.231",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.231",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.231",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.231",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.231",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.231",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.231",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.231",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.231",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.231",
40
41
  "@dnd-kit/core": "^6.3.1",
41
42
  "@dnd-kit/utilities": "^3.2.2",
42
43
  "@swc/helpers": "0.5.23",
@@ -1,4 +1,3 @@
1
- import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
2
1
  import type { GridColDef } from '@mui/x-data-grid';
3
2
  /**
4
3
  * The "Next activity" column the contacts, companies and deals lists share
@@ -15,15 +14,4 @@ import type { GridColDef } from '@mui/x-data-grid';
15
14
  * the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
16
15
  */
17
16
  export declare function nextActivityColumn(nowMs: number): GridColDef;
18
- /**
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. Asked of the query as `nextTaskAtMs ==
23
- * null` (AGL-3321): every contact, company and deal is created carrying
24
- * `null`, and the task writers put `null` back when nothing is left
25
- * scheduled.
26
- */
27
- export declare const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField;
28
- /** What the clause reads as on a chip. */
29
- export declare const CRM_NEXT_ACTIVITY_FILTER_HEADER = "Next activity";
17
+ export { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER, } from '../model/crm-next-activity';
@@ -69,24 +69,6 @@ import { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from "../model/crm-next-act
69
69
  }
70
70
  };
71
71
  }
72
- /**
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. Asked of the query as `nextTaskAtMs ==
77
- * null` (AGL-3321): every contact, company and deal is created carrying
78
- * `null`, and the task writers put `null` back when nothing is left
79
- * scheduled.
80
- */ export const CRM_NEXT_ACTIVITY_FILTER_FIELD = {
81
- column: CRM_NEXT_ACTIVITY_FIELD,
82
- kind: 'date',
83
- path: CRM_NEXT_ACTIVITY_FIELD,
84
- presence: 'nullable',
85
- storedAs: 'millis',
86
- operators: [
87
- 'isEmpty'
88
- ]
89
- };
90
- /** What the clause reads as on a chip. */ export const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity';
72
+ export { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER } from "../model/crm-next-activity.js";
91
73
 
92
74
  //# 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 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'\nimport { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from '../model/crm-next-activity'\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. Asked of the query as `nextTaskAtMs ==\n * null` (AGL-3321): every contact, company and deal is created carrying\n * `null`, and the task writers put `null` back when nothing is left\n * scheduled.\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 presence: 'nullable',\n storedAs: 'millis',\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":["Tooltip","Typography","TaskDueText","CRM_NEXT_ACTIVITY_FIELD","readNextTaskAtMs","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","presence","storedAs","operators","CRM_NEXT_ACTIVITY_FILTER_HEADER"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEnD,SAASC,WAAW,QAAQ,kBAAc;AAC1C,SAASC,uBAAuB,EAAEC,gBAAgB,QAAQ,gCAA4B;AAEtF;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAO;QACLC,OAAOJ;QACPK,YAAY;QACZC,MAAM;QACNC,UAAU;QACVC,MAAM;QACNC,UAAU;QACVC,YAAY;QACZC,aAAa,CAACC,QAAiBC;YAC7B,MAAMC,KAAKb,iBAAiBY;YAC5B,OAAOC,OAAO,OAAO,OAAO,IAAIC,KAAKD;QACvC;QACAE,YAAY,CAAC,EAAEH,GAAG,EAAuC;YACvD,MAAMC,KAAKb,iBAAiBY;YAC5B,IAAIC,OAAO,MAAM;gBACf,qBACE,KAAChB;oBAAWmB,SAAQ;oBAAUC,OAAM;8BACjC;;YAGP;YACA,qBACE,KAACrB;gBAAQsB,OAAO,IAAIJ,KAAKD,IAAIM,cAAc;0BACzC,cAAA,KAACC;8BACC,cAAA,KAACtB;wBAAYuB,MAAM;4BAAEC,QAAQ;4BAAQC,SAASV;wBAAG;wBAAGX,OAAOA;wBAAOc,SAAQ;;;;QAIlF;IACF;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMQ,iCAAkD;IAC7DC,QAAQ1B;IACR2B,MAAM;IACNC,MAAM5B;IACN6B,UAAU;IACVC,UAAU;IACVC,WAAW;QAAC;KAAU;AACxB,EAAC;AAED,wCAAwC,GACxC,OAAO,MAAMC,kCAAkC,gBAAe"}
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 { Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { TaskDueText } from './task-cells'\nimport { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from '../model/crm-next-activity'\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\nexport {\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n CRM_NEXT_ACTIVITY_FILTER_HEADER,\n} from '../model/crm-next-activity'\n"],"names":["Tooltip","Typography","TaskDueText","CRM_NEXT_ACTIVITY_FIELD","readNextTaskAtMs","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","CRM_NEXT_ACTIVITY_FILTER_HEADER"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEnD,SAASC,WAAW,QAAQ,kBAAc;AAC1C,SAASC,uBAAuB,EAAEC,gBAAgB,QAAQ,gCAA4B;AAEtF;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAO;QACLC,OAAOJ;QACPK,YAAY;QACZC,MAAM;QACNC,UAAU;QACVC,MAAM;QACNC,UAAU;QACVC,YAAY;QACZC,aAAa,CAACC,QAAiBC;YAC7B,MAAMC,KAAKb,iBAAiBY;YAC5B,OAAOC,OAAO,OAAO,OAAO,IAAIC,KAAKD;QACvC;QACAE,YAAY,CAAC,EAAEH,GAAG,EAAuC;YACvD,MAAMC,KAAKb,iBAAiBY;YAC5B,IAAIC,OAAO,MAAM;gBACf,qBACE,KAAChB;oBAAWmB,SAAQ;oBAAUC,OAAM;8BACjC;;YAGP;YACA,qBACE,KAACrB;gBAAQsB,OAAO,IAAIJ,KAAKD,IAAIM,cAAc;0BACzC,cAAA,KAACC;8BACC,cAAA,KAACtB;wBAAYuB,MAAM;4BAAEC,QAAQ;4BAAQC,SAASV;wBAAG;wBAAGX,OAAOA;wBAAOc,SAAQ;;;;QAIlF;IACF;AACF;AAEA,SACEQ,8BAA8B,EAC9BC,+BAA+B,QAC1B,gCAA4B"}
@@ -1,17 +1,5 @@
1
- import { type CrmLeadFields, type CrmLeadStatus, type CrmPicklist } from '@aglyn/aglyn';
2
- /**
3
- * The meanings a person sets by hand from a lead's status select: every one
4
- * but Qualified, which only a conversion sets (AGL-2608, AGL-3512).
5
- */
6
- export declare const LEAD_STATUS_HAND_SET: readonly CrmLeadStatus[];
7
- /** One choice of a lead status select: the org's label, and the meaning it sets. */
8
- export interface LeadStatusChoice {
9
- label: string;
10
- status: CrmLeadStatus;
11
- inactive: boolean;
12
- }
13
- /** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */
14
- export declare function leadStatusChoices(picklist: CrmPicklist, lead: Pick<CrmLeadFields, 'status' | 'statusLabel'>): LeadStatusChoice[];
1
+ import type { LeadStatusChoice } from '../model/lead-status-choices';
2
+ export { LEAD_STATUS_HAND_SET, type LeadStatusChoice, leadStatusChoices, } from '../model/lead-status-choices';
15
3
  /**
16
4
  * The select's items, by label (AGL-3512). An Unqualified value ends in "…"
17
5
  * because it opens the dialog that asks why; the lead's own value, when the
@@ -15,20 +15,8 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx } from "react/jsx-runtime";
18
- import { crmLeadStatusOptions } from "@aglyn/aglyn";
19
18
  import { ListItemText, MenuItem } from "@mui/material";
20
- /**
21
- * The meanings a person sets by hand from a lead's status select: every one
22
- * but Qualified, which only a conversion sets (AGL-2608, AGL-3512).
23
- */ export const LEAD_STATUS_HAND_SET = [
24
- 'new',
25
- 'nurturing',
26
- 'working',
27
- 'unqualified'
28
- ];
29
- /** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */ export function leadStatusChoices(picklist, lead) {
30
- return crmLeadStatusOptions(picklist, LEAD_STATUS_HAND_SET, lead);
31
- }
19
+ export { LEAD_STATUS_HAND_SET, leadStatusChoices } from "../model/lead-status-choices.js";
32
20
  /**
33
21
  * The select's items, by label (AGL-3512). An Unqualified value ends in "…"
34
22
  * because it opens the dialog that asks why; the lead's own value, when the
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-status-options.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type CrmLeadFields,\n type CrmLeadStatus,\n type CrmPicklist,\n crmLeadStatusOptions,\n} from '@aglyn/aglyn'\nimport { ListItemText, MenuItem } from '@mui/material'\n\n/**\n * The meanings a person sets by hand from a lead's status select: every one\n * but Qualified, which only a conversion sets (AGL-2608, AGL-3512).\n */\nexport const LEAD_STATUS_HAND_SET: readonly CrmLeadStatus[] = [\n 'new',\n 'nurturing',\n 'working',\n 'unqualified',\n]\n\n/** One choice of a lead status select: the org's label, and the meaning it sets. */\nexport interface LeadStatusChoice {\n label: string\n status: CrmLeadStatus\n inactive: boolean\n}\n\n/** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */\nexport function leadStatusChoices(\n picklist: CrmPicklist,\n lead: Pick<CrmLeadFields, 'status' | 'statusLabel'>,\n): LeadStatusChoice[] {\n return crmLeadStatusOptions(picklist, LEAD_STATUS_HAND_SET, lead)\n}\n\n/**\n * The select's items, by label (AGL-3512). An Unqualified value ends in \"…\"\n * because it opens the dialog that asks why; the lead's own value, when the\n * list no longer offers it, is shown and not offered.\n */\nexport function leadStatusMenuItems(choices: readonly LeadStatusChoice[]) {\n return choices.map((choice) => (\n <MenuItem key={choice.label} value={choice.label} disabled={choice.inactive}>\n <ListItemText\n primary={choice.status === 'unqualified' ? `${choice.label}…` : choice.label}\n slotProps={choice.inactive ? { primary: { color: 'text.secondary' } } : undefined}\n />\n </MenuItem>\n ))\n}\n"],"names":["crmLeadStatusOptions","ListItemText","MenuItem","LEAD_STATUS_HAND_SET","leadStatusChoices","picklist","lead","leadStatusMenuItems","choices","map","choice","value","label","disabled","inactive","primary","status","slotProps","color","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAIEA,oBAAoB,QACf,eAAc;AACrB,SAASC,YAAY,EAAEC,QAAQ,QAAQ,gBAAe;AAEtD;;;CAGC,GACD,OAAO,MAAMC,uBAAiD;IAC5D;IACA;IACA;IACA;CACD,CAAA;AASD,4GAA4G,GAC5G,OAAO,SAASC,kBACdC,QAAqB,EACrBC,IAAmD;IAEnD,OAAON,qBAAqBK,UAAUF,sBAAsBG;AAC9D;AAEA;;;;CAIC,GACD,OAAO,SAASC,oBAAoBC,OAAoC;IACtE,OAAOA,QAAQC,GAAG,CAAC,CAACC,uBAClB,KAACR;YAA4BS,OAAOD,OAAOE,KAAK;YAAEC,UAAUH,OAAOI,QAAQ;sBACzE,cAAA,KAACb;gBACCc,SAASL,OAAOM,MAAM,KAAK,gBAAgB,GAAGN,OAAOE,KAAK,CAAC,CAAC,CAAC,GAAGF,OAAOE,KAAK;gBAC5EK,WAAWP,OAAOI,QAAQ,GAAG;oBAAEC,SAAS;wBAAEG,OAAO;oBAAiB;gBAAE,IAAIC;;WAH7DT,OAAOE,KAAK;AAO/B"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-status-options.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 { ListItemText, MenuItem } from '@mui/material'\nimport type { LeadStatusChoice } from '../model/lead-status-choices'\n\nexport {\n LEAD_STATUS_HAND_SET,\n type LeadStatusChoice,\n leadStatusChoices,\n} from '../model/lead-status-choices'\n\n/**\n * The select's items, by label (AGL-3512). An Unqualified value ends in \"…\"\n * because it opens the dialog that asks why; the lead's own value, when the\n * list no longer offers it, is shown and not offered.\n */\nexport function leadStatusMenuItems(choices: readonly LeadStatusChoice[]) {\n return choices.map((choice) => (\n <MenuItem key={choice.label} value={choice.label} disabled={choice.inactive}>\n <ListItemText\n primary={choice.status === 'unqualified' ? `${choice.label}…` : choice.label}\n slotProps={choice.inactive ? { primary: { color: 'text.secondary' } } : undefined}\n />\n </MenuItem>\n ))\n}\n"],"names":["ListItemText","MenuItem","LEAD_STATUS_HAND_SET","leadStatusChoices","leadStatusMenuItems","choices","map","choice","value","label","disabled","inactive","primary","status","slotProps","color","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,YAAY,EAAEC,QAAQ,QAAQ,gBAAe;AAGtD,SACEC,oBAAoB,EAEpBC,iBAAiB,QACZ,kCAA8B;AAErC;;;;CAIC,GACD,OAAO,SAASC,oBAAoBC,OAAoC;IACtE,OAAOA,QAAQC,GAAG,CAAC,CAACC,uBAClB,KAACN;YAA4BO,OAAOD,OAAOE,KAAK;YAAEC,UAAUH,OAAOI,QAAQ;sBACzE,cAAA,KAACX;gBACCY,SAASL,OAAOM,MAAM,KAAK,gBAAgB,GAAGN,OAAOE,KAAK,CAAC,CAAC,CAAC,GAAGF,OAAOE,KAAK;gBAC5EK,WAAWP,OAAOI,QAAQ,GAAG;oBAAEC,SAAS;wBAAEG,OAAO;oBAAiB;gBAAE,IAAIC;;WAH7DT,OAAOE,KAAK;AAO/B"}
@@ -1,5 +1,4 @@
1
- /** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */
2
- export declare const UNQUALIFY_REASON_MAX = 500;
1
+ export { UNQUALIFY_REASON_MAX } from '../model/lead-status-choices';
3
2
  export interface LeadUnqualifyDialogProps {
4
3
  open: boolean;
5
4
  onClose: () => void;
@@ -25,7 +25,8 @@ import { useEffect, useState } from "react";
25
25
  import { useCrmScope } from "../hooks/use-crm-scope.js";
26
26
  import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
27
27
  import { useLeadStatusPicklist } from "../hooks/use-lead-status-picklist.js";
28
- /** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */ export const UNQUALIFY_REASON_MAX = 500;
28
+ import { UNQUALIFY_REASON_MAX } from "../model/lead-status-choices.js";
29
+ export { UNQUALIFY_REASON_MAX } from "../model/lead-status-choices.js";
29
30
  const REASON_MAX = UNQUALIFY_REASON_MAX;
30
31
  /**
31
32
  * Close a lead without converting it, with the reason why (AGL-2608).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-unqualify-dialog.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 { type CrmLeadFields, crmLeadStatusLabelFor, crmLeadStatusOptions } from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n} from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\n\n/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */\nexport const UNQUALIFY_REASON_MAX = 500\nconst REASON_MAX = UNQUALIFY_REASON_MAX\n\nexport interface LeadUnqualifyDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The lead's own site, or `null` at the organization level (AGL-3278).\n * The write is an org-scoped one-field update; the site is read for the\n * org root, which the mount answers without one.\n */\n hostId: string | null\n leadId: string\n /** How the lead reads in the title — its name, else its address. */\n leadLabel: string\n /**\n * The Unqualified value the lead is closed as, when the reader already\n * picked one (AGL-3512); the meaning's default label otherwise.\n */\n statusLabel?: string | null\n}\n\n/**\n * Close a lead without converting it, with the reason why (AGL-2608).\n *\n * The reason is REQUIRED. An unqualified lead with no reason is a row that\n * says only \"somebody gave up\", and the one thing a report on lost leads\n * wants to count is why. A client-direct write: `hosts/{hostId}/leads` is\n * not in the catch-all's update exclusions, so a site admin, editor or\n * author may update it, and nothing here needs the server.\n */\nexport function LeadUnqualifyDialog(props: LeadUnqualifyDialogProps) {\n const { open, onClose, hostId, leadId, leadLabel, statusLabel } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const [reason, setReason] = useState('')\n const [busy, setBusy] = useState(false)\n // The org's Unqualified values (AGL-3512): a choice only when it keeps several.\n const statuses = useLeadStatusPicklist(orgId)\n const closedAs = crmLeadStatusOptions(statuses.picklist, ['unqualified'])\n const [label, setLabel] = useState('')\n\n useEffect(() => {\n if (!open) return\n setReason('')\n setLabel(statusLabel || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'))\n // Seeded as the dialog opens; the list arriving later does not reset a pick.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, statusLabel])\n\n const submit = async () => {\n const trimmed = reason.trim()\n if (!trimmed) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setBusy(true)\n try {\n const fields: Required<Pick<CrmLeadFields, 'status' | 'statusLabel' | 'unqualifiedReason'>> = {\n status: 'unqualified',\n // The meaning and the org's label for it, always together (AGL-3512).\n statusLabel: label || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'),\n unqualifiedReason: trimmed.slice(0, REASON_MAX),\n }\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n // A rule may share by status (AGL-3336).\n followUpSharing('leads', [leadId])\n enqueueSnackbar(`Lead marked ${fields.statusLabel}`, { variant: 'success', persist: false })\n onClose()\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{`Unqualify ${leadLabel}?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'The lead stays on file and drops out of the open list. Say why, ' +\n 'so the reason can be counted later.'}\n </DialogContentText>\n <Stack spacing={2}>\n {closedAs.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={closedAs.some((option) => option.label === label) ? label : ''}\n onChange={(event) => setLabel(String(event.target.value))}\n fullWidth\n >\n {closedAs.map((option) => (\n <MenuItem key={option.label} value={option.label}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Reason\"\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n multiline\n minRows={2}\n fullWidth\n autoFocus\n slotProps={{ htmlInput: { maxLength: REASON_MAX } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"warning\"\n onClick={() => void submit()}\n disabled={busy || !reason.trim()}\n >\n {'Unqualify'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadUnqualifyDialog.displayName = 'LeadUnqualifyDialog'\n\nexport default LeadUnqualifyDialog\n"],"names":["crmLeadStatusLabelFor","crmLeadStatusOptions","useSnackbar","useFirestore","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","useCrmSharingFollowUp","useLeadStatusPicklist","UNQUALIFY_REASON_MAX","REASON_MAX","LeadUnqualifyDialog","props","open","onClose","hostId","leadId","leadLabel","statusLabel","orgId","followUpSharing","firestore","enqueueSnackbar","reason","setReason","busy","setBusy","statuses","closedAs","picklist","label","setLabel","submit","trimmed","trim","variant","persist","fields","status","unqualifiedReason","slice","updatedAt","error","Error","message","maxWidth","fullWidth","sx","mb","spacing","length","select","size","value","some","option","onChange","event","String","target","map","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","onClick","disabled","color","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-unqualify-dialog.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 { type CrmLeadFields, crmLeadStatusLabelFor, crmLeadStatusOptions } from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n} from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\nimport { UNQUALIFY_REASON_MAX } from '../model/lead-status-choices'\n\nexport { UNQUALIFY_REASON_MAX } from '../model/lead-status-choices'\n\nconst REASON_MAX = UNQUALIFY_REASON_MAX\n\nexport interface LeadUnqualifyDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The lead's own site, or `null` at the organization level (AGL-3278).\n * The write is an org-scoped one-field update; the site is read for the\n * org root, which the mount answers without one.\n */\n hostId: string | null\n leadId: string\n /** How the lead reads in the title — its name, else its address. */\n leadLabel: string\n /**\n * The Unqualified value the lead is closed as, when the reader already\n * picked one (AGL-3512); the meaning's default label otherwise.\n */\n statusLabel?: string | null\n}\n\n/**\n * Close a lead without converting it, with the reason why (AGL-2608).\n *\n * The reason is REQUIRED. An unqualified lead with no reason is a row that\n * says only \"somebody gave up\", and the one thing a report on lost leads\n * wants to count is why. A client-direct write: `hosts/{hostId}/leads` is\n * not in the catch-all's update exclusions, so a site admin, editor or\n * author may update it, and nothing here needs the server.\n */\nexport function LeadUnqualifyDialog(props: LeadUnqualifyDialogProps) {\n const { open, onClose, hostId, leadId, leadLabel, statusLabel } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const [reason, setReason] = useState('')\n const [busy, setBusy] = useState(false)\n // The org's Unqualified values (AGL-3512): a choice only when it keeps several.\n const statuses = useLeadStatusPicklist(orgId)\n const closedAs = crmLeadStatusOptions(statuses.picklist, ['unqualified'])\n const [label, setLabel] = useState('')\n\n useEffect(() => {\n if (!open) return\n setReason('')\n setLabel(statusLabel || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'))\n // Seeded as the dialog opens; the list arriving later does not reset a pick.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, statusLabel])\n\n const submit = async () => {\n const trimmed = reason.trim()\n if (!trimmed) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setBusy(true)\n try {\n const fields: Required<Pick<CrmLeadFields, 'status' | 'statusLabel' | 'unqualifiedReason'>> = {\n status: 'unqualified',\n // The meaning and the org's label for it, always together (AGL-3512).\n statusLabel: label || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'),\n unqualifiedReason: trimmed.slice(0, REASON_MAX),\n }\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n // A rule may share by status (AGL-3336).\n followUpSharing('leads', [leadId])\n enqueueSnackbar(`Lead marked ${fields.statusLabel}`, { variant: 'success', persist: false })\n onClose()\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{`Unqualify ${leadLabel}?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'The lead stays on file and drops out of the open list. Say why, ' +\n 'so the reason can be counted later.'}\n </DialogContentText>\n <Stack spacing={2}>\n {closedAs.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={closedAs.some((option) => option.label === label) ? label : ''}\n onChange={(event) => setLabel(String(event.target.value))}\n fullWidth\n >\n {closedAs.map((option) => (\n <MenuItem key={option.label} value={option.label}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Reason\"\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n multiline\n minRows={2}\n fullWidth\n autoFocus\n slotProps={{ htmlInput: { maxLength: REASON_MAX } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"warning\"\n onClick={() => void submit()}\n disabled={busy || !reason.trim()}\n >\n {'Unqualify'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadUnqualifyDialog.displayName = 'LeadUnqualifyDialog'\n\nexport default LeadUnqualifyDialog\n"],"names":["crmLeadStatusLabelFor","crmLeadStatusOptions","useSnackbar","useFirestore","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","useCrmSharingFollowUp","useLeadStatusPicklist","UNQUALIFY_REASON_MAX","REASON_MAX","LeadUnqualifyDialog","props","open","onClose","hostId","leadId","leadLabel","statusLabel","orgId","followUpSharing","firestore","enqueueSnackbar","reason","setReason","busy","setBusy","statuses","closedAs","picklist","label","setLabel","submit","trimmed","trim","variant","persist","fields","status","unqualifiedReason","slice","updatedAt","error","Error","message","maxWidth","fullWidth","sx","mb","spacing","length","select","size","value","some","option","onChange","event","String","target","map","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","onClick","disabled","color","displayName"],"mappings":"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"}
@@ -48,6 +48,24 @@ export declare function AutoCreateCompaniesCard(props: AutoCreateCompaniesCardPr
48
48
  export declare namespace AutoCreateCompaniesCard {
49
49
  var displayName: string;
50
50
  }
51
+ /**
52
+ * "Default sharing for new records" (AGL-3662) — what a new contact,
53
+ * company, deal or task starts shared with, set apart from the org's
54
+ * defaults for new datasets and media.
55
+ *
56
+ * Changes nothing that already exists: narrowing an address book from a
57
+ * select would hide records from sites already working them, with no
58
+ * confirmation. Each record's own sharing is where that is decided.
59
+ *
60
+ * Reads `crmDefaultScopeOf`, the same reader the capture door and every CRM
61
+ * creator stamp from, so the control cannot show one thing while records
62
+ * get another — including for an org that has never set it, which inherits
63
+ * the dataset default it shared before the split.
64
+ */
65
+ export declare function DefaultRecordSharingCard(props: AutoCreateCompaniesCardProps): import("react").JSX.Element;
66
+ export declare namespace DefaultRecordSharingCard {
67
+ var displayName: string;
68
+ }
51
69
  export interface AssignmentCardProps {
52
70
  /** The site the section is read under, or `null` at the organization level. */
53
71
  hostId: string | null;
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
- import { canManageOrg, CRM_ASSIGNMENT_RULES_MAX, CRM_ASSIGNMENT_RULES_PATH, CRM_AUTO_CREATE_COMPANIES_PATH, CRM_ROUND_ROBIN_POOL_MAX, CRM_ROUND_ROBIN_POOL_PATH, crmHostDefaultOwnerSegments, describeAssignmentRule, orgAutoCreatesCompanies, pluginDocsHelp, readCrmAssignmentSettings, roundRobinOrder, crmMemberPickerLabel } from "@aglyn/aglyn";
18
+ import { canManageOrg, CRM_ASSIGNMENT_RULES_MAX, CRM_ASSIGNMENT_RULES_PATH, CRM_AUTO_CREATE_COMPANIES_PATH, CRM_DEFAULT_RECORD_SCOPE_PATH, CRM_ROUND_ROBIN_POOL_MAX, CRM_ROUND_ROBIN_POOL_PATH, crmDefaultScopeOf, crmHostDefaultOwnerSegments, describeAssignmentRule, orgAutoCreatesCompanies, pluginDocsHelp, readCrmAssignmentSettings, roundRobinOrder, crmMemberPickerLabel } from "@aglyn/aglyn";
19
19
  import { mdiArrowDown, mdiArrowUp, mdiDeleteOutline } from "@aglyn/shared-data-mdi";
20
20
  import { CardDisplay, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
21
21
  import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
@@ -158,6 +158,98 @@ import SharingRulesCard from "./sharing-rules-card.js";
158
158
  });
159
159
  }
160
160
  AutoCreateCompaniesCard.displayName = 'AutoCreateCompaniesCard';
161
+ /**
162
+ * "Default sharing for new records" (AGL-3662) — what a new contact,
163
+ * company, deal or task starts shared with, set apart from the org's
164
+ * defaults for new datasets and media.
165
+ *
166
+ * Changes nothing that already exists: narrowing an address book from a
167
+ * select would hide records from sites already working them, with no
168
+ * confirmation. Each record's own sharing is where that is decided.
169
+ *
170
+ * Reads `crmDefaultScopeOf`, the same reader the capture door and every CRM
171
+ * creator stamp from, so the control cannot show one thing while records
172
+ * get another — including for an org that has never set it, which inherits
173
+ * the dataset default it shared before the split.
174
+ */ export function DefaultRecordSharingCard(props) {
175
+ var _crmDefaultScopeOf;
176
+ const { hostId, org } = props;
177
+ const firestore = useFirestore();
178
+ const { enqueueSnackbar } = useSnackbar();
179
+ const { orgId, ready: scopeReady } = useCrmScope({
180
+ hostId,
181
+ org
182
+ });
183
+ const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId);
184
+ const stored = (_crmDefaultScopeOf = crmDefaultScopeOf(org)) != null ? _crmDefaultScopeOf : 'host';
185
+ const [value, setValue] = useState(stored);
186
+ const [busy, setBusy] = useState(false);
187
+ useEffect(()=>setValue(stored), [
188
+ stored
189
+ ]);
190
+ const handleChange = async (next)=>{
191
+ if (!orgId || !canManage) return;
192
+ setValue(next);
193
+ setBusy(true);
194
+ try {
195
+ await updateDoc(doc(firestore, 'orgs', orgId), {
196
+ [CRM_DEFAULT_RECORD_SCOPE_PATH]: next
197
+ });
198
+ enqueueSnackbar('Default sharing updated', {
199
+ variant: 'success',
200
+ persist: false
201
+ });
202
+ } catch (error) {
203
+ console.error(error);
204
+ setValue(stored);
205
+ enqueueSnackbar('The setting could not be saved', {
206
+ variant: 'error',
207
+ allowDuplicate: true
208
+ });
209
+ } finally{
210
+ setBusy(false);
211
+ }
212
+ };
213
+ const ready = scopeReady && roleReady;
214
+ return /*#__PURE__*/ _jsx(CardDisplay, {
215
+ header: 'Default sharing for new records',
216
+ help: pluginDocsHelp('crmSettings'),
217
+ contentGutterX: true,
218
+ contentGutterY: true,
219
+ children: /*#__PURE__*/ _jsxs(Stack, {
220
+ spacing: 1,
221
+ sx: {
222
+ maxWidth: 480
223
+ },
224
+ children: [
225
+ /*#__PURE__*/ _jsxs(TextField, {
226
+ select: true,
227
+ size: "small",
228
+ label: "New contacts, companies, deals and tasks are shared with",
229
+ value: value,
230
+ disabled: !ready || !canManage || busy,
231
+ onChange: (event)=>void handleChange(event.target.value),
232
+ helperText: 'Only affects records created from now on. Datasets and media ' + 'have defaults of their own, on the organization Data and ' + 'Media pages.',
233
+ children: [
234
+ /*#__PURE__*/ _jsx(MenuItem, {
235
+ value: "org",
236
+ children: 'All sites'
237
+ }),
238
+ /*#__PURE__*/ _jsx(MenuItem, {
239
+ value: "host",
240
+ children: 'The site they came in on, and its consent group'
241
+ })
242
+ ]
243
+ }),
244
+ /*#__PURE__*/ _jsx(ManagersOnlyNote, {
245
+ ready: ready,
246
+ canManage: canManage
247
+ })
248
+ ]
249
+ })
250
+ });
251
+ }
252
+ DefaultRecordSharingCard.displayName = 'DefaultRecordSharingCard';
161
253
  /** The caption every card shows a member who may read but not change it. */ function ManagersOnlyNote(props) {
162
254
  return props.ready && !props.canManage ? /*#__PURE__*/ _jsx(Typography, {
163
255
  variant: "caption",
@@ -749,6 +841,10 @@ RoundRobinCard.displayName = 'RoundRobinCard';
749
841
  return /*#__PURE__*/ _jsxs(Stack, {
750
842
  spacing: 3,
751
843
  children: [
844
+ /*#__PURE__*/ _jsx(DefaultRecordSharingCard, {
845
+ hostId: hostId,
846
+ org: org
847
+ }),
752
848
  /*#__PURE__*/ _jsx(AutoCreateCompaniesCard, {
753
849
  hostId: hostId,
754
850
  org: org