@ketvietlab/ketsuite 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/modules/backend/design/HANDOFF.md +2 -2
  7. package/dist/modules/backend/design/auth.css +12 -0
  8. package/dist/modules/backend/design/client/table-selection.d.mts +3 -3
  9. package/dist/modules/backend/design/client/table-selection.d.mts.map +1 -1
  10. package/dist/modules/backend/design/client/table-selection.mjs +1 -1
  11. package/dist/modules/backend/design/client/table-selection.mjs.map +4 -4
  12. package/dist/modules/backend/design/design-system.css +1 -1
  13. package/dist/modules/backend/design/forms.css +62 -1
  14. package/dist/modules/backend/design/lists.css +26 -18
  15. package/dist/modules/backend/design/record.css +5 -2
  16. package/dist/modules/backend/design/responsive.css +5 -1
  17. package/dist/modules/backend/design/user-workflow.css +257 -0
  18. package/dist/modules/backend/index.d.ts.map +1 -1
  19. package/dist/modules/backend/index.js +1 -0
  20. package/dist/modules/backend/index.js.map +1 -1
  21. package/dist/modules/backend/messages.d.ts.map +1 -1
  22. package/dist/modules/backend/messages.js +2 -0
  23. package/dist/modules/backend/messages.js.map +1 -1
  24. package/dist/modules/crm/functions.d.ts +5 -0
  25. package/dist/modules/crm/functions.d.ts.map +1 -1
  26. package/dist/modules/crm/functions.js +105 -10
  27. package/dist/modules/crm/functions.js.map +1 -1
  28. package/dist/modules/crm/jobs.d.ts.map +1 -1
  29. package/dist/modules/crm/jobs.js +1 -0
  30. package/dist/modules/crm/jobs.js.map +1 -1
  31. package/dist/modules/crm/operations.d.ts.map +1 -1
  32. package/dist/modules/crm/operations.js +2 -1
  33. package/dist/modules/crm/operations.js.map +1 -1
  34. package/dist/modules/crm_backend/index.d.ts.map +1 -1
  35. package/dist/modules/crm_backend/index.js +5 -2
  36. package/dist/modules/crm_backend/index.js.map +1 -1
  37. package/dist/modules/crm_backend/messages.d.ts +116 -0
  38. package/dist/modules/crm_backend/messages.d.ts.map +1 -1
  39. package/dist/modules/crm_backend/messages.js +122 -6
  40. package/dist/modules/crm_backend/messages.js.map +1 -1
  41. package/dist/modules/crm_backend/routes.d.ts.map +1 -1
  42. package/dist/modules/crm_backend/routes.js +434 -174
  43. package/dist/modules/crm_backend/routes.js.map +1 -1
  44. package/dist/modules/crm_backend/screens/case-create.d.ts +7 -0
  45. package/dist/modules/crm_backend/screens/case-create.d.ts.map +1 -1
  46. package/dist/modules/crm_backend/screens/case-create.js +13 -4
  47. package/dist/modules/crm_backend/screens/case-create.js.map +1 -1
  48. package/dist/modules/crm_backend/screens/case-detail.d.ts +6 -0
  49. package/dist/modules/crm_backend/screens/case-detail.d.ts.map +1 -1
  50. package/dist/modules/crm_backend/screens/case-detail.js +73 -23
  51. package/dist/modules/crm_backend/screens/case-detail.js.map +1 -1
  52. package/dist/modules/crm_backend/screens/configuration.d.ts +21 -3
  53. package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
  54. package/dist/modules/crm_backend/screens/configuration.js +260 -55
  55. package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
  56. package/dist/modules/crm_backend/screens/index.d.ts +3 -3
  57. package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
  58. package/dist/modules/crm_backend/screens/index.js +2 -2
  59. package/dist/modules/crm_backend/screens/index.js.map +1 -1
  60. package/dist/modules/loyalty/admin-functions.d.ts +2 -0
  61. package/dist/modules/loyalty/admin-functions.d.ts.map +1 -1
  62. package/dist/modules/loyalty/admin-functions.js +96 -20
  63. package/dist/modules/loyalty/admin-functions.js.map +1 -1
  64. package/dist/modules/loyalty/engine.d.ts +4 -1
  65. package/dist/modules/loyalty/engine.d.ts.map +1 -1
  66. package/dist/modules/loyalty/engine.js +125 -18
  67. package/dist/modules/loyalty/engine.js.map +1 -1
  68. package/dist/modules/loyalty/index.d.ts.map +1 -1
  69. package/dist/modules/loyalty/index.js +4 -3
  70. package/dist/modules/loyalty/index.js.map +1 -1
  71. package/dist/modules/loyalty/jobs.d.ts.map +1 -1
  72. package/dist/modules/loyalty/jobs.js +0 -1
  73. package/dist/modules/loyalty/jobs.js.map +1 -1
  74. package/dist/modules/loyalty/membership-functions.d.ts +5 -0
  75. package/dist/modules/loyalty/membership-functions.d.ts.map +1 -1
  76. package/dist/modules/loyalty/membership-functions.js +37 -23
  77. package/dist/modules/loyalty/membership-functions.js.map +1 -1
  78. package/dist/modules/loyalty/messages.d.ts +14 -0
  79. package/dist/modules/loyalty/messages.d.ts.map +1 -1
  80. package/dist/modules/loyalty/messages.js +14 -0
  81. package/dist/modules/loyalty/messages.js.map +1 -1
  82. package/dist/modules/loyalty/models.d.ts.map +1 -1
  83. package/dist/modules/loyalty/models.js +17 -0
  84. package/dist/modules/loyalty/models.js.map +1 -1
  85. package/dist/modules/loyalty/order-functions.d.ts.map +1 -1
  86. package/dist/modules/loyalty/order-functions.js +70 -17
  87. package/dist/modules/loyalty/order-functions.js.map +1 -1
  88. package/dist/modules/loyalty/program-design.d.ts +11 -0
  89. package/dist/modules/loyalty/program-design.d.ts.map +1 -0
  90. package/dist/modules/loyalty/program-design.js +356 -0
  91. package/dist/modules/loyalty/program-design.js.map +1 -0
  92. package/dist/modules/loyalty/stats.d.ts +0 -13
  93. package/dist/modules/loyalty/stats.d.ts.map +1 -1
  94. package/dist/modules/loyalty/stats.js +11 -6
  95. package/dist/modules/loyalty/stats.js.map +1 -1
  96. package/dist/modules/loyalty/stored-value.d.ts.map +1 -1
  97. package/dist/modules/loyalty/stored-value.js +7 -2
  98. package/dist/modules/loyalty/stored-value.js.map +1 -1
  99. package/dist/modules/loyalty/types.d.ts +1 -1
  100. package/dist/modules/loyalty/types.d.ts.map +1 -1
  101. package/dist/modules/loyalty_backend/design-messages.d.ts +221 -0
  102. package/dist/modules/loyalty_backend/design-messages.d.ts.map +1 -0
  103. package/dist/modules/loyalty_backend/design-messages.js +221 -0
  104. package/dist/modules/loyalty_backend/design-messages.js.map +1 -0
  105. package/dist/modules/loyalty_backend/index.js +224 -412
  106. package/dist/modules/loyalty_backend/index.js.map +1 -1
  107. package/dist/modules/loyalty_backend/messages.d.ts +642 -214
  108. package/dist/modules/loyalty_backend/messages.d.ts.map +1 -1
  109. package/dist/modules/loyalty_backend/messages.js +19 -13
  110. package/dist/modules/loyalty_backend/messages.js.map +1 -1
  111. package/dist/modules/loyalty_backend/program-workspace.d.ts +4 -0
  112. package/dist/modules/loyalty_backend/program-workspace.d.ts.map +1 -0
  113. package/dist/modules/loyalty_backend/program-workspace.js +600 -0
  114. package/dist/modules/loyalty_backend/program-workspace.js.map +1 -0
  115. package/dist/modules/loyalty_backend/screens/index.d.ts +12 -36
  116. package/dist/modules/loyalty_backend/screens/index.d.ts.map +1 -1
  117. package/dist/modules/loyalty_backend/screens/index.js +148 -292
  118. package/dist/modules/loyalty_backend/screens/index.js.map +1 -1
  119. package/dist/modules/loyalty_backend/screens/page-frame.d.ts.map +1 -1
  120. package/dist/modules/loyalty_backend/screens/page-frame.js +2 -2
  121. package/dist/modules/loyalty_backend/screens/page-frame.js.map +1 -1
  122. package/dist/modules/loyalty_pos/functions.d.ts.map +1 -1
  123. package/dist/modules/loyalty_pos/functions.js +1 -0
  124. package/dist/modules/loyalty_pos/functions.js.map +1 -1
  125. package/dist/modules/loyalty_pos/index.js +1 -1
  126. package/dist/modules/loyalty_pos/index.js.map +1 -1
  127. package/dist/modules/loyalty_sale/functions.d.ts +1 -0
  128. package/dist/modules/loyalty_sale/functions.d.ts.map +1 -1
  129. package/dist/modules/loyalty_sale/functions.js +1 -0
  130. package/dist/modules/loyalty_sale/functions.js.map +1 -1
  131. package/dist/modules/partner_backend/client/partner.css +3 -1
  132. package/dist/modules/partner_backend/index.d.ts.map +1 -1
  133. package/dist/modules/partner_backend/index.js +4 -0
  134. package/dist/modules/partner_backend/index.js.map +1 -1
  135. package/dist/modules/partner_backend/routes.d.ts.map +1 -1
  136. package/dist/modules/partner_backend/routes.js +6 -1
  137. package/dist/modules/partner_backend/routes.js.map +1 -1
  138. package/dist/modules/partner_backend/screens/form.d.ts +1 -0
  139. package/dist/modules/partner_backend/screens/form.d.ts.map +1 -1
  140. package/dist/modules/partner_backend/screens/form.js +1 -0
  141. package/dist/modules/partner_backend/screens/form.js.map +1 -1
  142. package/dist/modules/user/authorization.d.ts +64 -1
  143. package/dist/modules/user/authorization.d.ts.map +1 -1
  144. package/dist/modules/user/authorization.js +445 -53
  145. package/dist/modules/user/authorization.js.map +1 -1
  146. package/dist/modules/user/functions.d.ts.map +1 -1
  147. package/dist/modules/user/functions.js +45 -23
  148. package/dist/modules/user/functions.js.map +1 -1
  149. package/dist/modules/user/index.d.ts +2 -1
  150. package/dist/modules/user/index.d.ts.map +1 -1
  151. package/dist/modules/user/index.js +1 -1
  152. package/dist/modules/user/index.js.map +1 -1
  153. package/dist/modules/user/models.d.ts.map +1 -1
  154. package/dist/modules/user/models.js +5 -0
  155. package/dist/modules/user/models.js.map +1 -1
  156. package/dist/modules/user/roles.d.ts.map +1 -1
  157. package/dist/modules/user/roles.js +2 -1
  158. package/dist/modules/user/roles.js.map +1 -1
  159. package/dist/modules/user_backend/access-routes.d.ts +31 -0
  160. package/dist/modules/user_backend/access-routes.d.ts.map +1 -0
  161. package/dist/modules/user_backend/access-routes.js +447 -0
  162. package/dist/modules/user_backend/access-routes.js.map +1 -0
  163. package/dist/modules/user_backend/index.d.ts +1 -0
  164. package/dist/modules/user_backend/index.d.ts.map +1 -1
  165. package/dist/modules/user_backend/index.js +1 -0
  166. package/dist/modules/user_backend/index.js.map +1 -1
  167. package/dist/modules/user_backend/routes.d.ts.map +1 -1
  168. package/dist/modules/user_backend/routes.js +10 -2
  169. package/dist/modules/user_backend/routes.js.map +1 -1
  170. package/dist/permission-catalogue.d.ts.map +1 -1
  171. package/dist/permission-catalogue.js +7 -0
  172. package/dist/permission-catalogue.js.map +1 -1
  173. package/dist/scaffold/index.js +1 -1
  174. package/dist/ui/client/table-selection-view.d.ts.map +1 -1
  175. package/dist/ui/client/table-selection-view.js +95 -1
  176. package/dist/ui/client/table-selection-view.js.map +1 -1
  177. package/dist/ui/client/user-workflow.d.ts +2 -0
  178. package/dist/ui/client/user-workflow.d.ts.map +1 -0
  179. package/dist/ui/client/user-workflow.js +141 -0
  180. package/dist/ui/client/user-workflow.js.map +1 -0
  181. package/dist/ui/form.d.ts +7 -0
  182. package/dist/ui/form.d.ts.map +1 -1
  183. package/dist/ui/form.js +28 -15
  184. package/dist/ui/form.js.map +1 -1
  185. package/dist/ui/icons.d.ts.map +1 -1
  186. package/dist/ui/icons.js +2 -0
  187. package/dist/ui/icons.js.map +1 -1
  188. package/dist/ui/index.d.ts +5 -1
  189. package/dist/ui/index.d.ts.map +1 -1
  190. package/dist/ui/index.js +4 -1
  191. package/dist/ui/index.js.map +1 -1
  192. package/dist/ui/layout.d.ts.map +1 -1
  193. package/dist/ui/layout.js +14 -4
  194. package/dist/ui/layout.js.map +1 -1
  195. package/dist/ui/nav.d.ts.map +1 -1
  196. package/dist/ui/nav.js +2 -1
  197. package/dist/ui/nav.js.map +1 -1
  198. package/dist/ui/table.js +1 -1
  199. package/dist/ui/table.js.map +1 -1
  200. package/dist/ui/user-workflow.d.ts +72 -0
  201. package/dist/ui/user-workflow.d.ts.map +1 -0
  202. package/dist/ui/user-workflow.js +144 -0
  203. package/dist/ui/user-workflow.js.map +1 -0
  204. package/package.json +8 -4
@@ -1,13 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { encodeListState, parseListState, table, text } from '@ketvietlab/ketjs';
3
3
  import { formatDateTime, formatMoney, modalWorkspace } from '../../ui/index.js';
4
- import { readForm, seeOther } from '../backend/forms.js';
4
+ import { formRefusal, readForm, seeOther } from '../backend/forms.js';
5
5
  import { choices, adminPage, inLocale, localeQuery, optional, timezoneOf } from '../backend/screen.js';
6
6
  import { withParam } from '../backend/paging.js';
7
7
  import { receiveAttachment } from '../storage/routes.js';
8
8
  import { caseListSearch } from '../crm/search.js';
9
+ import { caseFormSchema } from '../crm/functions.js';
9
10
  import { assigneeControl, caseControl, partnerControl, productControl, stageControl, tagsControl, teamControl, } from './relation-control.js';
10
- import { CONFIGURATION_TABS, caseConvertModal, caseCreateScreen, caseDetailScreen, casesListScreen, configurationScreen, leaderboardScreen, plannerScreen, permissionScreen, pipelineScreen, } from './screens/index.js';
11
+ import { CONFIGURATION_TABS, caseCloseModal, caseConvertModal, caseCreateScreen, caseDetailScreen, casesListScreen, configurationScreen, leaderboardScreen, plannerScreen, permissionScreen, pipelineScreen, teamConfigurationScreen, } from './screens/index.js';
11
12
  import { keepForListSearch, LIST_PAGE_SIZE, listFacets, listMenus, loadListGroups, } from '../backend/list-search.js';
12
13
  const bool = (value) => ['1', 'true', 'on'].includes(value ?? '');
13
14
  const errorsOf = (result, _) => (result?.errors ?? []).map((error) => {
@@ -61,72 +62,105 @@ const references = async (ctx, url, req) => {
61
62
  return { config, partners, users, tags };
62
63
  };
63
64
  const options = (rows) => rows.map((row) => ({ value: String(row.id), label: String(row.name ?? row.code ?? row.id) }));
64
- const caseFields = (_, row = {}, controls = {}) => [
65
- {
66
- name: 'name',
67
- label: _('crm_backend.field.name'),
68
- value: String(row.name ?? ''),
69
- required: true,
70
- span: 'full',
71
- },
72
- {
73
- name: 'kind',
74
- label: _('crm_backend.field.kind'),
75
- type: 'select',
76
- value: String(row.kind ?? 'lead'),
77
- disabled: Boolean(row.id),
78
- required: true,
79
- options: ['lead', 'opportunity'].map((value) => ({ value, label: _(`crm.kind.${value}`) })),
80
- },
81
- /*
82
- * Only where creating one. On a record that already exists the stage is moved
83
- * by the action beside the form, which records the move on the timeline and
84
- * refuses a stale version; a second field quietly writing the same column would
85
- * be a change nobody could later account for.
86
- */
87
- ...(controls.stage
88
- ? [{ name: 'stageId', label: _('crm_backend.field.stage'), control: controls.stage }]
89
- : []),
90
- { name: 'partnerId', label: _('crm_backend.field.partner'), control: controls.partner },
91
- { name: 'contactName', label: _('crm_backend.field.contactName'), value: String(row.contactName ?? '') },
92
- { name: 'email', label: _('crm_backend.field.email'), type: 'email', value: String(row.email ?? '') },
93
- { name: 'phone', label: _('crm_backend.field.phone'), type: 'tel', value: String(row.phone ?? '') },
94
- { name: 'teamId', label: _('crm_backend.field.team'), control: controls.team },
95
- { name: 'assigneeUserId', label: _('crm_backend.field.assignee'), control: controls.assignee },
96
- {
97
- name: 'priority',
98
- label: _('crm_backend.field.priority'),
99
- type: 'select',
100
- value: String(row.priority ?? '1'),
101
- options: ['0', '1', '2', '3'].map((value) => ({ value, label: _(`crm_backend.priority.${value}`) })),
102
- },
103
- { name: 'tagIds', label: _('crm_backend.field.tags'), control: controls.tags, span: 'full' },
104
- {
105
- name: 'expectedRevenue',
106
- label: _('crm_backend.field.expectedRevenue'),
107
- type: 'decimal',
108
- value: String(row.salesDetail?.expectedRevenue ?? 0),
109
- },
110
- {
111
- name: 'probability',
112
- label: _('crm_backend.field.probability'),
113
- type: 'decimal',
114
- value: String(row.salesDetail?.probability ?? 0),
115
- },
116
- {
117
- name: 'expectedClosing',
118
- label: _('crm_backend.field.expectedClosing'),
119
- type: 'date',
120
- value: String(row.salesDetail?.expectedClosing ?? ''),
121
- },
122
- {
123
- name: 'description',
124
- label: _('crm_backend.field.description'),
125
- type: 'textarea',
126
- value: String(row.description ?? ''),
127
- span: 'full',
128
- },
129
- ];
65
+ const caseFields = (_, row = {}, controls = {}, requirements = {}) => {
66
+ const source = String(row.utmSource ?? (requirements.need ? 'pancake' : ''));
67
+ const sources = ['pancake', 'zalo', 'facebook', 'website', 'customer_care', 'referral'];
68
+ if (source && !sources.includes(source))
69
+ sources.push(source);
70
+ return [
71
+ {
72
+ name: 'name',
73
+ label: _('crm_backend.field.name'),
74
+ value: String(row.name ?? ''),
75
+ required: true,
76
+ span: 'full',
77
+ error: requirements.error?.('name'),
78
+ },
79
+ {
80
+ name: 'kind',
81
+ label: _('crm_backend.field.kind'),
82
+ type: 'select',
83
+ value: String(row.kind ?? 'lead'),
84
+ disabled: Boolean(row.id),
85
+ required: true,
86
+ error: requirements.error?.('kind'),
87
+ options: ['lead', 'opportunity'].map((value) => ({ value, label: _(`crm.kind.${value}`) })),
88
+ },
89
+ /*
90
+ * Only where creating one. On a record that already exists the stage is moved
91
+ * by the action beside the form, which records the move on the timeline and
92
+ * refuses a stale version; a second field quietly writing the same column would
93
+ * be a change nobody could later account for.
94
+ */
95
+ ...(controls.stage
96
+ ? [{ name: 'stageId', label: _('crm_backend.field.stage'), control: controls.stage }]
97
+ : []),
98
+ {
99
+ name: 'partnerId',
100
+ label: _('crm_backend.field.partner'),
101
+ control: controls.partner,
102
+ required: requirements.partner,
103
+ error: requirements.error?.('partnerId'),
104
+ },
105
+ {
106
+ name: 'utmSource',
107
+ label: _('crm_backend.field.source'),
108
+ type: 'select',
109
+ value: source,
110
+ options: [
111
+ { value: '', label: '—' },
112
+ ...sources.map((value) => ({
113
+ value,
114
+ label: _.resolves(`crm_backend.source.${value}`) ? _(`crm_backend.source.${value}`) : value,
115
+ })),
116
+ ],
117
+ },
118
+ { name: 'contactName', label: _('crm_backend.field.contactName'), value: String(row.contactName ?? '') },
119
+ { name: 'email', label: _('crm_backend.field.email'), type: 'email', value: String(row.email ?? '') },
120
+ { name: 'phone', label: _('crm_backend.field.phone'), type: 'tel', value: String(row.phone ?? '') },
121
+ { name: 'teamId', label: _('crm_backend.field.team'), control: controls.team },
122
+ { name: 'assigneeUserId', label: _('crm_backend.field.assignee'), control: controls.assignee },
123
+ {
124
+ name: 'priority',
125
+ label: _('crm_backend.field.priority'),
126
+ type: 'select',
127
+ value: String(row.priority ?? '1'),
128
+ options: ['0', '1', '2', '3'].map((value) => ({ value, label: _(`crm_backend.priority.${value}`) })),
129
+ error: requirements.error?.('priority'),
130
+ },
131
+ { name: 'tagIds', label: _('crm_backend.field.tags'), control: controls.tags, span: 'full' },
132
+ {
133
+ name: 'expectedRevenue',
134
+ label: _('crm_backend.field.expectedRevenue'),
135
+ type: 'decimal',
136
+ value: String(row.salesDetail?.expectedRevenue ?? 0),
137
+ error: requirements.error?.('expectedRevenue'),
138
+ },
139
+ {
140
+ name: 'probability',
141
+ label: _('crm_backend.field.probability'),
142
+ type: 'decimal',
143
+ value: String(row.salesDetail?.probability ?? 0),
144
+ error: requirements.error?.('probability'),
145
+ },
146
+ {
147
+ name: 'expectedClosing',
148
+ label: _('crm_backend.field.expectedClosing'),
149
+ type: 'date',
150
+ value: String(row.salesDetail?.expectedClosing ?? ''),
151
+ error: requirements.error?.('expectedClosing'),
152
+ },
153
+ {
154
+ name: 'description',
155
+ label: requirements.need ? _('crm_backend.field.need') : _('crm_backend.field.description'),
156
+ type: 'textarea',
157
+ value: String(row.description ?? ''),
158
+ required: requirements.need,
159
+ span: 'full',
160
+ error: requirements.error?.('description'),
161
+ },
162
+ ];
163
+ };
130
164
  const caseControls = async (ctx, url, req, _, data, prefix, row = {},
131
165
  /** The stage picker belongs to the create form only — see the note in `caseFields`. */
132
166
  wants = {}) => {
@@ -178,6 +212,7 @@ const saveInput = (id, form, kind = form.kind ?? 'lead') => ({
178
212
  ...optional(form, 'assigneeUserId'),
179
213
  ...optional(form, 'stageId'),
180
214
  ...optional(form, 'description'),
215
+ ...optional(form, 'utmSource'),
181
216
  ...optional(form, 'expectedClosing'),
182
217
  priority: form.priority ?? '1',
183
218
  expectedRevenue: form.expectedRevenue || '0',
@@ -209,13 +244,17 @@ const caseCreateHref = (url, preset = {}) => {
209
244
  target.searchParams.set('returnTo', `${url.pathname}${url.search}`);
210
245
  return `${target.pathname}${target.search}`;
211
246
  };
212
- /** Only the CRM board and case list are valid destinations carried through the create form. */
247
+ /** Only CRM views and a Partner record are valid destinations carried through the create form. */
213
248
  const caseReturnTo = (url, raw) => {
214
- const fallback = inLocale(url, '/admin/crm/cases');
249
+ const partnerId = url.searchParams.get('partnerId');
250
+ const fallback = partnerId
251
+ ? inLocale(url, `/admin/partner/partners/${encodeURIComponent(partnerId)}`)
252
+ : inLocale(url, '/admin/crm/cases');
215
253
  if (!raw)
216
254
  return fallback;
217
255
  const target = new URL(raw, 'http://ket.local');
218
- return ['/admin/crm/cases', '/admin/crm/pipeline'].includes(target.pathname)
256
+ return ['/admin/crm/cases', '/admin/crm/pipeline'].includes(target.pathname) ||
257
+ /^\/admin\/partner\/partners\/[^/]+$/.test(target.pathname)
219
258
  ? `${target.pathname}${target.search}`
220
259
  : fallback;
221
260
  };
@@ -224,9 +263,25 @@ const caseCreatePage = async (ctx, url, req, options) => {
224
263
  const askedStage = url.searchParams.get('stageId');
225
264
  const askedKind = url.searchParams.get('kind');
226
265
  const submitted = options.form ?? {};
266
+ const partnerIntent = url.searchParams.has('partnerId') || submitted.partnerIntent === '1';
267
+ const askedPartnerId = url.searchParams.get('partnerId') || (partnerIntent ? submitted.partnerId : null);
268
+ const [selectedPartner, data] = await Promise.all([
269
+ askedPartnerId
270
+ ? ctx.call('partner.getPartner', { id: askedPartnerId }, url, req)
271
+ : Promise.resolve(null),
272
+ references(ctx, url, req),
273
+ ]);
227
274
  const preset = {
228
275
  ...(askedStage ? { stageId: askedStage } : {}),
229
276
  ...(askedKind === 'lead' || askedKind === 'opportunity' ? { kind: askedKind } : {}),
277
+ ...(selectedPartner
278
+ ? {
279
+ partnerId: selectedPartner.id,
280
+ contactName: selectedPartner.kind === 'person' ? selectedPartner.name : '',
281
+ email: selectedPartner.email ?? '',
282
+ phone: selectedPartner.phone ?? '',
283
+ }
284
+ : {}),
230
285
  ...submitted,
231
286
  ...(options.form
232
287
  ? {
@@ -238,18 +293,31 @@ const caseCreatePage = async (ctx, url, req, options) => {
238
293
  }
239
294
  : {}),
240
295
  };
241
- const data = await references(ctx, url, req);
296
+ if (selectedPartner && !data.partners.some((partner) => partner.id === selectedPartner.id))
297
+ data.partners.unshift(selectedPartner);
242
298
  const controls = await caseControls(ctx, url, req, _, data, 'crm-create', preset);
243
299
  const returnTo = caseReturnTo(url, submitted.returnTo ?? url.searchParams.get('returnTo'));
244
300
  return adminPage(ctx, url, req, {
245
301
  title: 'crm_backend.cases.title',
246
302
  active: '/admin/crm/cases',
247
303
  body: (_, frame) => caseCreateScreen(_, frame, {
248
- fields: caseFields(_, preset, controls),
304
+ title: partnerIntent ? _('crm_backend.action.createLead') : _('crm_backend.action.create'),
305
+ fields: caseFields(_, preset, controls, {
306
+ partner: Boolean(selectedPartner),
307
+ need: Boolean(selectedPartner),
308
+ error: options.refusal?.error,
309
+ }),
249
310
  action: inLocale(url, options.actionPath),
250
311
  cancelHref: returnTo,
251
312
  returnTo,
252
313
  errors: options.errors,
314
+ guidance: partnerIntent && selectedPartner
315
+ ? {
316
+ title: _('crm_backend.case.create.partnerHintTitle'),
317
+ description: _('crm_backend.case.create.partnerHint'),
318
+ }
319
+ : undefined,
320
+ partnerIntent: partnerIntent && Boolean(selectedPartner),
253
321
  }),
254
322
  });
255
323
  };
@@ -375,6 +443,82 @@ const configurationTabOf = (url) => {
375
443
  const asked = url.searchParams.get('tab') ?? '';
376
444
  return CONFIGURATION_TABS.includes(asked) ? asked : 'teams';
377
445
  };
446
+ const configurationStatusOf = (url) => {
447
+ const asked = url.searchParams.get('status') ?? 'active';
448
+ return ['active', 'archived', 'all'].includes(asked) ? asked : 'active';
449
+ };
450
+ const teamConfigurationRoute = (ctx, creating) => async (url, req, params) => {
451
+ const refused = refusePost(req);
452
+ if (refused)
453
+ return refused;
454
+ const _ = ctx.translate(ctx.localeOf(url, req));
455
+ const requestedId = creating ? '' : params.id;
456
+ const memberAsked = url.searchParams.get('member');
457
+ let errors = [];
458
+ let teamId = requestedId;
459
+ if (req.method === 'POST') {
460
+ const form = await readForm(req);
461
+ const call = (name, input) => ctx.call(name, input, url, req);
462
+ if (memberAsked && !creating) {
463
+ const result = await teamMemberWrite(call, form, requestedId, form.id || randomUUID());
464
+ if (result.ok)
465
+ return seeOther(inLocale(url, `/admin/crm/configuration/teams/${encodeURIComponent(requestedId)}`));
466
+ errors = errorsOf(result, _);
467
+ }
468
+ else {
469
+ teamId = form.id || requestedId || randomUUID();
470
+ const result = await configurationWrite(call, 'teams', form, teamId);
471
+ if (result.ok)
472
+ return seeOther(inLocale(url, `/admin/crm/configuration/teams/${encodeURIComponent(teamId)}`));
473
+ errors = errorsOf(result, _);
474
+ }
475
+ }
476
+ else if (req.method !== 'GET')
477
+ return text('GET or POST', { status: 405 });
478
+ const [config, users, listedMembers] = await Promise.all([
479
+ configuration(ctx, url, req),
480
+ ctx.call('user.listUsers', { includeArchived: false, limit: 200 }, url, req),
481
+ creating
482
+ ? Promise.resolve([])
483
+ : ctx.call('crm.team.member.list', { teamId: requestedId, limit: 200 }, url, req),
484
+ ]);
485
+ const team = creating
486
+ ? {}
487
+ : ((config.teams ?? []).find((row) => String(row.id) === requestedId) ?? null);
488
+ if (!team)
489
+ return text('not found', { status: 404 });
490
+ const memberEditing = memberAsked && memberAsked !== 'new'
491
+ ? (listedMembers.find((row) => String(row.id) === memberAsked) ?? null)
492
+ : null;
493
+ const path = creating
494
+ ? '/admin/crm/configuration/teams/new'
495
+ : `/admin/crm/configuration/teams/${encodeURIComponent(requestedId)}`;
496
+ const action = inLocale(url, path);
497
+ const memberAction = memberAsked
498
+ ? inLocale(url, `${path}?member=${encodeURIComponent(memberAsked)}`)
499
+ : undefined;
500
+ return adminPage(ctx, url, req, {
501
+ title: 'crm_backend.configuration.title',
502
+ active: '/admin/crm/configuration',
503
+ body: (_, frame) => teamConfigurationScreen(_, frame, {
504
+ team,
505
+ fields: configurationFields(_, 'teams', team, config.teams ?? [], users),
506
+ members: listedMembers,
507
+ action,
508
+ cancelHref: inLocale(url, '/admin/crm/configuration?tab=teams'),
509
+ errors,
510
+ creating,
511
+ memberCreateHref: creating ? undefined : inLocale(url, `${path}?member=new`),
512
+ memberEditHref: creating
513
+ ? undefined
514
+ : (row) => inLocale(url, `${path}?member=${encodeURIComponent(String(row.id))}`),
515
+ memberEditing,
516
+ memberCreating: memberAsked === 'new',
517
+ memberFields: memberAsked ? teamMemberFields(_, memberEditing, users) : undefined,
518
+ memberAction,
519
+ }),
520
+ });
521
+ };
378
522
  export const routes = {
379
523
  '/admin/crm': () => async (url, req) => req.method === 'GET' ? seeOther(inLocale(url, '/admin/crm/pipeline')) : text('GET', { status: 405 }),
380
524
  '/admin/crm/pipeline': (ctx) => async (url, req) => {
@@ -595,14 +739,24 @@ export const routes = {
595
739
  return refused;
596
740
  if (req.method === 'POST') {
597
741
  const form = await readForm(req);
742
+ const refusal = formRefusal(ctx.translate(ctx.localeOf(url, req)));
743
+ const fromPartner = form.partnerIntent === '1';
744
+ if (!refusal.check(caseFormSchema({ partner: fromPartner, need: fromPartner }), form))
745
+ return caseCreatePage(ctx, url, req, {
746
+ actionPath: '/admin/crm/cases',
747
+ form,
748
+ refusal,
749
+ });
598
750
  const id = randomUUID();
599
751
  const result = (await ctx.call('crm.case.save', saveInput(id, form), url, req));
600
752
  if (result.ok)
601
753
  return seeOther(inLocale(url, `/admin/crm/cases/${id}`));
754
+ refusal.add(errorsOf(result, ctx.translate(ctx.localeOf(url, req))));
602
755
  return caseCreatePage(ctx, url, req, {
603
756
  actionPath: '/admin/crm/cases',
604
- errors: errorsOf(result, ctx.translate(ctx.localeOf(url, req))),
757
+ errors: refusal.sentences(),
605
758
  form,
759
+ refusal,
606
760
  });
607
761
  }
608
762
  if (req.method !== 'GET')
@@ -658,14 +812,24 @@ export const routes = {
658
812
  return refused;
659
813
  if (req.method === 'POST') {
660
814
  const form = await readForm(req);
815
+ const refusal = formRefusal(ctx.translate(ctx.localeOf(url, req)));
816
+ const fromPartner = form.partnerIntent === '1';
817
+ if (!refusal.check(caseFormSchema({ partner: fromPartner, need: fromPartner }), form))
818
+ return caseCreatePage(ctx, url, req, {
819
+ actionPath: '/admin/crm/cases/new',
820
+ form,
821
+ refusal,
822
+ });
661
823
  const id = randomUUID();
662
824
  const result = (await ctx.call('crm.case.save', saveInput(id, form), url, req));
663
825
  if (result.ok)
664
826
  return seeOther(inLocale(url, `/admin/crm/cases/${id}`));
827
+ refusal.add(errorsOf(result, ctx.translate(ctx.localeOf(url, req))));
665
828
  return caseCreatePage(ctx, url, req, {
666
829
  actionPath: '/admin/crm/cases/new',
667
- errors: errorsOf(result, ctx.translate(ctx.localeOf(url, req))),
830
+ errors: refusal.sentences(),
668
831
  form,
832
+ refusal,
669
833
  });
670
834
  }
671
835
  if (req.method !== 'GET')
@@ -689,7 +853,7 @@ export const routes = {
689
853
  // behind these actions match by construction: a stale tab would win
690
854
  // silently. A request that does not say which version it saw is refused
691
855
  // instead of being handed the current one.
692
- const VERSIONED = ['move', 'convert', 'won', 'lost', 'assign', 'merge'];
856
+ const VERSIONED = ['move', 'convert', 'close', 'won', 'lost', 'assign', 'merge'];
693
857
  if (VERSIONED.includes(String(form.action ?? '')) && !form.expectedVersion)
694
858
  return text(_('crm_backend.convert.versionRequired'), { status: 422 });
695
859
  const base = {
@@ -711,8 +875,49 @@ export const routes = {
711
875
  ok: false,
712
876
  errors: [{ field: 'confirm', code: 'crm_backend.convert.confirmRequired' }],
713
877
  };
878
+ else if (!String(form.expectedRevenue ?? '').trim())
879
+ result = {
880
+ ok: false,
881
+ errors: [{ field: 'expectedRevenue', code: 'crm_backend.convert.revenueRequired' }],
882
+ };
883
+ else if (!String(form.expectedClosing ?? '').trim())
884
+ result = {
885
+ ok: false,
886
+ errors: [{ field: 'expectedClosing', code: 'crm_backend.convert.closingRequired' }],
887
+ };
888
+ else
889
+ result = await call('crm.case.convertLead', {
890
+ ...base,
891
+ ...optional(form, 'stageId'),
892
+ ...optional(form, 'expectedRevenue'),
893
+ ...optional(form, 'expectedClosing'),
894
+ });
895
+ }
896
+ else if (form.action === 'close') {
897
+ const reason = String(form.closeReason ?? '').trim();
898
+ if (!form.confirm)
899
+ result = {
900
+ ok: false,
901
+ errors: [{ field: 'confirm', code: 'crm_backend.close.confirmRequired' }],
902
+ };
903
+ else if (!reason)
904
+ result = {
905
+ ok: false,
906
+ errors: [{ field: 'closeReason', code: 'crm_backend.close.reasonRequired' }],
907
+ };
908
+ else if (form.terminal === 'won')
909
+ result = await call('crm.case.markWon', { ...base, closeReason: reason });
910
+ else if (form.terminal === 'lost')
911
+ result = await call('crm.case.markLost', {
912
+ ...base,
913
+ lostReason: reason,
914
+ closeReason: reason,
915
+ });
714
916
  else
715
- result = await call('crm.case.convertLead', { ...base, ...optional(form, 'stageId') });
917
+ result = {
918
+ ok: false,
919
+ errors: [{ field: 'terminal', code: 'crm_backend.close.invalidResult' }],
920
+ };
716
921
  }
717
922
  else if (form.action === 'won')
718
923
  result = await call('crm.case.markWon', base);
@@ -832,6 +1037,9 @@ export const routes = {
832
1037
  // Converting is confirmed in a step the URL owns, so the opportunity
833
1038
  // stages are only read when that step is open — and only for a lead.
834
1039
  const converting = row.kind === 'lead' && url.searchParams.get('modal') === 'convert';
1040
+ const closing = row.kind === 'opportunity' &&
1041
+ row.terminalState === 'open' &&
1042
+ url.searchParams.get('modal') === 'close';
835
1043
  const conversionStages = converting ? stagesFor('opportunity') : [];
836
1044
  const [warehouses, plans, activityTypes, duplicateResult, quotations, products] = await Promise.all([
837
1045
  ctx.call('stock.listWarehouses', {}, url, req),
@@ -897,7 +1105,7 @@ export const routes = {
897
1105
  }
898
1106
  : {}),
899
1107
  };
900
- const closeConvert = () => {
1108
+ const closeOverlay = () => {
901
1109
  const back = new URLSearchParams(url.searchParams);
902
1110
  back.delete('modal');
903
1111
  const query = back.toString();
@@ -919,7 +1127,7 @@ export const routes = {
919
1127
  controls,
920
1128
  // A conversion that was refused belongs to the step that asked for
921
1129
  // it, not to the save form behind it.
922
- errors: converting ? [] : errors,
1130
+ errors: converting || closing ? [] : errors,
923
1131
  locale: localeQuery(url),
924
1132
  tab: ['overview', 'sales', 'activities', 'timeline'].includes(url.searchParams.get('tab') ?? '')
925
1133
  ? String(url.searchParams.get('tab'))
@@ -930,11 +1138,17 @@ export const routes = {
930
1138
  // Posting back to the step keeps it open when the answer is
931
1139
  // no, with the reason inside it rather than on the page behind.
932
1140
  action: inLocale(url, `${url.pathname}?${url.searchParams.toString()}`),
933
- cancelHref: inLocale(url, closeConvert()),
1141
+ cancelHref: inLocale(url, closeOverlay()),
934
1142
  control: controls.convertStage,
935
1143
  errors,
936
1144
  }))
937
- : detail;
1145
+ : closing
1146
+ ? modalWorkspace(detail, caseCloseModal(_, row, {
1147
+ action: inLocale(url, `${url.pathname}?${url.searchParams.toString()}`),
1148
+ cancelHref: inLocale(url, closeOverlay()),
1149
+ errors,
1150
+ }))
1151
+ : detail;
938
1152
  },
939
1153
  });
940
1154
  },
@@ -1078,7 +1292,13 @@ export const routes = {
1078
1292
  return refused;
1079
1293
  const _ = ctx.translate(ctx.localeOf(url, req));
1080
1294
  const tab = configurationTabOf(url);
1081
- const back = inLocale(url, `/admin/crm/configuration?tab=${tab}`);
1295
+ const status = configurationStatusOf(url);
1296
+ const statusQuery = status === 'active' ? '' : `&status=${status}`;
1297
+ const back = inLocale(url, `/admin/crm/configuration?tab=${tab}${statusQuery}`);
1298
+ if (tab === 'teams' && url.searchParams.has('edit'))
1299
+ return seeOther(inLocale(url, `/admin/crm/configuration/teams/${encodeURIComponent(String(url.searchParams.get('edit')))}`));
1300
+ if (tab === 'teams' && url.searchParams.get('create') === '1')
1301
+ return seeOther(inLocale(url, '/admin/crm/configuration/teams/new'));
1082
1302
  let errors = [];
1083
1303
  if (req.method === 'POST') {
1084
1304
  if (crossSite(req))
@@ -1093,19 +1313,15 @@ export const routes = {
1093
1313
  }
1094
1314
  else if (req.method !== 'GET')
1095
1315
  return text('GET or POST', { status: 405 });
1096
- const [config, tags, members, users] = await Promise.all([
1316
+ const [config, tags, users] = await Promise.all([
1097
1317
  configuration(ctx, url, req),
1098
1318
  ctx.call('crm.tag.list', { includeArchived: true, limit: 200 }, url, req),
1099
- tab === 'members'
1100
- ? ctx.call('crm.team.member.list', { limit: 200 }, url, req)
1101
- : Promise.resolve([]),
1102
- tab === 'members'
1103
- ? ctx.call('user.listUsers', { includeArchived: false, limit: 200 }, url, req)
1104
- : Promise.resolve([]),
1319
+ ctx.call('user.listUsers', { includeArchived: false, limit: 200 }, url, req),
1105
1320
  ]);
1106
- const rows = tab === 'tags' ? tags : tab === 'members' ? members : (config[tab] ?? []);
1321
+ const allRows = tab === 'tags' ? tags : (config[tab] ?? []);
1322
+ const rows = allRows.filter((row) => status === 'all' ? true : status === 'active' ? row.active !== false : row.active === false);
1107
1323
  const asked = url.searchParams.get('edit');
1108
- const editing = asked ? (rows.find((row) => String(row.id) === asked) ?? null) : null;
1324
+ const editing = asked ? (allRows.find((row) => String(row.id) === asked) ?? null) : null;
1109
1325
  const teams = config.teams ?? [];
1110
1326
  return adminPage(ctx, url, req, {
1111
1327
  title: 'crm_backend.configuration.title',
@@ -1116,50 +1332,27 @@ export const routes = {
1116
1332
  creating: url.searchParams.get('create') === '1' || (req.method === 'POST' && !editing),
1117
1333
  errors,
1118
1334
  locale: localeQuery(url),
1335
+ status,
1336
+ teams,
1337
+ users,
1119
1338
  fields: configurationFields(_, tab, editing, teams, users),
1120
- ...(tab === 'members'
1121
- ? {
1122
- label: (row) => String(row.userName ?? row.userId),
1123
- detail: (row) => _('crm_backend.configuration.member.detail', {
1124
- capacity: String(row.capacity ?? 1),
1125
- assigned: String(row.assignedCount ?? 0),
1126
- }),
1127
- }
1128
- : {}),
1129
- ...(tab === 'stages' ? { detail: (row) => String(row.terminalState ?? 'open') } : {}),
1130
1339
  }),
1131
1340
  });
1132
1341
  },
1342
+ '/admin/crm/configuration/teams/new': (ctx) => teamConfigurationRoute(ctx, true),
1343
+ '/admin/crm/configuration/teams/{id}': (ctx) => teamConfigurationRoute(ctx, false),
1133
1344
  };
1134
1345
  /**
1135
1346
  * One write per configuration tab.
1136
1347
  *
1137
- * The tabs do not share a function signature — a tag saves by name, a team saves
1138
- * a values bag with an idempotency key, a member is a join row — so the mapping
1139
- * is spelled out rather than guessed from the tab name.
1348
+ * The tabs do not share a function signature — a tag saves by name while the
1349
+ * other configuration records save a versioned values bag.
1140
1350
  */
1141
1351
  async function configurationWrite(call, tab, form, id) {
1142
- const archiving = form.action === 'archive';
1143
- const restoring = form.action === 'restore';
1144
- const active = archiving ? false : restoring ? true : bool(form.active);
1352
+ const active = bool(form.active);
1145
1353
  if (tab === 'tags') {
1146
- if (archiving)
1147
- return call('crm.tag.archive', { id });
1148
1354
  return call('crm.tag.save', { id, name: form.name ?? '', active });
1149
1355
  }
1150
- if (tab === 'members') {
1151
- if (archiving)
1152
- return call('crm.team.member.remove', { id });
1153
- return call('crm.team.member.save', {
1154
- id,
1155
- teamId: form.teamId ?? '',
1156
- userId: form.userId ?? '',
1157
- capacity: Number(form.capacity ?? 1),
1158
- sequence: Number(form.sequence ?? 10),
1159
- active,
1160
- idempotencyKey: randomUUID(),
1161
- });
1162
- }
1163
1356
  const fn = {
1164
1357
  teams: 'crm.team.save',
1165
1358
  stages: 'crm.stage.save',
@@ -1171,20 +1364,67 @@ async function configurationWrite(call, tab, form, id) {
1171
1364
  id,
1172
1365
  active,
1173
1366
  ...(form.expectedVersion ? { expectedVersion: Number(form.expectedVersion) } : {}),
1174
- ...(form.allowedKinds === undefined
1175
- ? {}
1176
- : {
1177
- allowedKinds: form.allowedKinds
1178
- .split(',')
1179
- .map((item) => item.trim())
1180
- .filter(Boolean),
1181
- }),
1367
+ ...(['stages', 'assignmentRules'].includes(tab)
1368
+ ? { allowedKinds: ['lead', 'opportunity'].filter((kind) => bool(form[`kind_${kind}`])) }
1369
+ : {}),
1182
1370
  ...(form.sequence === undefined ? {} : { sequence: Number(form.sequence) }),
1183
1371
  ...(form.priority === undefined ? {} : { priority: Number(form.priority) }),
1372
+ ...(form.minimumScore === undefined || form.minimumScore === ''
1373
+ ? { minimumScore: null }
1374
+ : { minimumScore: form.minimumScore }),
1375
+ ...(form.teamId === '' ? { teamId: null } : {}),
1376
+ ...(form.assigneeUserId === '' ? { assigneeUserId: null } : {}),
1377
+ ...(form.leaderUserId === '' ? { leaderUserId: null } : {}),
1378
+ ...(form.fold === undefined ? {} : { fold: bool(form.fold) }),
1184
1379
  };
1185
1380
  delete values.action;
1381
+ delete values.kind_lead;
1382
+ delete values.kind_opportunity;
1186
1383
  return call(fn, { values, idempotencyKey: randomUUID() });
1187
1384
  }
1385
+ async function teamMemberWrite(call, form, teamId, id) {
1386
+ return call('crm.team.member.save', {
1387
+ id,
1388
+ teamId,
1389
+ userId: form.userId ?? '',
1390
+ capacity: Number(form.capacity ?? 1),
1391
+ sequence: Number(form.sequence ?? 10),
1392
+ active: bool(form.active),
1393
+ idempotencyKey: randomUUID(),
1394
+ });
1395
+ }
1396
+ function teamMemberFields(_, editing, users) {
1397
+ const value = (name, fallback = '') => String(editing?.[name] ?? fallback);
1398
+ return [
1399
+ {
1400
+ name: 'userId',
1401
+ label: _('crm_backend.field.assignee'),
1402
+ type: 'select',
1403
+ required: true,
1404
+ value: value('userId'),
1405
+ options: choices(users),
1406
+ },
1407
+ {
1408
+ name: 'capacity',
1409
+ label: _('crm_backend.field.capacity'),
1410
+ type: 'number',
1411
+ value: value('capacity', '1'),
1412
+ help: _('crm_backend.field.capacityHint'),
1413
+ },
1414
+ {
1415
+ name: 'sequence',
1416
+ label: _('crm_backend.field.sequence'),
1417
+ type: 'number',
1418
+ value: value('sequence', '10'),
1419
+ },
1420
+ {
1421
+ name: 'active',
1422
+ label: _('crm_backend.field.active'),
1423
+ type: 'checkbox',
1424
+ value: editing ? editing.active !== false : true,
1425
+ },
1426
+ ];
1427
+ }
1188
1428
  /** The form for one configuration tab, pre-filled when a row is being edited. */
1189
1429
  function configurationFields(_, tab, editing, teams, users) {
1190
1430
  const value = (name, fallback = '') => String(editing?.[name] ?? fallback);
@@ -1197,49 +1437,19 @@ function configurationFields(_, tab, editing, teams, users) {
1197
1437
  value: editing ? editing.active !== false : true,
1198
1438
  },
1199
1439
  ];
1200
- if (tab === 'members')
1201
- return [
1202
- {
1203
- name: 'teamId',
1204
- label: _('crm_backend.field.team'),
1205
- type: 'select',
1206
- required: true,
1207
- value: value('teamId'),
1208
- options: choices(teams),
1209
- },
1210
- {
1211
- name: 'userId',
1212
- label: _('crm_backend.field.assignee'),
1213
- type: 'select',
1214
- required: true,
1215
- value: value('userId'),
1216
- options: choices(users),
1217
- },
1218
- {
1219
- name: 'capacity',
1220
- label: _('crm_backend.field.capacity'),
1221
- type: 'number',
1222
- value: value('capacity', '1'),
1223
- },
1224
- {
1225
- name: 'sequence',
1226
- label: _('crm_backend.field.sequence'),
1227
- type: 'number',
1228
- value: value('sequence', '10'),
1229
- },
1230
- {
1231
- name: 'active',
1232
- label: _('crm_backend.field.active'),
1233
- type: 'checkbox',
1234
- value: editing ? editing.active !== false : true,
1235
- },
1236
- ];
1237
1440
  if (tab === 'tags')
1238
1441
  return common;
1239
1442
  if (tab === 'teams')
1240
1443
  return [
1241
1444
  ...common,
1242
1445
  { name: 'code', label: _('crm_backend.field.code'), value: value('code'), required: true },
1446
+ {
1447
+ name: 'leaderUserId',
1448
+ label: _('crm_backend.field.teamLeader'),
1449
+ type: 'select',
1450
+ value: value('leaderUserId'),
1451
+ options: [{ value: '', label: _('crm_backend.value.unset') }, ...choices(users)],
1452
+ },
1243
1453
  {
1244
1454
  name: 'assignmentMode',
1245
1455
  label: _('crm_backend.field.assignmentMode'),
@@ -1264,9 +1474,23 @@ function configurationFields(_, tab, editing, teams, users) {
1264
1474
  {
1265
1475
  name: 'allowedKinds',
1266
1476
  label: _('crm_backend.field.allowedKinds'),
1267
- value: Array.isArray(editing?.allowedKinds)
1268
- ? editing.allowedKinds.map(String).join(',')
1269
- : 'lead,opportunity',
1477
+ type: 'checkbox-group',
1478
+ required: true,
1479
+ options: ['lead', 'opportunity'].map((kind) => ({
1480
+ name: `kind_${kind}`,
1481
+ value: '1',
1482
+ label: _(`crm.kind.${kind}`),
1483
+ checked: editing
1484
+ ? Array.isArray(editing.allowedKinds) && editing.allowedKinds.includes(kind)
1485
+ : true,
1486
+ })),
1487
+ },
1488
+ {
1489
+ name: 'teamId',
1490
+ label: _('crm_backend.field.team'),
1491
+ type: 'select',
1492
+ value: value('teamId'),
1493
+ options: [{ value: '', label: _('crm_backend.value.allTeams') }, ...choices(teams)],
1270
1494
  },
1271
1495
  {
1272
1496
  name: 'terminalState',
@@ -1275,6 +1499,12 @@ function configurationFields(_, tab, editing, teams, users) {
1275
1499
  value: value('terminalState', 'open'),
1276
1500
  options: ['open', 'won', 'lost'].map((item) => ({ value: item, label: _(`crm.terminal.${item}`) })),
1277
1501
  },
1502
+ {
1503
+ name: 'fold',
1504
+ label: _('crm_backend.field.fold'),
1505
+ type: 'checkbox',
1506
+ value: editing?.fold === true,
1507
+ },
1278
1508
  ];
1279
1509
  if (tab === 'assignmentRules')
1280
1510
  return [
@@ -1288,9 +1518,16 @@ function configurationFields(_, tab, editing, teams, users) {
1288
1518
  {
1289
1519
  name: 'allowedKinds',
1290
1520
  label: _('crm_backend.field.allowedKinds'),
1291
- value: Array.isArray(editing?.allowedKinds)
1292
- ? editing.allowedKinds.map(String).join(',')
1293
- : 'lead,opportunity',
1521
+ type: 'checkbox-group',
1522
+ required: true,
1523
+ options: ['lead', 'opportunity'].map((kind) => ({
1524
+ name: `kind_${kind}`,
1525
+ value: '1',
1526
+ label: _(`crm.kind.${kind}`),
1527
+ checked: editing
1528
+ ? Array.isArray(editing.allowedKinds) && editing.allowedKinds.includes(kind)
1529
+ : true,
1530
+ })),
1294
1531
  },
1295
1532
  {
1296
1533
  name: 'teamId',
@@ -1300,11 +1537,34 @@ function configurationFields(_, tab, editing, teams, users) {
1300
1537
  value: value('teamId'),
1301
1538
  options: choices(teams),
1302
1539
  },
1540
+ {
1541
+ name: 'assigneeUserId',
1542
+ label: _('crm_backend.field.assignee'),
1543
+ type: 'select',
1544
+ value: value('assigneeUserId'),
1545
+ options: [{ value: '', label: _('crm_backend.value.teamMode') }, ...choices(users)],
1546
+ },
1303
1547
  { name: 'utmSource', label: _('crm_backend.field.utmSource'), value: value('utmSource') },
1548
+ {
1549
+ name: 'minimumScore',
1550
+ label: _('crm_backend.field.minimumScore'),
1551
+ type: 'number',
1552
+ value: value('minimumScore'),
1553
+ },
1304
1554
  ];
1305
1555
  return [
1306
1556
  ...common,
1307
- { name: 'field', label: _('crm_backend.field.ruleField'), value: value('field'), required: true },
1557
+ {
1558
+ name: 'field',
1559
+ label: _('crm_backend.field.ruleField'),
1560
+ type: 'select',
1561
+ value: value('field', 'email'),
1562
+ required: true,
1563
+ options: ['email', 'utmSource', 'expectedRevenue'].map((field) => ({
1564
+ value: field,
1565
+ label: _(`crm_backend.scoreField.${field}`),
1566
+ })),
1567
+ },
1308
1568
  {
1309
1569
  name: 'operator',
1310
1570
  label: _('crm_backend.field.operator'),