@aglyn/plugins-crm 1.0.0-beta.246 → 1.0.0-beta.249
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/contact-custom-columns.d.ts +15 -6
- package/src/lib/components/contact-custom-columns.js +27 -7
- package/src/lib/components/contact-custom-columns.js.map +1 -1
- package/src/lib/components/contact-field-drawer.d.ts +2 -0
- package/src/lib/components/contact-field-drawer.js +25 -1
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-custom-field-control.d.ts +4 -2
- package/src/lib/components/crm-custom-field-control.js +53 -1
- package/src/lib/components/crm-custom-field-control.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -2
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +2 -0
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +7 -0
- package/src/lib/constants/contact-filters.js +8 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +2 -0
- package/src/lib/declarations.console-server.js +5 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/crm-picklist-setup-content.d.ts +121 -0
- package/src/lib/model/crm-picklist-setup-content.js +262 -0
- package/src/lib/model/crm-picklist-setup-content.js.map +1 -0
- package/src/lib/model/crm-record-routes.js +6 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-setup-content.d.ts +4 -1
- package/src/lib/model/crm-setup-content.js +15 -4
- package/src/lib/model/crm-setup-content.js.map +1 -1
- package/src/lib/server/crm-setup-ai-capabilities.d.ts +13 -0
- package/src/lib/server/crm-setup-ai-capabilities.js +94 -2
- package/src/lib/server/crm-setup-ai-capabilities.js.map +1 -1
- package/src/lib/server/crm-setup-writers.server.d.ts +28 -0
- package/src/lib/server/crm-setup-writers.server.js +181 -4
- package/src/lib/server/crm-setup-writers.server.js.map +1 -1
- package/src/lib/server/picklist-regroup.d.ts +30 -0
- package/src/lib/server/picklist-regroup.js +61 -0
- package/src/lib/server/picklist-regroup.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +2 -14
- package/src/lib/server/picklist-values.js +3 -40
- package/src/lib/server/picklist-values.js.map +1 -1
- package/src/lib/transfer/common.d.ts +4 -2
- package/src/lib/transfer/common.js +14 -3
- package/src/lib/transfer/common.js.map +1 -1
- package/src/lib/transfer/fields.js +10 -1
- package/src/lib/transfer/fields.js.map +1 -1
- package/src/lib/transfer/pipelines.js +4 -2
- package/src/lib/transfer/pipelines.js.map +1 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ // Modules by their own paths, never the `@aglyn/aglyn` barrel: the console
|
|
18
|
+
// server's boot reaches this file through the CRM's setup writers.
|
|
19
|
+
import { fieldDefinitionsForObject } from "@aglyn/aglyn/app-utils/contact-custom-fields";
|
|
20
|
+
import { CRM_FIELD_OBJECT_LABELS, CRM_FIELD_OBJECTS, CRM_PICKLIST_IDS, CRM_PICKLIST_OBJECT_LABELS, crmPicklistDefinition, crmPicklistValueByLabel, isCrmFieldObject, normalizeContactFieldKey, normalizeCrmPicklistLabel } from "@aglyn/aglyn/app-utils/crm";
|
|
21
|
+
import { CRM_FIELD_OPTION_MAX_LENGTH, CRM_FIELD_OPTIONS_MAX } from "./crm-setup-content.js";
|
|
22
|
+
import { addPicklistValue, picklistAddableMeanings, picklistMeaningLabel } from "./picklist-values.js";
|
|
23
|
+
/**
|
|
24
|
+
* VALUES ADDED TO A CRM PICKLIST (AGL-3756): what a person adds in CRM →
|
|
25
|
+
* Fields with a picklist's **Add value**, or by typing another line into a
|
|
26
|
+
* Choice field's choices — as pure rules, for Aglyn AI's build.
|
|
27
|
+
*
|
|
28
|
+
* Two kinds of list take values, and the console extends both:
|
|
29
|
+
*
|
|
30
|
+
* - every STANDARD picklist in `CRM_PICKLIST_DEFINITIONS` — lead status,
|
|
31
|
+
* lead source, a deal's type and contact role, a company's type,
|
|
32
|
+
* industry, rating and ownership, the salutation, and a task's status,
|
|
33
|
+
* priority, type and subject. An added value is `addPicklistValue`'s, the
|
|
34
|
+
* console's own move: a lead status or task status names its MEANING
|
|
35
|
+
* (never one the platform reserves, such as Qualified), a lead source may
|
|
36
|
+
* name its GROUP (Inbound or Outbound);
|
|
37
|
+
* - a custom CHOICE field's options, on the object it describes.
|
|
38
|
+
*
|
|
39
|
+
* ONLY ADDS. Nothing is removed, renamed, reordered, deactivated or
|
|
40
|
+
* reactivated: a value already in the list, in any capitals, is left as it
|
|
41
|
+
* is — an inactive one included, which the console would have the person
|
|
42
|
+
* activate, so it is reported rather than touched. Asked twice, nothing is
|
|
43
|
+
* added twice.
|
|
44
|
+
*/ /** The resource the CRM's picklist values writer is registered under. */ export const CRM_PICKLIST_VALUES_SETUP_RESOURCE = 'crm-picklist-values';
|
|
45
|
+
/** The `list` that names a custom Choice field rather than a standard picklist. */ export const CRM_CUSTOM_CHOICE_LIST = 'custom';
|
|
46
|
+
/** The most values one ask adds. */ export const CRM_PICKLIST_VALUES_SETUP_MAX = 50;
|
|
47
|
+
/** Every `list` an ask may name: the standard picklists, then a custom Choice field. */ export const CRM_PICKLIST_VALUES_LISTS = [
|
|
48
|
+
...CRM_PICKLIST_IDS,
|
|
49
|
+
CRM_CUSTOM_CHOICE_LIST
|
|
50
|
+
];
|
|
51
|
+
function text(value) {
|
|
52
|
+
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
53
|
+
}
|
|
54
|
+
const lower = (value)=>value.trim().toLowerCase();
|
|
55
|
+
/** A meaning named by its id or by how it reads ("Nurturing"), or `null`. */ function meaningNamed(definition, asked) {
|
|
56
|
+
var _find, _definition_meanings;
|
|
57
|
+
const key = lower(asked);
|
|
58
|
+
return (_find = ((_definition_meanings = definition.meanings) != null ? _definition_meanings : []).find((meaning)=>lower(meaning) === key || lower(picklistMeaningLabel(definition, meaning)) === key)) != null ? _find : null;
|
|
59
|
+
}
|
|
60
|
+
/** A group named by its id or its label ("Inbound"), or `null`. */ function groupNamed(definition, asked) {
|
|
61
|
+
var _ref, _definition_groups;
|
|
62
|
+
var _find;
|
|
63
|
+
const key = lower(asked);
|
|
64
|
+
return (_ref = (_find = ((_definition_groups = definition.groups) != null ? _definition_groups : []).find((group)=>lower(group.id) === key || lower(group.label) === key)) == null ? void 0 : _find.id) != null ? _ref : null;
|
|
65
|
+
}
|
|
66
|
+
/** An ask's content, read and judged. Pure. */ export function readCrmPicklistValuesSetup(content) {
|
|
67
|
+
const problems = [];
|
|
68
|
+
const list = text(content['list']);
|
|
69
|
+
const definition = list === CRM_CUSTOM_CHOICE_LIST ? null : crmPicklistDefinition(list);
|
|
70
|
+
if (list !== CRM_CUSTOM_CHOICE_LIST && !definition) {
|
|
71
|
+
problems.push(`A list is one of ${CRM_PICKLIST_VALUES_LISTS.join(', ')}.`);
|
|
72
|
+
}
|
|
73
|
+
let target = definition ? {
|
|
74
|
+
kind: 'picklist',
|
|
75
|
+
id: definition.id
|
|
76
|
+
} : null;
|
|
77
|
+
if (list === CRM_CUSTOM_CHOICE_LIST) {
|
|
78
|
+
const object = content['object'];
|
|
79
|
+
const field = text(content['field']);
|
|
80
|
+
if (!isCrmFieldObject(object)) problems.push(`A custom Choice field is on one of ${CRM_FIELD_OBJECTS.join(', ')}.`);
|
|
81
|
+
if (!field) problems.push('Name the custom Choice field the values go on.');
|
|
82
|
+
if (isCrmFieldObject(object) && field) target = {
|
|
83
|
+
kind: 'custom',
|
|
84
|
+
object,
|
|
85
|
+
field
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const maxLength = (target == null ? void 0 : target.kind) === 'custom' ? CRM_FIELD_OPTION_MAX_LENGTH : 120;
|
|
89
|
+
const listed = Array.isArray(content['values']) ? content['values'] : [];
|
|
90
|
+
const values = [];
|
|
91
|
+
for (const entry of listed){
|
|
92
|
+
if (typeof entry !== 'string') {
|
|
93
|
+
problems.push('Each value is a name.');
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
const label = normalizeCrmPicklistLabel(entry).slice(0, maxLength);
|
|
97
|
+
if (!label || values.some((one)=>lower(one) === lower(label))) continue;
|
|
98
|
+
values.push(label);
|
|
99
|
+
}
|
|
100
|
+
if (!values.length) problems.push('Name at least one value to add.');
|
|
101
|
+
if (values.length > CRM_PICKLIST_VALUES_SETUP_MAX) {
|
|
102
|
+
problems.push(`One ask adds at most ${CRM_PICKLIST_VALUES_SETUP_MAX} values.`);
|
|
103
|
+
}
|
|
104
|
+
let meaning = null;
|
|
105
|
+
let group = null;
|
|
106
|
+
if (definition) {
|
|
107
|
+
var _definition_meanings, _definition_groups;
|
|
108
|
+
const addable = picklistAddableMeanings(definition);
|
|
109
|
+
if ((_definition_meanings = definition.meanings) == null ? void 0 : _definition_meanings.length) {
|
|
110
|
+
var _definition_reservedMeanings;
|
|
111
|
+
const askedMeaning = text(content['meaning']);
|
|
112
|
+
meaning = askedMeaning ? meaningNamed(definition, askedMeaning) : null;
|
|
113
|
+
const names = addable.map((one)=>picklistMeaningLabel(definition, one)).join(', ');
|
|
114
|
+
if (meaning && ((_definition_reservedMeanings = definition.reservedMeanings) == null ? void 0 : _definition_reservedMeanings.includes(meaning))) {
|
|
115
|
+
problems.push(`Only the platform sets ${picklistMeaningLabel(definition, meaning)}; an added ${definition.label.toLowerCase()} means one of: ${names}.`);
|
|
116
|
+
} else if (!meaning) {
|
|
117
|
+
problems.push(`Each ${definition.label.toLowerCase()} value means one of: ${names}. Say which.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const askedGroup = text(content['group']);
|
|
121
|
+
if (askedGroup && ((_definition_groups = definition.groups) == null ? void 0 : _definition_groups.length)) {
|
|
122
|
+
group = groupNamed(definition, askedGroup);
|
|
123
|
+
if (!group) problems.push(`A ${definition.label.toLowerCase()} group is one of: ${definition.groups.map((one)=>one.label).join(', ')}.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (problems.length || !target) return {
|
|
127
|
+
ok: false,
|
|
128
|
+
problems: problems.length ? problems : [
|
|
129
|
+
'Name a list.'
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
value: {
|
|
135
|
+
target,
|
|
136
|
+
values,
|
|
137
|
+
meaning,
|
|
138
|
+
group
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** The writer's check: whether the content is an ask, with the list and values it adds. Pure. */ export function checkCrmPicklistValuesSetup(content) {
|
|
143
|
+
const read = readCrmPicklistValuesSetup(content);
|
|
144
|
+
if (read.ok === false) return {
|
|
145
|
+
ok: false,
|
|
146
|
+
problems: read.problems
|
|
147
|
+
};
|
|
148
|
+
const { target, values } = read.value;
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
facts: target.kind === 'picklist' ? {
|
|
152
|
+
list: target.id,
|
|
153
|
+
values
|
|
154
|
+
} : {
|
|
155
|
+
list: CRM_CUSTOM_CHOICE_LIST,
|
|
156
|
+
object: target.object,
|
|
157
|
+
field: target.field,
|
|
158
|
+
values
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The values a standard picklist gains, as the console's Add value adds them
|
|
164
|
+
* one by one at the end of the list. Pure. A value the list holds, in any
|
|
165
|
+
* capitals, is left as it is; the list's ceiling refuses the whole ask
|
|
166
|
+
* rather than adding part of it.
|
|
167
|
+
*/ export function planCrmPicklistValues(definition, picklist, setup) {
|
|
168
|
+
let next = picklist;
|
|
169
|
+
const outcome = {
|
|
170
|
+
added: [],
|
|
171
|
+
existing: [],
|
|
172
|
+
inactive: []
|
|
173
|
+
};
|
|
174
|
+
for (const label of setup.values){
|
|
175
|
+
const held = crmPicklistValueByLabel(next, label);
|
|
176
|
+
if (held) {
|
|
177
|
+
;
|
|
178
|
+
(held.active ? outcome.existing : outcome.inactive).push(held.label);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const move = addPicklistValue(definition, next, label, {
|
|
182
|
+
group: setup.group,
|
|
183
|
+
meaning: setup.meaning
|
|
184
|
+
});
|
|
185
|
+
if (move.ok === false) return {
|
|
186
|
+
ok: false,
|
|
187
|
+
error: move.error
|
|
188
|
+
};
|
|
189
|
+
next = move.picklist;
|
|
190
|
+
outcome.added.push(label);
|
|
191
|
+
}
|
|
192
|
+
return _extends({
|
|
193
|
+
ok: true,
|
|
194
|
+
picklist: next
|
|
195
|
+
}, outcome);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The choices a custom Choice field gains, appended as the drawer appends a
|
|
199
|
+
* typed line. Pure. The field is the active one on the object with that
|
|
200
|
+
* label or key, in any capitals; a choice it holds in any capitals is left
|
|
201
|
+
* as it is, and the drawer's 50-choice ceiling refuses the whole ask.
|
|
202
|
+
*/ export function planCrmCustomChoiceValues(setup, definitions) {
|
|
203
|
+
var _match_options;
|
|
204
|
+
const { object, field } = setup.target;
|
|
205
|
+
const key = normalizeContactFieldKey(field);
|
|
206
|
+
const noun = CRM_FIELD_OBJECT_LABELS[object].toLowerCase();
|
|
207
|
+
const match = fieldDefinitionsForObject(definitions, object).find((one)=>{
|
|
208
|
+
var _one_label;
|
|
209
|
+
return !one.retiredAt && (lower(String((_one_label = one.label) != null ? _one_label : '')) === lower(field) || key !== null && one.key === key);
|
|
210
|
+
});
|
|
211
|
+
if (!match) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
status: 404,
|
|
215
|
+
error: `There is no custom field “${field}” on ${noun}. Add it as a Choice field first.`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (match.type !== 'select') {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
status: 409,
|
|
222
|
+
error: `“${match.label}” on ${noun} is not a Choice field, so it has no choices to add to.`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const options = [
|
|
226
|
+
...(_match_options = match.options) != null ? _match_options : []
|
|
227
|
+
];
|
|
228
|
+
const outcome = {
|
|
229
|
+
added: [],
|
|
230
|
+
existing: [],
|
|
231
|
+
inactive: []
|
|
232
|
+
};
|
|
233
|
+
for (const label of setup.values){
|
|
234
|
+
const held = options.find((one)=>lower(one) === lower(label));
|
|
235
|
+
if (held) {
|
|
236
|
+
outcome.existing.push(held);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
options.push(label);
|
|
240
|
+
outcome.added.push(label);
|
|
241
|
+
}
|
|
242
|
+
if (options.length > CRM_FIELD_OPTIONS_MAX) {
|
|
243
|
+
var _ref;
|
|
244
|
+
var _match_options1;
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
status: 409,
|
|
248
|
+
error: `“${match.label}” holds at most ${CRM_FIELD_OPTIONS_MAX} choices; it has ${(_ref = (_match_options1 = match.options) == null ? void 0 : _match_options1.length) != null ? _ref : 0}.`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return _extends({
|
|
252
|
+
ok: true,
|
|
253
|
+
id: match.$id,
|
|
254
|
+
label: match.label,
|
|
255
|
+
options
|
|
256
|
+
}, outcome);
|
|
257
|
+
}
|
|
258
|
+
/** Where a picklist's values are kept, for a refusal and a record's name: "Lead status (Leads)". */ export function crmPicklistValuesName(definition) {
|
|
259
|
+
return `${definition.label} (${CRM_PICKLIST_OBJECT_LABELS[definition.object]})`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//# sourceMappingURL=crm-picklist-setup-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-picklist-setup-content.ts"],"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// Modules by their own paths, never the `@aglyn/aglyn` barrel: the console\n// server's boot reaches this file through the CRM's setup writers.\nimport { fieldDefinitionsForObject } from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport {\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n CRM_PICKLIST_IDS,\n CRM_PICKLIST_OBJECT_LABELS,\n type CrmFieldObject,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n crmPicklistDefinition,\n crmPicklistValueByLabel,\n isCrmFieldObject,\n normalizeContactFieldKey,\n normalizeCrmPicklistLabel,\n} from '@aglyn/aglyn/app-utils/crm'\nimport type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n CRM_FIELD_OPTION_MAX_LENGTH,\n CRM_FIELD_OPTIONS_MAX,\n type CrmFieldDefinitionDoc,\n} from './crm-setup-content'\nimport { addPicklistValue, picklistAddableMeanings, picklistMeaningLabel } from './picklist-values'\n\n/**\n * VALUES ADDED TO A CRM PICKLIST (AGL-3756): what a person adds in CRM →\n * Fields with a picklist's **Add value**, or by typing another line into a\n * Choice field's choices — as pure rules, for Aglyn AI's build.\n *\n * Two kinds of list take values, and the console extends both:\n *\n * - every STANDARD picklist in `CRM_PICKLIST_DEFINITIONS` — lead status,\n * lead source, a deal's type and contact role, a company's type,\n * industry, rating and ownership, the salutation, and a task's status,\n * priority, type and subject. An added value is `addPicklistValue`'s, the\n * console's own move: a lead status or task status names its MEANING\n * (never one the platform reserves, such as Qualified), a lead source may\n * name its GROUP (Inbound or Outbound);\n * - a custom CHOICE field's options, on the object it describes.\n *\n * ONLY ADDS. Nothing is removed, renamed, reordered, deactivated or\n * reactivated: a value already in the list, in any capitals, is left as it\n * is — an inactive one included, which the console would have the person\n * activate, so it is reported rather than touched. Asked twice, nothing is\n * added twice.\n */\n\n/** The resource the CRM's picklist values writer is registered under. */\nexport const CRM_PICKLIST_VALUES_SETUP_RESOURCE = 'crm-picklist-values'\n\n/** The `list` that names a custom Choice field rather than a standard picklist. */\nexport const CRM_CUSTOM_CHOICE_LIST = 'custom'\n\n/** The most values one ask adds. */\nexport const CRM_PICKLIST_VALUES_SETUP_MAX = 50\n\n/** Every `list` an ask may name: the standard picklists, then a custom Choice field. */\nexport const CRM_PICKLIST_VALUES_LISTS: readonly string[] = [...CRM_PICKLIST_IDS, CRM_CUSTOM_CHOICE_LIST]\n\n/** Which list an ask adds to. */\nexport type CrmPicklistValuesTarget =\n | { kind: 'picklist'; id: CrmPicklistId }\n | { kind: 'custom'; object: CrmFieldObject; field: string }\n\n/** One ask, read. */\nexport interface CrmPicklistValuesSetup {\n target: CrmPicklistValuesTarget\n /** The labels, normalized, without duplicates in any capitals, in the asked order. */\n values: string[]\n /** What each value means, on a picklist with meanings; `null` otherwise. */\n meaning: string | null\n /** The group each value is filed under, on a picklist with groups; `null` for none. */\n group: string | null\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n}\n\nconst lower = (value: string) => value.trim().toLowerCase()\n\n/** A meaning named by its id or by how it reads (\"Nurturing\"), or `null`. */\nfunction meaningNamed(definition: CrmPicklistDefinition, asked: string): string | null {\n const key = lower(asked)\n return (\n (definition.meanings ?? []).find(\n (meaning) => lower(meaning) === key || lower(picklistMeaningLabel(definition, meaning)) === key,\n ) ?? null\n )\n}\n\n/** A group named by its id or its label (\"Inbound\"), or `null`. */\nfunction groupNamed(definition: CrmPicklistDefinition, asked: string): string | null {\n const key = lower(asked)\n return (definition.groups ?? []).find((group) => lower(group.id) === key || lower(group.label) === key)?.id ?? null\n}\n\n/** An ask's content, read and judged. Pure. */\nexport function readCrmPicklistValuesSetup(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CrmPicklistValuesSetup } | { ok: false; problems: string[] } {\n const problems: string[] = []\n const list = text(content['list'])\n const definition = list === CRM_CUSTOM_CHOICE_LIST ? null : crmPicklistDefinition(list)\n if (list !== CRM_CUSTOM_CHOICE_LIST && !definition) {\n problems.push(`A list is one of ${CRM_PICKLIST_VALUES_LISTS.join(', ')}.`)\n }\n\n let target: CrmPicklistValuesTarget | null = definition ? { kind: 'picklist', id: definition.id as CrmPicklistId } : null\n if (list === CRM_CUSTOM_CHOICE_LIST) {\n const object = content['object']\n const field = text(content['field'])\n if (!isCrmFieldObject(object)) problems.push(`A custom Choice field is on one of ${CRM_FIELD_OBJECTS.join(', ')}.`)\n if (!field) problems.push('Name the custom Choice field the values go on.')\n if (isCrmFieldObject(object) && field) target = { kind: 'custom', object, field }\n }\n\n const maxLength = target?.kind === 'custom' ? CRM_FIELD_OPTION_MAX_LENGTH : 120\n const listed = Array.isArray(content['values']) ? content['values'] : []\n const values: string[] = []\n for (const entry of listed) {\n if (typeof entry !== 'string') {\n problems.push('Each value is a name.')\n break\n }\n const label = normalizeCrmPicklistLabel(entry).slice(0, maxLength)\n if (!label || values.some((one) => lower(one) === lower(label))) continue\n values.push(label)\n }\n if (!values.length) problems.push('Name at least one value to add.')\n if (values.length > CRM_PICKLIST_VALUES_SETUP_MAX) {\n problems.push(`One ask adds at most ${CRM_PICKLIST_VALUES_SETUP_MAX} values.`)\n }\n\n let meaning: string | null = null\n let group: string | null = null\n if (definition) {\n const addable = picklistAddableMeanings(definition)\n if (definition.meanings?.length) {\n const askedMeaning = text(content['meaning'])\n meaning = askedMeaning ? meaningNamed(definition, askedMeaning) : null\n const names = addable.map((one) => picklistMeaningLabel(definition, one)).join(', ')\n if (meaning && definition.reservedMeanings?.includes(meaning)) {\n problems.push(`Only the platform sets ${picklistMeaningLabel(definition, meaning)}; an added ${definition.label.toLowerCase()} means one of: ${names}.`)\n } else if (!meaning) {\n problems.push(`Each ${definition.label.toLowerCase()} value means one of: ${names}. Say which.`)\n }\n }\n const askedGroup = text(content['group'])\n if (askedGroup && definition.groups?.length) {\n group = groupNamed(definition, askedGroup)\n if (!group) problems.push(`A ${definition.label.toLowerCase()} group is one of: ${definition.groups.map((one) => one.label).join(', ')}.`)\n }\n }\n\n if (problems.length || !target) return { ok: false, problems: problems.length ? problems : ['Name a list.'] }\n return { ok: true, value: { target, values, meaning, group } }\n}\n\n/** The writer's check: whether the content is an ask, with the list and values it adds. Pure. */\nexport function checkCrmPicklistValuesSetup(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readCrmPicklistValuesSetup(content)\n if (read.ok === false) return { ok: false, problems: read.problems }\n const { target, values } = read.value\n return {\n ok: true,\n facts: target.kind === 'picklist' ? { list: target.id, values } : { list: CRM_CUSTOM_CHOICE_LIST, object: target.object, field: target.field, values },\n }\n}\n\n/** What adding to a list comes to: the values added, those it already held, and those it holds inactive. */\nexport interface CrmPicklistValuesOutcome {\n added: string[]\n existing: string[]\n inactive: string[]\n}\n\n/**\n * The values a standard picklist gains, as the console's Add value adds them\n * one by one at the end of the list. Pure. A value the list holds, in any\n * capitals, is left as it is; the list's ceiling refuses the whole ask\n * rather than adding part of it.\n */\nexport function planCrmPicklistValues(\n definition: CrmPicklistDefinition,\n picklist: CrmPicklist,\n setup: Pick<CrmPicklistValuesSetup, 'values' | 'meaning' | 'group'>,\n): ({ ok: true; picklist: CrmPicklist } & CrmPicklistValuesOutcome) | { ok: false; error: string } {\n let next = picklist\n const outcome: CrmPicklistValuesOutcome = { added: [], existing: [], inactive: [] }\n for (const label of setup.values) {\n const held = crmPicklistValueByLabel(next, label)\n if (held) {\n ;(held.active ? outcome.existing : outcome.inactive).push(held.label)\n continue\n }\n const move = addPicklistValue(definition, next, label, { group: setup.group, meaning: setup.meaning })\n if (move.ok === false) return { ok: false, error: move.error }\n next = move.picklist\n outcome.added.push(label)\n }\n return { ok: true, picklist: next, ...outcome }\n}\n\n/** What adding choices to a custom Choice field comes to. */\nexport type CrmCustomChoiceValuesPlan =\n | ({ ok: true; id: string; label: string; options: string[] } & CrmPicklistValuesOutcome)\n | { ok: false; status: 404 | 409; error: string }\n\n/**\n * The choices a custom Choice field gains, appended as the drawer appends a\n * typed line. Pure. The field is the active one on the object with that\n * label or key, in any capitals; a choice it holds in any capitals is left\n * as it is, and the drawer's 50-choice ceiling refuses the whole ask.\n */\nexport function planCrmCustomChoiceValues(\n setup: Pick<CrmPicklistValuesSetup, 'values'> & { target: Extract<CrmPicklistValuesTarget, { kind: 'custom' }> },\n definitions: readonly CrmFieldDefinitionDoc[],\n): CrmCustomChoiceValuesPlan {\n const { object, field } = setup.target\n const key = normalizeContactFieldKey(field)\n const noun = CRM_FIELD_OBJECT_LABELS[object].toLowerCase()\n const match = fieldDefinitionsForObject(definitions, object).find(\n (one) => !one.retiredAt && (lower(String(one.label ?? '')) === lower(field) || (key !== null && one.key === key)),\n )\n if (!match) {\n return {\n ok: false,\n status: 404,\n error: `There is no custom field “${field}” on ${noun}. Add it as a Choice field first.`,\n }\n }\n if (match.type !== 'select') {\n return { ok: false, status: 409, error: `“${match.label}” on ${noun} is not a Choice field, so it has no choices to add to.` }\n }\n const options = [...(match.options ?? [])]\n const outcome: CrmPicklistValuesOutcome = { added: [], existing: [], inactive: [] }\n for (const label of setup.values) {\n const held = options.find((one) => lower(one) === lower(label))\n if (held) {\n outcome.existing.push(held)\n continue\n }\n options.push(label)\n outcome.added.push(label)\n }\n if (options.length > CRM_FIELD_OPTIONS_MAX) {\n return {\n ok: false,\n status: 409,\n error: `“${match.label}” holds at most ${CRM_FIELD_OPTIONS_MAX} choices; it has ${match.options?.length ?? 0}.`,\n }\n }\n return { ok: true, id: match.$id, label: match.label, options, ...outcome }\n}\n\n/** Where a picklist's values are kept, for a refusal and a record's name: \"Lead status (Leads)\". */\nexport function crmPicklistValuesName(definition: CrmPicklistDefinition): string {\n return `${definition.label} (${CRM_PICKLIST_OBJECT_LABELS[definition.object]})`\n}\n"],"names":["fieldDefinitionsForObject","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","CRM_PICKLIST_IDS","CRM_PICKLIST_OBJECT_LABELS","crmPicklistDefinition","crmPicklistValueByLabel","isCrmFieldObject","normalizeContactFieldKey","normalizeCrmPicklistLabel","CRM_FIELD_OPTION_MAX_LENGTH","CRM_FIELD_OPTIONS_MAX","addPicklistValue","picklistAddableMeanings","picklistMeaningLabel","CRM_PICKLIST_VALUES_SETUP_RESOURCE","CRM_CUSTOM_CHOICE_LIST","CRM_PICKLIST_VALUES_SETUP_MAX","CRM_PICKLIST_VALUES_LISTS","text","value","replace","trim","lower","toLowerCase","meaningNamed","definition","asked","key","meanings","find","meaning","groupNamed","groups","group","id","label","readCrmPicklistValuesSetup","content","problems","list","push","join","target","kind","object","field","maxLength","listed","Array","isArray","values","entry","slice","some","one","length","addable","askedMeaning","names","map","reservedMeanings","includes","askedGroup","ok","checkCrmPicklistValuesSetup","read","facts","planCrmPicklistValues","picklist","setup","next","outcome","added","existing","inactive","held","active","move","error","planCrmCustomChoiceValues","definitions","match","noun","retiredAt","String","status","type","options","$id","crmPicklistValuesName"],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
15
15
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
16
16
|
import { crmRoutes } from "./crm-routes.js";
|
|
17
|
+
import { CRM_PICKLIST_VALUES_SETUP_RESOURCE } from "./crm-picklist-setup-content.js";
|
|
17
18
|
import { CRM_FIELD_SETUP_RESOURCE, CRM_PIPELINE_SETUP_RESOURCE } from "./crm-setup-content.js";
|
|
18
19
|
/**
|
|
19
20
|
* Where the CRM's records are read, published for every other surface.
|
|
@@ -78,6 +79,11 @@ function hub(context) {
|
|
|
78
79
|
list: (context)=>hub(context).section('fields'),
|
|
79
80
|
record: (context)=>hub(context).section('fields')
|
|
80
81
|
}, owner);
|
|
82
|
+
// A picklist's values and a Choice field's choices are kept on Fields (AGL-3756).
|
|
83
|
+
registerPluginRecordRoute(CRM_PICKLIST_VALUES_SETUP_RESOURCE, {
|
|
84
|
+
list: (context)=>hub(context).section('fields'),
|
|
85
|
+
record: (context)=>hub(context).section('fields')
|
|
86
|
+
}, owner);
|
|
81
87
|
// A task has no page of its own: it is a row in the Tasks list, at both
|
|
82
88
|
// levels, so a task's record answers the list.
|
|
83
89
|
registerPluginRecordRoute('task', {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { crmRoutes, type CrmRoutes } from './crm-routes'\nimport { CRM_FIELD_SETUP_RESOURCE, CRM_PIPELINE_SETUP_RESOURCE } from './crm-setup-content'\n\n/**\n * Where the CRM's records are read, published for every other surface.\n *\n * The Inbox links a lead, a submission links its sender, a form links the\n * people it captured. Each used to import {@link crmRoutes} and spell the\n * CRM's nav slug to do it, which made every one of them a reader of this\n * plugin's address table. They ask the record-route registry now, and get\n * `null` — text instead of a link — in a workspace where the CRM is not\n * loaded, which is also the first time that link stopped pointing at a page\n * the workspace cannot open.\n */\n\n/** The nav slug the shell resolves this plugin's hub by, at either scope. */\nconst CRM_SLUG = 'crm'\n\nfunction hub(context: PluginRecordRouteContext): CrmRoutes {\n return crmRoutes(\n context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_SLUG,\n })\n : buildRoute(Route.ORG_PLUGIN, {\n orgSlug: context.orgSlug,\n pluginSlug: CRM_SLUG,\n }),\n )\n}\n\n/** The hub's routes at the scope the context names — the site's, or the org's. */\nexport { hub as crmHubRoutes }\n\n/** The one narrowing the contacts list publishes: the form that captured them. */\nexport const CRM_CONTACT_FILTER_FORM = 'form'\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the routes register the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerCrmRecordRoutes(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginRecordRoute('contact', {\n list: (context) => hub(context).section('contacts'),\n record: (context, id) => hub(context).contact(id),\n byEmail: (context, email) => hub(context).contactByEmail(email),\n filtered: (context, filter, value) =>\n filter === CRM_CONTACT_FILTER_FORM\n ? hub(context).contactsByForm(value)\n : null,\n }, owner)\n registerPluginRecordRoute('lead', {\n list: (context) => hub(context).section('leads'),\n // One org row per person since AGL-3275, so the id alone addresses a\n // lead at both levels — the organization's hub included, where the\n // organization's Inbox links its leads (AGL-3303).\n record: (context, id) => hub(context).lead(id),\n }, owner)\n registerPluginRecordRoute('company', {\n list: (context) => hub(context).section('companies'),\n record: (context, id) => hub(context).company(id),\n }, owner)\n registerPluginRecordRoute('deal', {\n list: (context) => hub(context).section('deals'),\n record: (context, id) => hub(context).deal(id),\n }, owner)\n // The setup an AI build saves (AGL-3746): a pipeline is edited from the\n // Deals section's Pipelines, and a custom field is a row in Fields, so\n // each record answers its section.\n registerPluginRecordRoute(CRM_PIPELINE_SETUP_RESOURCE, {\n list: (context) => hub(context).section('deals'),\n record: (context) => hub(context).section('deals'),\n }, owner)\n registerPluginRecordRoute(CRM_FIELD_SETUP_RESOURCE, {\n list: (context) => hub(context).section('fields'),\n record: (context) => hub(context).section('fields'),\n }, owner)\n // A task has no page of its own: it is a row in the Tasks list, at both\n // levels, so a task's record answers the list.\n registerPluginRecordRoute('task', {\n list: (context) => hub(context).section('tasks'),\n record: (context) => hub(context).section('tasks'),\n }, owner)\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","crmRoutes","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","CRM_SLUG","hub","context","host","HOST_PLUGIN","orgSlug","pluginSlug","ORG_PLUGIN","crmHubRoutes","CRM_CONTACT_FILTER_FORM","registerCrmRecordRoutes","owner","pluginId","list","section","record","id","contact","byEmail","email","contactByEmail","filtered","filter","value","contactsByForm","lead","company","deal"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,SAAS,QAAwB,kBAAc;AACxD,SAASC,wBAAwB,EAAEC,2BAA2B,QAAQ,yBAAqB;AAE3F;;;;;;;;;;CAUC,GAED,2EAA2E,GAC3E,MAAMC,WAAW;AAEjB,SAASC,IAAIC,OAAiC;IAC5C,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { crmRoutes, type CrmRoutes } from './crm-routes'\nimport { CRM_PICKLIST_VALUES_SETUP_RESOURCE } from './crm-picklist-setup-content'\nimport { CRM_FIELD_SETUP_RESOURCE, CRM_PIPELINE_SETUP_RESOURCE } from './crm-setup-content'\n\n/**\n * Where the CRM's records are read, published for every other surface.\n *\n * The Inbox links a lead, a submission links its sender, a form links the\n * people it captured. Each used to import {@link crmRoutes} and spell the\n * CRM's nav slug to do it, which made every one of them a reader of this\n * plugin's address table. They ask the record-route registry now, and get\n * `null` — text instead of a link — in a workspace where the CRM is not\n * loaded, which is also the first time that link stopped pointing at a page\n * the workspace cannot open.\n */\n\n/** The nav slug the shell resolves this plugin's hub by, at either scope. */\nconst CRM_SLUG = 'crm'\n\nfunction hub(context: PluginRecordRouteContext): CrmRoutes {\n return crmRoutes(\n context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: CRM_SLUG,\n })\n : buildRoute(Route.ORG_PLUGIN, {\n orgSlug: context.orgSlug,\n pluginSlug: CRM_SLUG,\n }),\n )\n}\n\n/** The hub's routes at the scope the context names — the site's, or the org's. */\nexport { hub as crmHubRoutes }\n\n/** The one narrowing the contacts list publishes: the form that captured them. */\nexport const CRM_CONTACT_FILTER_FORM = 'form'\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the routes register the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerCrmRecordRoutes(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginRecordRoute('contact', {\n list: (context) => hub(context).section('contacts'),\n record: (context, id) => hub(context).contact(id),\n byEmail: (context, email) => hub(context).contactByEmail(email),\n filtered: (context, filter, value) =>\n filter === CRM_CONTACT_FILTER_FORM\n ? hub(context).contactsByForm(value)\n : null,\n }, owner)\n registerPluginRecordRoute('lead', {\n list: (context) => hub(context).section('leads'),\n // One org row per person since AGL-3275, so the id alone addresses a\n // lead at both levels — the organization's hub included, where the\n // organization's Inbox links its leads (AGL-3303).\n record: (context, id) => hub(context).lead(id),\n }, owner)\n registerPluginRecordRoute('company', {\n list: (context) => hub(context).section('companies'),\n record: (context, id) => hub(context).company(id),\n }, owner)\n registerPluginRecordRoute('deal', {\n list: (context) => hub(context).section('deals'),\n record: (context, id) => hub(context).deal(id),\n }, owner)\n // The setup an AI build saves (AGL-3746): a pipeline is edited from the\n // Deals section's Pipelines, and a custom field is a row in Fields, so\n // each record answers its section.\n registerPluginRecordRoute(CRM_PIPELINE_SETUP_RESOURCE, {\n list: (context) => hub(context).section('deals'),\n record: (context) => hub(context).section('deals'),\n }, owner)\n registerPluginRecordRoute(CRM_FIELD_SETUP_RESOURCE, {\n list: (context) => hub(context).section('fields'),\n record: (context) => hub(context).section('fields'),\n }, owner)\n // A picklist's values and a Choice field's choices are kept on Fields (AGL-3756).\n registerPluginRecordRoute(CRM_PICKLIST_VALUES_SETUP_RESOURCE, {\n list: (context) => hub(context).section('fields'),\n record: (context) => hub(context).section('fields'),\n }, owner)\n // A task has no page of its own: it is a row in the Tasks list, at both\n // levels, so a task's record answers the list.\n registerPluginRecordRoute('task', {\n list: (context) => hub(context).section('tasks'),\n record: (context) => hub(context).section('tasks'),\n }, owner)\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","crmRoutes","CRM_PICKLIST_VALUES_SETUP_RESOURCE","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","CRM_SLUG","hub","context","host","HOST_PLUGIN","orgSlug","pluginSlug","ORG_PLUGIN","crmHubRoutes","CRM_CONTACT_FILTER_FORM","registerCrmRecordRoutes","owner","pluginId","list","section","record","id","contact","byEmail","email","contactByEmail","filtered","filter","value","contactsByForm","lead","company","deal"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,SAAS,QAAwB,kBAAc;AACxD,SAASC,kCAAkC,QAAQ,kCAA8B;AACjF,SAASC,wBAAwB,EAAEC,2BAA2B,QAAQ,yBAAqB;AAE3F;;;;;;;;;;CAUC,GAED,2EAA2E,GAC3E,MAAMC,WAAW;AAEjB,SAASC,IAAIC,OAAiC;IAC5C,OAAON,UACLM,QAAQC,IAAI,GACRX,WAAWC,MAAMW,WAAW,EAAE;QAC5BC,SAASH,QAAQG,OAAO;QACxBF,MAAMD,QAAQC,IAAI;QAClBG,YAAYN;IACd,KACAR,WAAWC,MAAMc,UAAU,EAAE;QAC3BF,SAASH,QAAQG,OAAO;QACxBC,YAAYN;IACd;AAER;AAEA,gFAAgF,GAChF,SAASC,OAAOO,YAAY,GAAE;AAE9B,gFAAgF,GAChF,OAAO,MAAMC,0BAA0B,OAAM;AAE7C;;;;CAIC,GACD,OAAO,SAASC;IACd,MAAMC,QAAQ;QAAEC,UAAUjB;IAAU;IACpCD,0BAA0B,WAAW;QACnCmB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASe,OAAO,CAACD;QAC9CE,SAAS,CAAChB,SAASiB,QAAUlB,IAAIC,SAASkB,cAAc,CAACD;QACzDE,UAAU,CAACnB,SAASoB,QAAQC,QAC1BD,WAAWb,0BACPR,IAAIC,SAASsB,cAAc,CAACD,SAC5B;IACR,GAAGZ;IACHjB,0BAA0B,QAAQ;QAChCmB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxC,qEAAqE;QACrE,mEAAmE;QACnE,mDAAmD;QACnDC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASuB,IAAI,CAACT;IAC7C,GAAGL;IACHjB,0BAA0B,WAAW;QACnCmB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASwB,OAAO,CAACV;IAChD,GAAGL;IACHjB,0BAA0B,QAAQ;QAChCmB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,SAASc,KAAOf,IAAIC,SAASyB,IAAI,CAACX;IAC7C,GAAGL;IACH,wEAAwE;IACxE,uEAAuE;IACvE,mCAAmC;IACnCjB,0BAA0BK,6BAA6B;QACrDc,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,UAAYD,IAAIC,SAASY,OAAO,CAAC;IAC5C,GAAGH;IACHjB,0BAA0BI,0BAA0B;QAClDe,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,UAAYD,IAAIC,SAASY,OAAO,CAAC;IAC5C,GAAGH;IACH,kFAAkF;IAClFjB,0BAA0BG,oCAAoC;QAC5DgB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,UAAYD,IAAIC,SAASY,OAAO,CAAC;IAC5C,GAAGH;IACH,wEAAwE;IACxE,+CAA+C;IAC/CjB,0BAA0B,QAAQ;QAChCmB,MAAM,CAACX,UAAYD,IAAIC,SAASY,OAAO,CAAC;QACxCC,QAAQ,CAACb,UAAYD,IAAIC,SAASY,OAAO,CAAC;IAC5C,GAAGH;AACL"}
|
|
@@ -35,7 +35,8 @@ import { type PipelineDoc, type PipelineWriteContext } from './deal-board-model'
|
|
|
35
35
|
* `newStageId`'s slugs.
|
|
36
36
|
* - A FIELD is the Fields section's create: a key derived from the label by
|
|
37
37
|
* `normalizeContactFieldKey`, one of `CONTACT_FIELD_TYPES`, the choices
|
|
38
|
-
* the drawer allows,
|
|
38
|
+
* the drawer allows, a currency field's ISO 4217 code (USD when unsaid,
|
|
39
|
+
* as the drawer's default), landing last on its object's tab, org-wide.
|
|
39
40
|
*
|
|
40
41
|
* IDEMPOTENT BY NAME. Asked twice, nothing is made twice: an active pipeline
|
|
41
42
|
* is matched by name and a stage within it by name, a field by label or key
|
|
@@ -147,6 +148,8 @@ export interface CrmFieldSetup {
|
|
|
147
148
|
type: ContactFieldType;
|
|
148
149
|
/** The choices, for `select`; empty otherwise. */
|
|
149
150
|
options: string[];
|
|
151
|
+
/** For `currency` (AGL-3756): the lowercase ISO 4217 code; `null` otherwise. */
|
|
152
|
+
currency: string | null;
|
|
150
153
|
required: boolean;
|
|
151
154
|
}
|
|
152
155
|
/** A field's content, read and judged. Pure. */
|
|
@@ -16,7 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ // Modules by their own paths, never the `@aglyn/aglyn` barrel: the console
|
|
18
18
|
// server's boot reaches this file through the CRM's setup writers.
|
|
19
|
-
import { CONTACT_FIELD_TYPES, CONTACT_FIELDS_MAX_PER_ORG, fieldDefinitionsForObject, isContactFieldType } from "@aglyn/aglyn/app-utils/contact-custom-fields";
|
|
19
|
+
import { CONTACT_FIELD_DEFAULT_CURRENCY, CONTACT_FIELD_TYPES, CONTACT_FIELDS_MAX_PER_ORG, fieldDefinitionsForObject, isCustomFieldCurrency, isContactFieldType } from "@aglyn/aglyn/app-utils/contact-custom-fields";
|
|
20
20
|
import { CRM_FIELD_OBJECT_LABELS, CRM_FIELD_OBJECTS, crmFieldListFields, DEFAULT_DEAL_STAGES, isCrmFieldObject, normalizeContactFieldKey } from "@aglyn/aglyn/app-utils/crm";
|
|
21
21
|
import { activePipelines, clampProbability, newPipelineDocument, newStageId, PIPELINE_NAME_MAX, pipelineNameProblem, renumberStages, sortedStages, stagesProblem } from "./deal-board-model.js";
|
|
22
22
|
/**
|
|
@@ -37,7 +37,8 @@ import { activePipelines, clampProbability, newPipelineDocument, newStageId, PIP
|
|
|
37
37
|
* `newStageId`'s slugs.
|
|
38
38
|
* - A FIELD is the Fields section's create: a key derived from the label by
|
|
39
39
|
* `normalizeContactFieldKey`, one of `CONTACT_FIELD_TYPES`, the choices
|
|
40
|
-
* the drawer allows,
|
|
40
|
+
* the drawer allows, a currency field's ISO 4217 code (USD when unsaid,
|
|
41
|
+
* as the drawer's default), landing last on its object's tab, org-wide.
|
|
41
42
|
*
|
|
42
43
|
* IDEMPOTENT BY NAME. Asked twice, nothing is made twice: an active pipeline
|
|
43
44
|
* is matched by name and a stage within it by name, a field by label or key
|
|
@@ -309,6 +310,11 @@ export const CRM_FIELD_LABEL_MAX_LENGTH = 80;
|
|
|
309
310
|
const listed = Array.isArray(content['options']) ? content['options'].filter((one)=>typeof one === 'string') : [];
|
|
310
311
|
const options = type === 'select' ? crmFieldOptions(listed) : [];
|
|
311
312
|
if (type === 'select' && !options.length) problems.push('A Choice field needs its choices.');
|
|
313
|
+
// A currency field's code, as the drawer offers it: named, or the default.
|
|
314
|
+
const asked = text(content['currency']).toLowerCase();
|
|
315
|
+
if (type === 'currency' && asked && !isCustomFieldCurrency(asked)) {
|
|
316
|
+
problems.push('A currency field’s currency is an ISO 4217 code, such as USD or EUR.');
|
|
317
|
+
}
|
|
312
318
|
if (problems.length) return {
|
|
313
319
|
ok: false,
|
|
314
320
|
problems
|
|
@@ -321,6 +327,7 @@ export const CRM_FIELD_LABEL_MAX_LENGTH = 80;
|
|
|
321
327
|
key: key,
|
|
322
328
|
type: type,
|
|
323
329
|
options,
|
|
330
|
+
currency: type === 'currency' ? asked || CONTACT_FIELD_DEFAULT_CURRENCY : null,
|
|
324
331
|
required: content['required'] === true
|
|
325
332
|
}
|
|
326
333
|
};
|
|
@@ -333,11 +340,13 @@ export const CRM_FIELD_LABEL_MAX_LENGTH = 80;
|
|
|
333
340
|
};
|
|
334
341
|
return {
|
|
335
342
|
ok: true,
|
|
336
|
-
facts: {
|
|
343
|
+
facts: _extends({
|
|
337
344
|
object: read.value.object,
|
|
338
345
|
key: read.value.key,
|
|
339
346
|
type: read.value.type
|
|
340
|
-
}
|
|
347
|
+
}, read.value.currency ? {
|
|
348
|
+
currency: read.value.currency
|
|
349
|
+
} : {})
|
|
341
350
|
};
|
|
342
351
|
}
|
|
343
352
|
/**
|
|
@@ -393,6 +402,8 @@ export const CRM_FIELD_LABEL_MAX_LENGTH = 80;
|
|
|
393
402
|
type: setup.type
|
|
394
403
|
}, setup.type === 'select' ? {
|
|
395
404
|
options: setup.options
|
|
405
|
+
} : {}, setup.type === 'currency' && setup.currency ? {
|
|
406
|
+
currency: setup.currency
|
|
396
407
|
} : {}, {
|
|
397
408
|
required: setup.required,
|
|
398
409
|
order,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-setup-content.ts"],"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// Modules by their own paths, never the `@aglyn/aglyn` barrel: the console\n// server's boot reaches this file through the CRM's setup writers.\nimport {\n CONTACT_FIELD_TYPES,\n CONTACT_FIELDS_MAX_PER_ORG,\n fieldDefinitionsForObject,\n isContactFieldType,\n} from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport {\n type ContactFieldDefinition,\n type ContactFieldType,\n type CrmDealStage,\n type CrmFieldObject,\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n crmFieldListFields,\n DEFAULT_DEAL_STAGES,\n isCrmFieldObject,\n normalizeContactFieldKey,\n} from '@aglyn/aglyn/app-utils/crm'\nimport type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n activePipelines,\n clampProbability,\n newPipelineDocument,\n newStageId,\n PIPELINE_NAME_MAX,\n type PipelineDoc,\n type PipelineWriteContext,\n pipelineNameProblem,\n renumberStages,\n sortedStages,\n stagesProblem,\n} from './deal-board-model'\n\n/**\n * CRM SETUP ANOTHER PLUGIN ASKS FOR (AGL-3746): a deal pipeline with its\n * stages, and a custom field on a CRM object — what a person sets up in\n * CRM → Deals → Pipelines and CRM → Fields, as pure rules.\n *\n * Aglyn AI's build asks for them through the CRM's writers on the\n * resource-drafts seam (`server/crm-setup-writers.server.ts`). Neither has a\n * draft state: a pipeline and a field are in use once saved, as the console's\n * own saves are. So every rule here is the console's, read from the modules\n * the console reads:\n *\n * - A PIPELINE is `newPipelineDocument` with the stages asked for in place\n * of the default set, held to `stagesProblem` (exactly one Won and one\n * Lost, at least one open stage) and named under `pipelineNameProblem`\n * (unique among the active pipelines, without case). Stage ids are\n * `newStageId`'s slugs.\n * - A FIELD is the Fields section's create: a key derived from the label by\n * `normalizeContactFieldKey`, one of `CONTACT_FIELD_TYPES`, the choices\n * the drawer allows, landing last on its object's tab, org-wide.\n *\n * IDEMPOTENT BY NAME. Asked twice, nothing is made twice: an active pipeline\n * is matched by name and a stage within it by name, a field by label or key\n * within its object, all without case. A match is brought to what was\n * asked — missing stages added, stages put in the asked order, a\n * probability set — and nothing is ever removed: a stage may hold deals and\n * a field values, and removing either is a person's decision.\n */\n\n// ── Pipelines ─────────────────────────────────────────────────────────────\n\n/** The resource the CRM's pipeline writer is registered under. */\nexport const CRM_PIPELINE_SETUP_RESOURCE = 'crm-pipeline'\n\n/** The longest stage name, as long as a pipeline's name. */\nexport const CRM_SETUP_STAGE_NAME_MAX = PIPELINE_NAME_MAX\n\n/** The most open stages one ask sets; a board wider than this does not read. */\nexport const CRM_SETUP_OPEN_STAGES_MAX = 12\n\n/** A pipeline as one ask states it, once read. */\nexport interface CrmPipelineSetup {\n name: string\n /** An active pipeline to rename to `name`, by its current name. */\n renameFrom: string | null\n /** The open stages, in order, each with the probability asked for, if any. */\n stages: Array<{ name: string; probability: number | null }>\n /** What the Won and Lost stages are called, where the ask says. */\n wonStage: string | null\n lostStage: string | null\n /** Whether this becomes the pipeline a new deal lands in. */\n makeDefault: boolean\n}\n\nconst WON_NAME = /^(closed[\\s-]*)?won$/i\nconst LOST_NAME = /^(closed[\\s-]*)?lost$/i\n/** \"Discovery (20%)\": a stage's name with its probability. */\nconst STAGE_WITH_PROBABILITY = /^(.*?)\\s*\\(\\s*(\\d{1,3}(?:\\.\\d+)?)\\s*%?\\s*\\)\\s*$/\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n}\n\nconst same = (a: string, b: string) => a.trim().toLowerCase() === b.trim().toLowerCase()\n\n/** One listed stage, read: its name and the probability written after it. */\nexport function readCrmSetupStage(entry: string): { name: string; probability: number | null } {\n const raw = text(entry)\n const match = STAGE_WITH_PROBABILITY.exec(raw)\n if (!match) return { name: raw, probability: null }\n return { name: text(match[1]), probability: clampProbability(match[2]) }\n}\n\n/**\n * A pipeline's content, read and judged. Pure. A stage listed as Won or\n * Lost (\"Won\", \"Closed Lost\") names the closing stage rather than adding an\n * open one, since every pipeline already has exactly one of each.\n */\nexport function readCrmPipelineSetup(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CrmPipelineSetup } | { ok: false; problems: string[] } {\n const problems: string[] = []\n const name = text(content['name'])\n if (!name) problems.push('A pipeline needs a name.')\n else if (name.length > PIPELINE_NAME_MAX) problems.push(`A pipeline's name is at most ${PIPELINE_NAME_MAX} characters.`)\n let wonStage = text(content['wonStage']) || null\n let lostStage = text(content['lostStage']) || null\n const listed = Array.isArray(content['stages']) ? content['stages'] : []\n const stages: CrmPipelineSetup['stages'] = []\n for (const entry of listed) {\n if (typeof entry !== 'string') {\n problems.push('Each stage is a name.')\n break\n }\n const stage = readCrmSetupStage(entry)\n if (!stage.name) continue\n if (WON_NAME.test(stage.name)) {\n wonStage ??= stage.name\n continue\n }\n if (LOST_NAME.test(stage.name)) {\n lostStage ??= stage.name\n continue\n }\n if (stage.name.length > CRM_SETUP_STAGE_NAME_MAX) {\n problems.push(`A stage's name is at most ${CRM_SETUP_STAGE_NAME_MAX} characters.`)\n break\n }\n if (stages.some((one) => same(one.name, stage.name))) continue\n stages.push(stage)\n }\n if (stages.length > CRM_SETUP_OPEN_STAGES_MAX) {\n problems.push(`A pipeline is set up with at most ${CRM_SETUP_OPEN_STAGES_MAX} open stages.`)\n }\n for (const closing of [wonStage, lostStage]) {\n if (closing && closing.length > CRM_SETUP_STAGE_NAME_MAX) {\n problems.push(`A stage's name is at most ${CRM_SETUP_STAGE_NAME_MAX} characters.`)\n }\n }\n if (wonStage && lostStage && same(wonStage, lostStage)) problems.push('The Won and Lost stages need different names.')\n if (problems.length) return { ok: false, problems }\n return {\n ok: true,\n value: {\n name,\n renameFrom: text(content['renameFrom']) || null,\n stages,\n wonStage,\n lostStage,\n makeDefault: content['makeDefault'] === true,\n },\n }\n}\n\n/** The `crm-pipeline` writer's check: whether the content is a pipeline ask, with its name and stages. Pure. */\nexport function checkCrmPipelineSetup(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readCrmPipelineSetup(content)\n if (read.ok === false) return { ok: false, problems: read.problems }\n return { ok: true, facts: { name: read.value.name, stages: read.value.stages.map((one) => one.name) } }\n}\n\n/**\n * The probability an open stage takes when the ask names none: spread over\n * the pipeline by position, to the nearest 5 — the first of four stages 20,\n * the last 80 — so a weighted forecast grows as a deal moves right.\n */\nexport function crmSetupStageProbability(index: number, count: number): number {\n const spread = ((index + 1) / (count + 1)) * 100\n return Math.min(95, Math.max(5, Math.round(spread / 5) * 5))\n}\n\n/**\n * The stages a pipeline ends up with: the open stages asked for, in the asked\n * order, each an existing stage of that name where there is one (its id kept,\n * so the deals in it stay), then the open stages the ask did not name, as\n * they were, then Won and Lost, renamed where asked. Nothing is removed.\n */\nexport function mergeCrmSetupStages(\n existing: readonly CrmDealStage[],\n setup: Pick<CrmPipelineSetup, 'stages' | 'wonStage' | 'lostStage'>,\n): CrmDealStage[] {\n const sorted = sortedStages({ stages: [...existing] })\n const opens = sorted.filter((stage) => stage.kind === 'open')\n const taken: CrmDealStage[] = [...sorted]\n const listed: CrmDealStage[] = setup.stages.map((asked, index) => {\n const found = opens.find((stage) => same(stage.name, asked.name))\n if (found) return asked.probability === null ? found : { ...found, probability: asked.probability }\n const stage: CrmDealStage = {\n id: newStageId(asked.name, taken),\n name: asked.name,\n order: 0,\n probability: asked.probability ?? crmSetupStageProbability(index, setup.stages.length),\n kind: 'open',\n forecastCategory: 'pipeline',\n }\n taken.push(stage)\n return stage\n })\n const unlisted = opens.filter((stage) => !listed.some((one) => one.id === stage.id))\n const closing = (kind: 'won' | 'lost', name: string | null): CrmDealStage[] =>\n sorted\n .filter((stage) => stage.kind === kind)\n .map((stage) => (name ? { ...stage, name } : stage))\n return renumberStages([\n ...listed,\n ...unlisted,\n ...closing('won', setup.wonStage),\n ...closing('lost', setup.lostStage),\n ])\n}\n\n/** What saving a pipeline ask comes to against the pipelines the member's site can read. */\nexport type CrmPipelineSetupPlan =\n | {\n kind: 'create'\n document: Record<string, unknown>\n /** Pipelines whose default flag comes off, because this one takes it. */\n defaultsOff: string[]\n }\n | {\n kind: 'update'\n id: string\n name: string\n /** The fields that change; empty when the pipeline already is what was asked. */\n patch: { name?: string; stages?: CrmDealStage[]; isDefault?: true }\n defaultsOff: string[]\n }\n | { kind: 'refuse'; error: string }\n\n/**\n * Create, or bring an existing pipeline to the ask. Pure.\n *\n * The pipeline is the active one called `renameFrom`, else the active one\n * called `name`; with neither, a new one, which is also the default when the\n * site reads no active pipeline yet (the board's seed would have made it so)\n * or when asked.\n */\nexport function planCrmPipelineSetup(\n setup: CrmPipelineSetup,\n pipelines: readonly PipelineDoc[],\n context: PipelineWriteContext,\n): CrmPipelineSetupPlan {\n const active = activePipelines(pipelines)\n const named = (name: string | null) => (name ? active.find((one) => same(String(one.name ?? ''), name)) : undefined)\n const target = named(setup.renameFrom) ?? named(setup.name)\n const defaultsOff = (keep: string | null) =>\n active.filter((one) => one.isDefault && one.$id !== keep).map((one) => one.$id)\n\n if (!target) {\n const problem = pipelineNameProblem(setup.name, pipelines)\n if (problem) return { kind: 'refuse', error: problem }\n // Stages asked for replace the default open stages; none asked keeps\n // them all, as the console's New pipeline does.\n const stages = mergeCrmSetupStages(\n setup.stages.length ? DEFAULT_DEAL_STAGES.filter((stage) => stage.kind !== 'open') : DEFAULT_DEAL_STAGES,\n setup,\n )\n const invalid = stagesProblem(stages)\n if (invalid) return { kind: 'refuse', error: invalid }\n const isDefault = setup.makeDefault || active.length === 0\n return {\n kind: 'create',\n document: { ...newPipelineDocument(setup.name, context), stages, isDefault },\n defaultsOff: isDefault ? defaultsOff(null) : [],\n }\n }\n\n const patch: { name?: string; stages?: CrmDealStage[]; isDefault?: true } = {}\n // A name that differs only in case is the same pipeline, and keeps its own.\n if (!same(String(target.name ?? ''), setup.name)) {\n const problem = pipelineNameProblem(setup.name, pipelines, target.$id)\n if (problem) return { kind: 'refuse', error: problem }\n patch.name = setup.name\n }\n const current = sortedStages(target)\n const stages = mergeCrmSetupStages(current, setup)\n const invalid = stagesProblem(stages)\n if (invalid) return { kind: 'refuse', error: invalid }\n if (JSON.stringify(stages) !== JSON.stringify(renumberStages(current))) patch.stages = stages\n if (setup.makeDefault && !target.isDefault) patch.isDefault = true\n return {\n kind: 'update',\n id: target.$id,\n name: patch.name ?? target.name,\n patch,\n defaultsOff: patch.isDefault ? defaultsOff(target.$id) : [],\n }\n}\n\n/** What a pipeline's writer reports: its open stages in order, and its closing stages. */\nexport function crmPipelineSetupFacts(stages: readonly CrmDealStage[]): Readonly<Record<string, unknown>> {\n const sorted = sortedStages({ stages: [...stages] })\n return {\n stages: sorted.filter((stage) => stage.kind === 'open').map((stage) => `${stage.name} (${stage.probability}%)`),\n wonStage: sorted.find((stage) => stage.kind === 'won')?.name ?? null,\n lostStage: sorted.find((stage) => stage.kind === 'lost')?.name ?? null,\n }\n}\n\n// ── Custom fields ─────────────────────────────────────────────────────────\n\n/** The resource the CRM's custom field writer is registered under. */\nexport const CRM_FIELD_SETUP_RESOURCE = 'crm-field'\n\n/** How many choices a Choice field may declare, and how long each and a label may be: the field drawer's. */\nexport const CRM_FIELD_OPTIONS_MAX = 50\nexport const CRM_FIELD_OPTION_MAX_LENGTH = 120\nexport const CRM_FIELD_LABEL_MAX_LENGTH = 80\n\n/** One choice per entry, trimmed, blanks dropped, duplicates collapsed, at most the drawer's count. */\nexport function crmFieldOptions(entries: readonly string[]): string[] {\n const seen = new Set<string>()\n const options: string[] = []\n for (const entry of entries) {\n const option = String(entry ?? '').trim().slice(0, CRM_FIELD_OPTION_MAX_LENGTH)\n if (!option || seen.has(option)) continue\n seen.add(option)\n options.push(option)\n if (options.length >= CRM_FIELD_OPTIONS_MAX) break\n }\n return options\n}\n\n/** A custom field as one ask states it, once read. */\nexport interface CrmFieldSetup {\n object: CrmFieldObject\n label: string\n /** The key its values are stored under, derived from the label. */\n key: string\n type: ContactFieldType\n /** The choices, for `select`; empty otherwise. */\n options: string[]\n required: boolean\n}\n\n/** A field's content, read and judged. Pure. */\nexport function readCrmFieldSetup(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CrmFieldSetup } | { ok: false; problems: string[] } {\n const problems: string[] = []\n const object = content['object']\n if (!isCrmFieldObject(object)) problems.push(`A field describes one of ${CRM_FIELD_OBJECTS.join(', ')}.`)\n const type = content['type']\n if (!isContactFieldType(type)) problems.push(`A field's type is one of ${CONTACT_FIELD_TYPES.join(', ')}.`)\n const label = text(content['label'])\n const key = normalizeContactFieldKey(label)\n if (!label) problems.push('A field needs a label.')\n else if (label.length > CRM_FIELD_LABEL_MAX_LENGTH) {\n problems.push(`A field's label is at most ${CRM_FIELD_LABEL_MAX_LENGTH} characters.`)\n } else if (!key) problems.push('A field’s label needs a letter to make its key from.')\n const listed = Array.isArray(content['options']) ? content['options'].filter((one): one is string => typeof one === 'string') : []\n const options = type === 'select' ? crmFieldOptions(listed) : []\n if (type === 'select' && !options.length) problems.push('A Choice field needs its choices.')\n if (problems.length) return { ok: false, problems }\n return {\n ok: true,\n value: {\n object: object as CrmFieldObject,\n label,\n key: key as string,\n type: type as ContactFieldType,\n options,\n required: content['required'] === true,\n },\n }\n}\n\n/** The `crm-field` writer's check: whether the content is a field ask, with the key it derives. Pure. */\nexport function checkCrmFieldSetup(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readCrmFieldSetup(content)\n if (read.ok === false) return { ok: false, problems: read.problems }\n return { ok: true, facts: { object: read.value.object, key: read.value.key, type: read.value.type } }\n}\n\n/** A stored definition, as the writer reads one: its document id beside it. */\nexport type CrmFieldDefinitionDoc = ContactFieldDefinition & { $id: string }\n\n/** What saving a field ask comes to against every definition the org holds. */\nexport type CrmFieldSetupPlan =\n | { kind: 'create'; document: Record<string, unknown> }\n /** The field exists; `options` are the choices its Choice list gains, empty when none. */\n | { kind: 'existing'; id: string; label: string; options: string[] }\n | { kind: 'refuse'; status: 403 | 409; error: string }\n\n/**\n * Create the field, or find it. Pure.\n *\n * An active field on the same object with the same label or key is the\n * field: a re-run makes nothing, and a Choice field gains only the choices it\n * lacks. A retired field keeps its key, as the drawer says, so a key a\n * retired field holds is refused rather than reused — its old values would\n * read back as the new field's. The org's ceiling counts every definition,\n * retired ones included, as the Fields section's read does.\n */\nexport function planCrmFieldSetup(\n setup: CrmFieldSetup,\n definitions: readonly CrmFieldDefinitionDoc[],\n context: { hostId: string; now: Date; visibleTo: readonly string[] },\n): CrmFieldSetupPlan {\n const ofObject = fieldDefinitionsForObject(definitions, setup.object)\n const match = ofObject.find(\n (one) => !one.retiredAt && (same(String(one.label ?? ''), setup.label) || one.key === setup.key),\n )\n if (match) {\n const missing =\n match.type === 'select' && setup.type === 'select'\n ? setup.options.filter((option) => !(match.options ?? []).includes(option))\n : []\n const room = Math.max(0, CRM_FIELD_OPTIONS_MAX - (match.options ?? []).length)\n return { kind: 'existing', id: match.$id, label: match.label, options: missing.slice(0, room) }\n }\n const retired = ofObject.find((one) => one.retiredAt && one.key === setup.key)\n if (retired) {\n return {\n kind: 'refuse',\n status: 409,\n error:\n `A retired ${CRM_FIELD_OBJECT_LABELS[setup.object].toLowerCase()} field, “${retired.label}”, keeps the key ` +\n `“${setup.key}”. Restore it in CRM → Fields, or give this field another name.`,\n }\n }\n if (definitions.length >= CONTACT_FIELDS_MAX_PER_ORG) {\n return {\n kind: 'refuse',\n status: 403,\n error: `An organization keeps at most ${CONTACT_FIELDS_MAX_PER_ORG} custom fields. Retire or delete one in CRM → Fields first.`,\n }\n }\n const order = ofObject.reduce((highest, one) => Math.max(highest, Number(one.order) || 0), -1) + 1\n return {\n kind: 'create',\n document: {\n key: setup.key,\n label: setup.label,\n type: setup.type,\n ...(setup.type === 'select' ? { options: setup.options } : {}),\n required: setup.required,\n order,\n retiredAt: null,\n ...crmFieldListFields({ key: setup.key, label: setup.label, required: setup.required, object: setup.object }),\n hostId: context.hostId,\n visibleTo: [...context.visibleTo],\n createdAt: context.now,\n updatedAt: context.now,\n },\n }\n}\n"],"names":["CONTACT_FIELD_TYPES","CONTACT_FIELDS_MAX_PER_ORG","fieldDefinitionsForObject","isContactFieldType","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","crmFieldListFields","DEFAULT_DEAL_STAGES","isCrmFieldObject","normalizeContactFieldKey","activePipelines","clampProbability","newPipelineDocument","newStageId","PIPELINE_NAME_MAX","pipelineNameProblem","renumberStages","sortedStages","stagesProblem","CRM_PIPELINE_SETUP_RESOURCE","CRM_SETUP_STAGE_NAME_MAX","CRM_SETUP_OPEN_STAGES_MAX","WON_NAME","LOST_NAME","STAGE_WITH_PROBABILITY","text","value","replace","trim","same","a","b","toLowerCase","readCrmSetupStage","entry","raw","match","exec","name","probability","readCrmPipelineSetup","content","problems","push","length","wonStage","lostStage","listed","Array","isArray","stages","stage","test","some","one","closing","ok","renameFrom","makeDefault","checkCrmPipelineSetup","read","facts","map","crmSetupStageProbability","index","count","spread","Math","min","max","round","mergeCrmSetupStages","existing","setup","sorted","opens","filter","kind","taken","asked","found","find","id","order","forecastCategory","unlisted","planCrmPipelineSetup","pipelines","context","named","target","patch","active","String","undefined","defaultsOff","keep","isDefault","$id","problem","error","invalid","document","current","JSON","stringify","crmPipelineSetupFacts","CRM_FIELD_SETUP_RESOURCE","CRM_FIELD_OPTIONS_MAX","CRM_FIELD_OPTION_MAX_LENGTH","CRM_FIELD_LABEL_MAX_LENGTH","crmFieldOptions","entries","seen","Set","options","option","slice","has","add","readCrmFieldSetup","object","join","type","label","key","required","checkCrmFieldSetup","planCrmFieldSetup","definitions","ofObject","retiredAt","missing","includes","room","retired","status","reduce","highest","Number","hostId","visibleTo","createdAt","now","updatedAt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-setup-content.ts"],"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// Modules by their own paths, never the `@aglyn/aglyn` barrel: the console\n// server's boot reaches this file through the CRM's setup writers.\nimport {\n CONTACT_FIELD_DEFAULT_CURRENCY,\n CONTACT_FIELD_TYPES,\n CONTACT_FIELDS_MAX_PER_ORG,\n fieldDefinitionsForObject,\n isCustomFieldCurrency,\n isContactFieldType,\n} from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport {\n type ContactFieldDefinition,\n type ContactFieldType,\n type CrmDealStage,\n type CrmFieldObject,\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n crmFieldListFields,\n DEFAULT_DEAL_STAGES,\n isCrmFieldObject,\n normalizeContactFieldKey,\n} from '@aglyn/aglyn/app-utils/crm'\nimport type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n activePipelines,\n clampProbability,\n newPipelineDocument,\n newStageId,\n PIPELINE_NAME_MAX,\n type PipelineDoc,\n type PipelineWriteContext,\n pipelineNameProblem,\n renumberStages,\n sortedStages,\n stagesProblem,\n} from './deal-board-model'\n\n/**\n * CRM SETUP ANOTHER PLUGIN ASKS FOR (AGL-3746): a deal pipeline with its\n * stages, and a custom field on a CRM object — what a person sets up in\n * CRM → Deals → Pipelines and CRM → Fields, as pure rules.\n *\n * Aglyn AI's build asks for them through the CRM's writers on the\n * resource-drafts seam (`server/crm-setup-writers.server.ts`). Neither has a\n * draft state: a pipeline and a field are in use once saved, as the console's\n * own saves are. So every rule here is the console's, read from the modules\n * the console reads:\n *\n * - A PIPELINE is `newPipelineDocument` with the stages asked for in place\n * of the default set, held to `stagesProblem` (exactly one Won and one\n * Lost, at least one open stage) and named under `pipelineNameProblem`\n * (unique among the active pipelines, without case). Stage ids are\n * `newStageId`'s slugs.\n * - A FIELD is the Fields section's create: a key derived from the label by\n * `normalizeContactFieldKey`, one of `CONTACT_FIELD_TYPES`, the choices\n * the drawer allows, a currency field's ISO 4217 code (USD when unsaid,\n * as the drawer's default), landing last on its object's tab, org-wide.\n *\n * IDEMPOTENT BY NAME. Asked twice, nothing is made twice: an active pipeline\n * is matched by name and a stage within it by name, a field by label or key\n * within its object, all without case. A match is brought to what was\n * asked — missing stages added, stages put in the asked order, a\n * probability set — and nothing is ever removed: a stage may hold deals and\n * a field values, and removing either is a person's decision.\n */\n\n// ── Pipelines ─────────────────────────────────────────────────────────────\n\n/** The resource the CRM's pipeline writer is registered under. */\nexport const CRM_PIPELINE_SETUP_RESOURCE = 'crm-pipeline'\n\n/** The longest stage name, as long as a pipeline's name. */\nexport const CRM_SETUP_STAGE_NAME_MAX = PIPELINE_NAME_MAX\n\n/** The most open stages one ask sets; a board wider than this does not read. */\nexport const CRM_SETUP_OPEN_STAGES_MAX = 12\n\n/** A pipeline as one ask states it, once read. */\nexport interface CrmPipelineSetup {\n name: string\n /** An active pipeline to rename to `name`, by its current name. */\n renameFrom: string | null\n /** The open stages, in order, each with the probability asked for, if any. */\n stages: Array<{ name: string; probability: number | null }>\n /** What the Won and Lost stages are called, where the ask says. */\n wonStage: string | null\n lostStage: string | null\n /** Whether this becomes the pipeline a new deal lands in. */\n makeDefault: boolean\n}\n\nconst WON_NAME = /^(closed[\\s-]*)?won$/i\nconst LOST_NAME = /^(closed[\\s-]*)?lost$/i\n/** \"Discovery (20%)\": a stage's name with its probability. */\nconst STAGE_WITH_PROBABILITY = /^(.*?)\\s*\\(\\s*(\\d{1,3}(?:\\.\\d+)?)\\s*%?\\s*\\)\\s*$/\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n}\n\nconst same = (a: string, b: string) => a.trim().toLowerCase() === b.trim().toLowerCase()\n\n/** One listed stage, read: its name and the probability written after it. */\nexport function readCrmSetupStage(entry: string): { name: string; probability: number | null } {\n const raw = text(entry)\n const match = STAGE_WITH_PROBABILITY.exec(raw)\n if (!match) return { name: raw, probability: null }\n return { name: text(match[1]), probability: clampProbability(match[2]) }\n}\n\n/**\n * A pipeline's content, read and judged. Pure. A stage listed as Won or\n * Lost (\"Won\", \"Closed Lost\") names the closing stage rather than adding an\n * open one, since every pipeline already has exactly one of each.\n */\nexport function readCrmPipelineSetup(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CrmPipelineSetup } | { ok: false; problems: string[] } {\n const problems: string[] = []\n const name = text(content['name'])\n if (!name) problems.push('A pipeline needs a name.')\n else if (name.length > PIPELINE_NAME_MAX) problems.push(`A pipeline's name is at most ${PIPELINE_NAME_MAX} characters.`)\n let wonStage = text(content['wonStage']) || null\n let lostStage = text(content['lostStage']) || null\n const listed = Array.isArray(content['stages']) ? content['stages'] : []\n const stages: CrmPipelineSetup['stages'] = []\n for (const entry of listed) {\n if (typeof entry !== 'string') {\n problems.push('Each stage is a name.')\n break\n }\n const stage = readCrmSetupStage(entry)\n if (!stage.name) continue\n if (WON_NAME.test(stage.name)) {\n wonStage ??= stage.name\n continue\n }\n if (LOST_NAME.test(stage.name)) {\n lostStage ??= stage.name\n continue\n }\n if (stage.name.length > CRM_SETUP_STAGE_NAME_MAX) {\n problems.push(`A stage's name is at most ${CRM_SETUP_STAGE_NAME_MAX} characters.`)\n break\n }\n if (stages.some((one) => same(one.name, stage.name))) continue\n stages.push(stage)\n }\n if (stages.length > CRM_SETUP_OPEN_STAGES_MAX) {\n problems.push(`A pipeline is set up with at most ${CRM_SETUP_OPEN_STAGES_MAX} open stages.`)\n }\n for (const closing of [wonStage, lostStage]) {\n if (closing && closing.length > CRM_SETUP_STAGE_NAME_MAX) {\n problems.push(`A stage's name is at most ${CRM_SETUP_STAGE_NAME_MAX} characters.`)\n }\n }\n if (wonStage && lostStage && same(wonStage, lostStage)) problems.push('The Won and Lost stages need different names.')\n if (problems.length) return { ok: false, problems }\n return {\n ok: true,\n value: {\n name,\n renameFrom: text(content['renameFrom']) || null,\n stages,\n wonStage,\n lostStage,\n makeDefault: content['makeDefault'] === true,\n },\n }\n}\n\n/** The `crm-pipeline` writer's check: whether the content is a pipeline ask, with its name and stages. Pure. */\nexport function checkCrmPipelineSetup(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readCrmPipelineSetup(content)\n if (read.ok === false) return { ok: false, problems: read.problems }\n return { ok: true, facts: { name: read.value.name, stages: read.value.stages.map((one) => one.name) } }\n}\n\n/**\n * The probability an open stage takes when the ask names none: spread over\n * the pipeline by position, to the nearest 5 — the first of four stages 20,\n * the last 80 — so a weighted forecast grows as a deal moves right.\n */\nexport function crmSetupStageProbability(index: number, count: number): number {\n const spread = ((index + 1) / (count + 1)) * 100\n return Math.min(95, Math.max(5, Math.round(spread / 5) * 5))\n}\n\n/**\n * The stages a pipeline ends up with: the open stages asked for, in the asked\n * order, each an existing stage of that name where there is one (its id kept,\n * so the deals in it stay), then the open stages the ask did not name, as\n * they were, then Won and Lost, renamed where asked. Nothing is removed.\n */\nexport function mergeCrmSetupStages(\n existing: readonly CrmDealStage[],\n setup: Pick<CrmPipelineSetup, 'stages' | 'wonStage' | 'lostStage'>,\n): CrmDealStage[] {\n const sorted = sortedStages({ stages: [...existing] })\n const opens = sorted.filter((stage) => stage.kind === 'open')\n const taken: CrmDealStage[] = [...sorted]\n const listed: CrmDealStage[] = setup.stages.map((asked, index) => {\n const found = opens.find((stage) => same(stage.name, asked.name))\n if (found) return asked.probability === null ? found : { ...found, probability: asked.probability }\n const stage: CrmDealStage = {\n id: newStageId(asked.name, taken),\n name: asked.name,\n order: 0,\n probability: asked.probability ?? crmSetupStageProbability(index, setup.stages.length),\n kind: 'open',\n forecastCategory: 'pipeline',\n }\n taken.push(stage)\n return stage\n })\n const unlisted = opens.filter((stage) => !listed.some((one) => one.id === stage.id))\n const closing = (kind: 'won' | 'lost', name: string | null): CrmDealStage[] =>\n sorted\n .filter((stage) => stage.kind === kind)\n .map((stage) => (name ? { ...stage, name } : stage))\n return renumberStages([\n ...listed,\n ...unlisted,\n ...closing('won', setup.wonStage),\n ...closing('lost', setup.lostStage),\n ])\n}\n\n/** What saving a pipeline ask comes to against the pipelines the member's site can read. */\nexport type CrmPipelineSetupPlan =\n | {\n kind: 'create'\n document: Record<string, unknown>\n /** Pipelines whose default flag comes off, because this one takes it. */\n defaultsOff: string[]\n }\n | {\n kind: 'update'\n id: string\n name: string\n /** The fields that change; empty when the pipeline already is what was asked. */\n patch: { name?: string; stages?: CrmDealStage[]; isDefault?: true }\n defaultsOff: string[]\n }\n | { kind: 'refuse'; error: string }\n\n/**\n * Create, or bring an existing pipeline to the ask. Pure.\n *\n * The pipeline is the active one called `renameFrom`, else the active one\n * called `name`; with neither, a new one, which is also the default when the\n * site reads no active pipeline yet (the board's seed would have made it so)\n * or when asked.\n */\nexport function planCrmPipelineSetup(\n setup: CrmPipelineSetup,\n pipelines: readonly PipelineDoc[],\n context: PipelineWriteContext,\n): CrmPipelineSetupPlan {\n const active = activePipelines(pipelines)\n const named = (name: string | null) => (name ? active.find((one) => same(String(one.name ?? ''), name)) : undefined)\n const target = named(setup.renameFrom) ?? named(setup.name)\n const defaultsOff = (keep: string | null) =>\n active.filter((one) => one.isDefault && one.$id !== keep).map((one) => one.$id)\n\n if (!target) {\n const problem = pipelineNameProblem(setup.name, pipelines)\n if (problem) return { kind: 'refuse', error: problem }\n // Stages asked for replace the default open stages; none asked keeps\n // them all, as the console's New pipeline does.\n const stages = mergeCrmSetupStages(\n setup.stages.length ? DEFAULT_DEAL_STAGES.filter((stage) => stage.kind !== 'open') : DEFAULT_DEAL_STAGES,\n setup,\n )\n const invalid = stagesProblem(stages)\n if (invalid) return { kind: 'refuse', error: invalid }\n const isDefault = setup.makeDefault || active.length === 0\n return {\n kind: 'create',\n document: { ...newPipelineDocument(setup.name, context), stages, isDefault },\n defaultsOff: isDefault ? defaultsOff(null) : [],\n }\n }\n\n const patch: { name?: string; stages?: CrmDealStage[]; isDefault?: true } = {}\n // A name that differs only in case is the same pipeline, and keeps its own.\n if (!same(String(target.name ?? ''), setup.name)) {\n const problem = pipelineNameProblem(setup.name, pipelines, target.$id)\n if (problem) return { kind: 'refuse', error: problem }\n patch.name = setup.name\n }\n const current = sortedStages(target)\n const stages = mergeCrmSetupStages(current, setup)\n const invalid = stagesProblem(stages)\n if (invalid) return { kind: 'refuse', error: invalid }\n if (JSON.stringify(stages) !== JSON.stringify(renumberStages(current))) patch.stages = stages\n if (setup.makeDefault && !target.isDefault) patch.isDefault = true\n return {\n kind: 'update',\n id: target.$id,\n name: patch.name ?? target.name,\n patch,\n defaultsOff: patch.isDefault ? defaultsOff(target.$id) : [],\n }\n}\n\n/** What a pipeline's writer reports: its open stages in order, and its closing stages. */\nexport function crmPipelineSetupFacts(stages: readonly CrmDealStage[]): Readonly<Record<string, unknown>> {\n const sorted = sortedStages({ stages: [...stages] })\n return {\n stages: sorted.filter((stage) => stage.kind === 'open').map((stage) => `${stage.name} (${stage.probability}%)`),\n wonStage: sorted.find((stage) => stage.kind === 'won')?.name ?? null,\n lostStage: sorted.find((stage) => stage.kind === 'lost')?.name ?? null,\n }\n}\n\n// ── Custom fields ─────────────────────────────────────────────────────────\n\n/** The resource the CRM's custom field writer is registered under. */\nexport const CRM_FIELD_SETUP_RESOURCE = 'crm-field'\n\n/** How many choices a Choice field may declare, and how long each and a label may be: the field drawer's. */\nexport const CRM_FIELD_OPTIONS_MAX = 50\nexport const CRM_FIELD_OPTION_MAX_LENGTH = 120\nexport const CRM_FIELD_LABEL_MAX_LENGTH = 80\n\n/** One choice per entry, trimmed, blanks dropped, duplicates collapsed, at most the drawer's count. */\nexport function crmFieldOptions(entries: readonly string[]): string[] {\n const seen = new Set<string>()\n const options: string[] = []\n for (const entry of entries) {\n const option = String(entry ?? '').trim().slice(0, CRM_FIELD_OPTION_MAX_LENGTH)\n if (!option || seen.has(option)) continue\n seen.add(option)\n options.push(option)\n if (options.length >= CRM_FIELD_OPTIONS_MAX) break\n }\n return options\n}\n\n/** A custom field as one ask states it, once read. */\nexport interface CrmFieldSetup {\n object: CrmFieldObject\n label: string\n /** The key its values are stored under, derived from the label. */\n key: string\n type: ContactFieldType\n /** The choices, for `select`; empty otherwise. */\n options: string[]\n /** For `currency` (AGL-3756): the lowercase ISO 4217 code; `null` otherwise. */\n currency: string | null\n required: boolean\n}\n\n/** A field's content, read and judged. Pure. */\nexport function readCrmFieldSetup(\n content: Readonly<Record<string, unknown>>,\n): { ok: true; value: CrmFieldSetup } | { ok: false; problems: string[] } {\n const problems: string[] = []\n const object = content['object']\n if (!isCrmFieldObject(object)) problems.push(`A field describes one of ${CRM_FIELD_OBJECTS.join(', ')}.`)\n const type = content['type']\n if (!isContactFieldType(type)) problems.push(`A field's type is one of ${CONTACT_FIELD_TYPES.join(', ')}.`)\n const label = text(content['label'])\n const key = normalizeContactFieldKey(label)\n if (!label) problems.push('A field needs a label.')\n else if (label.length > CRM_FIELD_LABEL_MAX_LENGTH) {\n problems.push(`A field's label is at most ${CRM_FIELD_LABEL_MAX_LENGTH} characters.`)\n } else if (!key) problems.push('A field’s label needs a letter to make its key from.')\n const listed = Array.isArray(content['options']) ? content['options'].filter((one): one is string => typeof one === 'string') : []\n const options = type === 'select' ? crmFieldOptions(listed) : []\n if (type === 'select' && !options.length) problems.push('A Choice field needs its choices.')\n // A currency field's code, as the drawer offers it: named, or the default.\n const asked = text(content['currency']).toLowerCase()\n if (type === 'currency' && asked && !isCustomFieldCurrency(asked)) {\n problems.push('A currency field’s currency is an ISO 4217 code, such as USD or EUR.')\n }\n if (problems.length) return { ok: false, problems }\n return {\n ok: true,\n value: {\n object: object as CrmFieldObject,\n label,\n key: key as string,\n type: type as ContactFieldType,\n options,\n currency: type === 'currency' ? asked || CONTACT_FIELD_DEFAULT_CURRENCY : null,\n required: content['required'] === true,\n },\n }\n}\n\n/** The `crm-field` writer's check: whether the content is a field ask, with the key it derives. Pure. */\nexport function checkCrmFieldSetup(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readCrmFieldSetup(content)\n if (read.ok === false) return { ok: false, problems: read.problems }\n return {\n ok: true,\n facts: {\n object: read.value.object,\n key: read.value.key,\n type: read.value.type,\n ...(read.value.currency ? { currency: read.value.currency } : {}),\n },\n }\n}\n\n/** A stored definition, as the writer reads one: its document id beside it. */\nexport type CrmFieldDefinitionDoc = ContactFieldDefinition & { $id: string }\n\n/** What saving a field ask comes to against every definition the org holds. */\nexport type CrmFieldSetupPlan =\n | { kind: 'create'; document: Record<string, unknown> }\n /** The field exists; `options` are the choices its Choice list gains, empty when none. */\n | { kind: 'existing'; id: string; label: string; options: string[] }\n | { kind: 'refuse'; status: 403 | 409; error: string }\n\n/**\n * Create the field, or find it. Pure.\n *\n * An active field on the same object with the same label or key is the\n * field: a re-run makes nothing, and a Choice field gains only the choices it\n * lacks. A retired field keeps its key, as the drawer says, so a key a\n * retired field holds is refused rather than reused — its old values would\n * read back as the new field's. The org's ceiling counts every definition,\n * retired ones included, as the Fields section's read does.\n */\nexport function planCrmFieldSetup(\n setup: CrmFieldSetup,\n definitions: readonly CrmFieldDefinitionDoc[],\n context: { hostId: string; now: Date; visibleTo: readonly string[] },\n): CrmFieldSetupPlan {\n const ofObject = fieldDefinitionsForObject(definitions, setup.object)\n const match = ofObject.find(\n (one) => !one.retiredAt && (same(String(one.label ?? ''), setup.label) || one.key === setup.key),\n )\n if (match) {\n const missing =\n match.type === 'select' && setup.type === 'select'\n ? setup.options.filter((option) => !(match.options ?? []).includes(option))\n : []\n const room = Math.max(0, CRM_FIELD_OPTIONS_MAX - (match.options ?? []).length)\n return { kind: 'existing', id: match.$id, label: match.label, options: missing.slice(0, room) }\n }\n const retired = ofObject.find((one) => one.retiredAt && one.key === setup.key)\n if (retired) {\n return {\n kind: 'refuse',\n status: 409,\n error:\n `A retired ${CRM_FIELD_OBJECT_LABELS[setup.object].toLowerCase()} field, “${retired.label}”, keeps the key ` +\n `“${setup.key}”. Restore it in CRM → Fields, or give this field another name.`,\n }\n }\n if (definitions.length >= CONTACT_FIELDS_MAX_PER_ORG) {\n return {\n kind: 'refuse',\n status: 403,\n error: `An organization keeps at most ${CONTACT_FIELDS_MAX_PER_ORG} custom fields. Retire or delete one in CRM → Fields first.`,\n }\n }\n const order = ofObject.reduce((highest, one) => Math.max(highest, Number(one.order) || 0), -1) + 1\n return {\n kind: 'create',\n document: {\n key: setup.key,\n label: setup.label,\n type: setup.type,\n ...(setup.type === 'select' ? { options: setup.options } : {}),\n ...(setup.type === 'currency' && setup.currency ? { currency: setup.currency } : {}),\n required: setup.required,\n order,\n retiredAt: null,\n ...crmFieldListFields({ key: setup.key, label: setup.label, required: setup.required, object: setup.object }),\n hostId: context.hostId,\n visibleTo: [...context.visibleTo],\n createdAt: context.now,\n updatedAt: context.now,\n },\n }\n}\n"],"names":["CONTACT_FIELD_DEFAULT_CURRENCY","CONTACT_FIELD_TYPES","CONTACT_FIELDS_MAX_PER_ORG","fieldDefinitionsForObject","isCustomFieldCurrency","isContactFieldType","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","crmFieldListFields","DEFAULT_DEAL_STAGES","isCrmFieldObject","normalizeContactFieldKey","activePipelines","clampProbability","newPipelineDocument","newStageId","PIPELINE_NAME_MAX","pipelineNameProblem","renumberStages","sortedStages","stagesProblem","CRM_PIPELINE_SETUP_RESOURCE","CRM_SETUP_STAGE_NAME_MAX","CRM_SETUP_OPEN_STAGES_MAX","WON_NAME","LOST_NAME","STAGE_WITH_PROBABILITY","text","value","replace","trim","same","a","b","toLowerCase","readCrmSetupStage","entry","raw","match","exec","name","probability","readCrmPipelineSetup","content","problems","push","length","wonStage","lostStage","listed","Array","isArray","stages","stage","test","some","one","closing","ok","renameFrom","makeDefault","checkCrmPipelineSetup","read","facts","map","crmSetupStageProbability","index","count","spread","Math","min","max","round","mergeCrmSetupStages","existing","setup","sorted","opens","filter","kind","taken","asked","found","find","id","order","forecastCategory","unlisted","planCrmPipelineSetup","pipelines","context","named","target","patch","active","String","undefined","defaultsOff","keep","isDefault","$id","problem","error","invalid","document","current","JSON","stringify","crmPipelineSetupFacts","CRM_FIELD_SETUP_RESOURCE","CRM_FIELD_OPTIONS_MAX","CRM_FIELD_OPTION_MAX_LENGTH","CRM_FIELD_LABEL_MAX_LENGTH","crmFieldOptions","entries","seen","Set","options","option","slice","has","add","readCrmFieldSetup","object","join","type","label","key","currency","required","checkCrmFieldSetup","planCrmFieldSetup","definitions","ofObject","retiredAt","missing","includes","room","retired","status","reduce","highest","Number","hostId","visibleTo","createdAt","now","updatedAt"],"mappings":"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"}
|