@ketvietlab/ketsuite 0.1.20 → 0.1.24

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 (245) hide show
  1. package/dist/cli.js +9 -3
  2. package/dist/cli.js.map +1 -1
  3. package/dist/demo/crm.d.ts +3 -0
  4. package/dist/demo/crm.d.ts.map +1 -0
  5. package/dist/demo/crm.js +418 -0
  6. package/dist/demo/crm.js.map +1 -0
  7. package/dist/demo/partner.d.ts +8 -0
  8. package/dist/demo/partner.d.ts.map +1 -0
  9. package/dist/demo/partner.js +179 -0
  10. package/dist/demo/partner.js.map +1 -0
  11. package/dist/demo/product.d.ts +25 -0
  12. package/dist/demo/product.d.ts.map +1 -0
  13. package/dist/demo/product.js +579 -0
  14. package/dist/demo/product.js.map +1 -0
  15. package/dist/demo/purchase.d.ts +4 -0
  16. package/dist/demo/purchase.d.ts.map +1 -0
  17. package/dist/demo/purchase.js +58 -0
  18. package/dist/demo/purchase.js.map +1 -0
  19. package/dist/demo/sale.d.ts +4 -0
  20. package/dist/demo/sale.d.ts.map +1 -0
  21. package/dist/demo/sale.js +52 -0
  22. package/dist/demo/sale.js.map +1 -0
  23. package/dist/demo/seed.d.ts +14 -0
  24. package/dist/demo/seed.d.ts.map +1 -0
  25. package/dist/demo/seed.js +88 -0
  26. package/dist/demo/seed.js.map +1 -0
  27. package/dist/demo/stock.d.ts +7 -0
  28. package/dist/demo/stock.d.ts.map +1 -0
  29. package/dist/demo/stock.js +26 -0
  30. package/dist/demo/stock.js.map +1 -0
  31. package/dist/demo/user.d.ts +4 -0
  32. package/dist/demo/user.d.ts.map +1 -0
  33. package/dist/demo/user.js +41 -0
  34. package/dist/demo/user.js.map +1 -0
  35. package/dist/demo/util.d.ts +7 -0
  36. package/dist/demo/util.d.ts.map +1 -0
  37. package/dist/demo/util.js +19 -0
  38. package/dist/demo/util.js.map +1 -0
  39. package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
  40. package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
  41. package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
  42. package/dist/modules/backend/design/client/backend-shell.mjs.map +3 -3
  43. package/dist/modules/backend/design/content.css +9 -9
  44. package/dist/modules/backend/design/controls.css +5 -5
  45. package/dist/modules/backend/design/forms.css +12 -5
  46. package/dist/modules/backend/design/foundation.css +3 -3
  47. package/dist/modules/backend/design/lists.css +23 -24
  48. package/dist/modules/backend/design/user-workflow.css +2 -2
  49. package/dist/modules/backend/screen.d.ts.map +1 -1
  50. package/dist/modules/backend/screen.js +11 -2
  51. package/dist/modules/backend/screen.js.map +1 -1
  52. package/dist/modules/crm/functions/access.d.ts +3 -0
  53. package/dist/modules/crm/functions/access.d.ts.map +1 -0
  54. package/dist/modules/crm/functions/access.js +89 -0
  55. package/dist/modules/crm/functions/access.js.map +1 -0
  56. package/dist/modules/crm/functions/activities.d.ts +3 -0
  57. package/dist/modules/crm/functions/activities.d.ts.map +1 -0
  58. package/dist/modules/crm/functions/activities.js +174 -0
  59. package/dist/modules/crm/functions/activities.js.map +1 -0
  60. package/dist/modules/crm/functions/cases.d.ts +3 -0
  61. package/dist/modules/crm/functions/cases.d.ts.map +1 -0
  62. package/dist/modules/crm/functions/cases.js +585 -0
  63. package/dist/modules/crm/functions/cases.js.map +1 -0
  64. package/dist/modules/crm/functions/configuration.d.ts +3 -0
  65. package/dist/modules/crm/functions/configuration.d.ts.map +1 -0
  66. package/dist/modules/crm/functions/configuration.js +331 -0
  67. package/dist/modules/crm/functions/configuration.js.map +1 -0
  68. package/dist/modules/crm/functions/gamification.d.ts +3 -0
  69. package/dist/modules/crm/functions/gamification.d.ts.map +1 -0
  70. package/dist/modules/crm/functions/gamification.js +58 -0
  71. package/dist/modules/crm/functions/gamification.js.map +1 -0
  72. package/dist/modules/crm/functions/index.d.ts +4 -0
  73. package/dist/modules/crm/functions/index.d.ts.map +1 -0
  74. package/dist/modules/crm/functions/index.js +33 -0
  75. package/dist/modules/crm/functions/index.js.map +1 -0
  76. package/dist/modules/crm/functions/modal-context.d.ts +3 -0
  77. package/dist/modules/crm/functions/modal-context.d.ts.map +1 -0
  78. package/dist/modules/crm/functions/modal-context.js +248 -0
  79. package/dist/modules/crm/functions/modal-context.js.map +1 -0
  80. package/dist/modules/crm/functions/previews.d.ts +3 -0
  81. package/dist/modules/crm/functions/previews.d.ts.map +1 -0
  82. package/dist/modules/crm/functions/previews.js +20 -0
  83. package/dist/modules/crm/functions/previews.js.map +1 -0
  84. package/dist/modules/crm/functions/shared.d.ts +31 -0
  85. package/dist/modules/crm/functions/shared.d.ts.map +1 -0
  86. package/dist/modules/crm/functions/shared.js +194 -0
  87. package/dist/modules/crm/functions/shared.js.map +1 -0
  88. package/dist/modules/crm/index.d.ts +1 -1
  89. package/dist/modules/crm/index.d.ts.map +1 -1
  90. package/dist/modules/crm/index.js +2 -2
  91. package/dist/modules/crm/index.js.map +1 -1
  92. package/dist/modules/crm/messages.d.ts +4 -1
  93. package/dist/modules/crm/messages.d.ts.map +1 -1
  94. package/dist/modules/crm/messages.js +6 -0
  95. package/dist/modules/crm/messages.js.map +1 -1
  96. package/dist/modules/crm/record-modal-labels.d.ts +9 -0
  97. package/dist/modules/crm/record-modal-labels.d.ts.map +1 -0
  98. package/dist/modules/crm/record-modal-labels.js +36 -0
  99. package/dist/modules/crm/record-modal-labels.js.map +1 -0
  100. package/dist/modules/crm/types.d.ts +5 -0
  101. package/dist/modules/crm/types.d.ts.map +1 -1
  102. package/dist/modules/crm/types.js +9 -0
  103. package/dist/modules/crm/types.js.map +1 -1
  104. package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +352 -0
  105. package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -0
  106. package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +2 -0
  107. package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +7 -0
  108. package/dist/modules/crm_backend/index.d.ts.map +1 -1
  109. package/dist/modules/crm_backend/index.js +5 -1
  110. package/dist/modules/crm_backend/index.js.map +1 -1
  111. package/dist/modules/crm_backend/islands.d.ts +27 -0
  112. package/dist/modules/crm_backend/islands.d.ts.map +1 -1
  113. package/dist/modules/crm_backend/islands.js +22 -0
  114. package/dist/modules/crm_backend/islands.js.map +1 -1
  115. package/dist/modules/crm_backend/messages.d.ts +30 -0
  116. package/dist/modules/crm_backend/messages.d.ts.map +1 -1
  117. package/dist/modules/crm_backend/messages.js +30 -0
  118. package/dist/modules/crm_backend/messages.js.map +1 -1
  119. package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts +55 -0
  120. package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts.map +1 -0
  121. package/dist/modules/crm_backend/modal/configuration-modal-view.js +601 -0
  122. package/dist/modules/crm_backend/modal/configuration-modal-view.js.map +1 -0
  123. package/dist/modules/crm_backend/routes.d.ts.map +1 -1
  124. package/dist/modules/crm_backend/routes.js +107 -368
  125. package/dist/modules/crm_backend/routes.js.map +1 -1
  126. package/dist/modules/crm_backend/screens/configuration.d.ts +24 -32
  127. package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
  128. package/dist/modules/crm_backend/screens/configuration.js +183 -274
  129. package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
  130. package/dist/modules/crm_backend/screens/index.d.ts +2 -2
  131. package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
  132. package/dist/modules/crm_backend/screens/index.js +1 -1
  133. package/dist/modules/crm_backend/screens/index.js.map +1 -1
  134. package/dist/modules/product/functions.d.ts +14 -1
  135. package/dist/modules/product/functions.d.ts.map +1 -1
  136. package/dist/modules/product/functions.js +36 -31
  137. package/dist/modules/product/functions.js.map +1 -1
  138. package/dist/modules/product/product-modal-context.d.ts +3 -0
  139. package/dist/modules/product/product-modal-context.d.ts.map +1 -0
  140. package/dist/modules/product/product-modal-context.js +171 -0
  141. package/dist/modules/product/product-modal-context.js.map +1 -0
  142. package/dist/modules/product_backend/client/product-modal.d.mts +143 -0
  143. package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -0
  144. package/dist/modules/product_backend/client/product-modal.mjs +2 -0
  145. package/dist/modules/product_backend/client/product-modal.mjs.map +7 -0
  146. package/dist/modules/product_backend/index.d.ts.map +1 -1
  147. package/dist/modules/product_backend/index.js +16 -0
  148. package/dist/modules/product_backend/index.js.map +1 -1
  149. package/dist/modules/product_backend/islands.d.ts +1 -0
  150. package/dist/modules/product_backend/islands.d.ts.map +1 -1
  151. package/dist/modules/product_backend/islands.js +9 -0
  152. package/dist/modules/product_backend/islands.js.map +1 -1
  153. package/dist/modules/product_backend/modal/product-modal-view.d.ts +43 -0
  154. package/dist/modules/product_backend/modal/product-modal-view.d.ts.map +1 -0
  155. package/dist/modules/product_backend/modal/product-modal-view.js +602 -0
  156. package/dist/modules/product_backend/modal/product-modal-view.js.map +1 -0
  157. package/dist/modules/product_backend/routes.d.ts.map +1 -1
  158. package/dist/modules/product_backend/routes.js +44 -330
  159. package/dist/modules/product_backend/routes.js.map +1 -1
  160. package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
  161. package/dist/modules/product_backend/screens/detail.js +6 -108
  162. package/dist/modules/product_backend/screens/detail.js.map +1 -1
  163. package/dist/modules/product_backend/screens/index.d.ts +0 -1
  164. package/dist/modules/product_backend/screens/index.d.ts.map +1 -1
  165. package/dist/modules/product_backend/screens/index.js +0 -1
  166. package/dist/modules/product_backend/screens/index.js.map +1 -1
  167. package/dist/modules/product_backend/screens/list.d.ts +3 -1
  168. package/dist/modules/product_backend/screens/list.d.ts.map +1 -1
  169. package/dist/modules/product_backend/screens/list.js +6 -5
  170. package/dist/modules/product_backend/screens/list.js.map +1 -1
  171. package/dist/modules/user/authorization.d.ts.map +1 -1
  172. package/dist/modules/user/authorization.js +114 -0
  173. package/dist/modules/user/authorization.js.map +1 -1
  174. package/dist/modules/user/functions.d.ts.map +1 -1
  175. package/dist/modules/user/functions.js +2 -0
  176. package/dist/modules/user/functions.js.map +1 -1
  177. package/dist/modules/user/user-modal-context.d.ts +3 -0
  178. package/dist/modules/user/user-modal-context.d.ts.map +1 -0
  179. package/dist/modules/user/user-modal-context.js +151 -0
  180. package/dist/modules/user/user-modal-context.js.map +1 -0
  181. package/dist/modules/user_backend/client/user-modal.d.mts +72 -0
  182. package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -0
  183. package/dist/modules/user_backend/client/user-modal.mjs +2 -0
  184. package/dist/modules/user_backend/client/user-modal.mjs.map +7 -0
  185. package/dist/modules/user_backend/index.d.ts.map +1 -1
  186. package/dist/modules/user_backend/index.js +9 -0
  187. package/dist/modules/user_backend/index.js.map +1 -1
  188. package/dist/modules/user_backend/islands.d.ts +15 -0
  189. package/dist/modules/user_backend/islands.d.ts.map +1 -0
  190. package/dist/modules/user_backend/islands.js +14 -0
  191. package/dist/modules/user_backend/islands.js.map +1 -0
  192. package/dist/modules/user_backend/messages.d.ts +30 -0
  193. package/dist/modules/user_backend/messages.d.ts.map +1 -1
  194. package/dist/modules/user_backend/messages.js +30 -0
  195. package/dist/modules/user_backend/messages.js.map +1 -1
  196. package/dist/modules/user_backend/modal/user-modal-view.d.ts +26 -0
  197. package/dist/modules/user_backend/modal/user-modal-view.d.ts.map +1 -0
  198. package/dist/modules/user_backend/modal/user-modal-view.js +320 -0
  199. package/dist/modules/user_backend/modal/user-modal-view.js.map +1 -0
  200. package/dist/modules/user_backend/routes.d.ts.map +1 -1
  201. package/dist/modules/user_backend/routes.js +15 -3
  202. package/dist/modules/user_backend/routes.js.map +1 -1
  203. package/dist/permission-catalogue.d.ts.map +1 -1
  204. package/dist/permission-catalogue.js +8 -0
  205. package/dist/permission-catalogue.js.map +1 -1
  206. package/dist/scaffold/index.js +1 -1
  207. package/dist/ui/client/activity.css +6 -6
  208. package/dist/ui/client/calendar.css +6 -6
  209. package/dist/ui/client/flow-app.css +1 -1
  210. package/dist/ui/client/mail.css +6 -6
  211. package/dist/ui/client/record-modal-form.d.ts +67 -0
  212. package/dist/ui/client/record-modal-form.d.ts.map +1 -0
  213. package/dist/ui/client/record-modal-form.js +45 -0
  214. package/dist/ui/client/record-modal-form.js.map +1 -0
  215. package/dist/ui/client/record-modal.d.ts +97 -6
  216. package/dist/ui/client/record-modal.d.ts.map +1 -1
  217. package/dist/ui/client/record-modal.js +330 -64
  218. package/dist/ui/client/record-modal.js.map +1 -1
  219. package/dist/ui/index.d.ts +2 -1
  220. package/dist/ui/index.d.ts.map +1 -1
  221. package/dist/ui/index.js +2 -1
  222. package/dist/ui/index.js.map +1 -1
  223. package/dist/ui/layout.d.ts.map +1 -1
  224. package/dist/ui/layout.js +5 -3
  225. package/dist/ui/layout.js.map +1 -1
  226. package/dist/ui/nav.d.ts +7 -0
  227. package/dist/ui/nav.d.ts.map +1 -1
  228. package/dist/ui/nav.js +1 -1
  229. package/dist/ui/nav.js.map +1 -1
  230. package/dist/ui/record-modal.d.ts +15 -0
  231. package/dist/ui/record-modal.d.ts.map +1 -1
  232. package/dist/ui/record-modal.js +12 -0
  233. package/dist/ui/record-modal.js.map +1 -1
  234. package/dist/ui/table.d.ts.map +1 -1
  235. package/dist/ui/table.js +1 -1
  236. package/dist/ui/table.js.map +1 -1
  237. package/package.json +4 -4
  238. package/dist/modules/crm/functions.d.ts +0 -10
  239. package/dist/modules/crm/functions.d.ts.map +0 -1
  240. package/dist/modules/crm/functions.js +0 -1394
  241. package/dist/modules/crm/functions.js.map +0 -1
  242. package/dist/modules/product_backend/screens/create.d.ts +0 -16
  243. package/dist/modules/product_backend/screens/create.d.ts.map +0 -1
  244. package/dist/modules/product_backend/screens/create.js +0 -86
  245. package/dist/modules/product_backend/screens/create.js.map +0 -1
@@ -1,14 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { encodeListState, parseListState, table, text } from '@ketvietlab/ketjs';
3
- import { formatDateTime, formatMoney, modalWorkspace } from '../../ui/index.js';
3
+ import { formatDateTime, formatMoney, modalWorkspace, recordModalCreateHref, recordModalHref, } from '../../ui/index.js';
4
4
  import { formRefusal, readForm, seeOther } from '../backend/forms.js';
5
- import { choices, adminPage, inLocale, localeQuery, optional, timezoneOf } from '../backend/screen.js';
5
+ import { 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
+ import { caseFormSchema } from '../crm/functions/index.js';
10
10
  import { assigneeControl, caseControl, partnerControl, productControl, stageControl, tagsControl, teamControl, } from './relation-control.js';
11
- import { CONFIGURATION_TABS, caseCloseModal, caseConvertModal, caseCreateScreen, caseDetailScreen, casesListScreen, configurationScreen, leaderboardScreen, plannerScreen, permissionScreen, pipelineScreen, teamConfigurationScreen, } from './screens/index.js';
11
+ import { CONFIGURATION_RECORD_KINDS, CONFIGURATION_SAVE_FUNCTIONS, CONFIGURATION_SECTIONS, CONFIGURATION_STATUSES, caseCloseModal, caseConvertModal, caseCreateScreen, caseDetailScreen, casesListScreen, configurationScreen, leaderboardScreen, plannerScreen, permissionScreen, pipelineScreen, } from './screens/index.js';
12
12
  import { keepForListSearch, LIST_PAGE_SIZE, listFacets, listMenus, loadListGroups, } from '../backend/list-search.js';
13
13
  const bool = (value) => ['1', 'true', 'on'].includes(value ?? '');
14
14
  const errorsOf = (result, _) => (result?.errors ?? []).map((error) => {
@@ -52,11 +52,40 @@ const configuration = (ctx, url, req) => ctx.call('crm.configuration.get', {}, u
52
52
  * fill the menu that opens before the dialog does.
53
53
  */
54
54
  const PRELOAD = 40;
55
+ /**
56
+ * Call a function only when the viewer may, answering `fallback` otherwise.
57
+ *
58
+ * A configuration or case screen reads several modules at once. One read the
59
+ * viewer's role does not grant used to turn the whole screen into a 403, so a
60
+ * CRM or care manager could not open CRM configuration at all.
61
+ */
62
+ const allowed = async (ctx, name, input, url, req, fallback) => (await ctx.allows(name, url, req)) ? ctx.call(name, input, url, req) : fallback;
63
+ /**
64
+ * The people a CRM screen may offer in its pickers.
65
+ *
66
+ * Listing every user needs `user.listUsers` (the sensitive user bundle). Without
67
+ * it the screen offers the people already in CRM teams, read through the CRM
68
+ * team membership the viewer can see, shaped like user rows (`id`, `name`).
69
+ */
70
+ const people = async (ctx, url, req, limit) => {
71
+ if (await ctx.allows('user.listUsers', url, req))
72
+ return ctx.call('user.listUsers', { includeArchived: false, limit }, url, req);
73
+ const members = await allowed(ctx, 'crm.team.member.list', { limit: 200 }, url, req, []);
74
+ const byUser = new Map();
75
+ for (const member of members) {
76
+ if (member.active === false)
77
+ continue;
78
+ const id = String(member.userId ?? '');
79
+ if (id && !byUser.has(id))
80
+ byUser.set(id, { id, name: member.userName ?? id });
81
+ }
82
+ return [...byUser.values()].slice(0, limit);
83
+ };
55
84
  const references = async (ctx, url, req) => {
56
85
  const [config, partners, users, tags] = await Promise.all([
57
86
  configuration(ctx, url, req),
58
87
  ctx.call('partner.listPartners', { includeArchived: false, limit: PRELOAD }, url, req),
59
- ctx.call('user.listUsers', { includeArchived: false, limit: PRELOAD }, url, req),
88
+ people(ctx, url, req, PRELOAD),
60
89
  ctx.call('crm.tag.list', { limit: PRELOAD }, url, req),
61
90
  ]);
62
91
  return { config, partners, users, tags };
@@ -439,85 +468,57 @@ const pipelineFigures = (_, summary, money) => {
439
468
  },
440
469
  ];
441
470
  };
442
- const configurationTabOf = (url) => {
443
- const asked = url.searchParams.get('tab') ?? '';
444
- return CONFIGURATION_TABS.includes(asked) ? asked : 'teams';
471
+ const configurationSectionOf = (url) => {
472
+ const asked = url.searchParams.get('section') ?? '';
473
+ return CONFIGURATION_SECTIONS.includes(asked)
474
+ ? asked
475
+ : 'teams';
445
476
  };
446
477
  const configurationStatusOf = (url) => {
447
478
  const asked = url.searchParams.get('status') ?? 'active';
448
- return ['active', 'archived', 'all'].includes(asked) ? asked : 'active';
479
+ return CONFIGURATION_STATUSES.includes(asked)
480
+ ? asked
481
+ : 'active';
449
482
  };
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
- });
483
+ /**
484
+ * Where an old configuration link lives now, or null when it is current.
485
+ *
486
+ * Catalogues used to be `?tab=`, which the record modal reserves for its own tab,
487
+ * and editing used `&edit=<id>` / `&create=1`. A `tab` that names a catalogue and
488
+ * comes without a `record` is the old shape; one next to `record` is a modal tab.
489
+ */
490
+ const legacyConfigurationHref = (url) => {
491
+ const tab = url.searchParams.get('tab');
492
+ const legacyTab = tab !== null &&
493
+ !url.searchParams.has('record') &&
494
+ CONFIGURATION_SECTIONS.includes(tab);
495
+ const edit = url.searchParams.get('edit');
496
+ const create = url.searchParams.get('create') === '1';
497
+ if (!legacyTab && edit === null && !create)
498
+ return null;
499
+ const section = legacyTab ? tab : configurationSectionOf(url);
500
+ const next = new URL(`${url.pathname}${url.search}`, 'http://ket.local');
501
+ if (legacyTab)
502
+ next.searchParams.delete('tab');
503
+ next.searchParams.delete('edit');
504
+ next.searchParams.delete('create');
505
+ next.searchParams.set('section', section);
506
+ const path = `${next.pathname}${next.search}`;
507
+ const kind = CONFIGURATION_RECORD_KINDS[section];
508
+ if (edit)
509
+ return recordModalHref(path, { kind, id: edit });
510
+ if (create)
511
+ return recordModalCreateHref(path, { kind });
512
+ return path;
513
+ };
514
+ /** The team pages became the team record modal over the teams catalogue. */
515
+ const teamPageRedirect = (creating) => async (url, req, params) => {
516
+ if (req.method !== 'GET')
517
+ return text('GET', { status: 405 });
518
+ const list = inLocale(url, '/admin/crm/configuration?section=teams');
519
+ return seeOther(creating
520
+ ? recordModalCreateHref(list, { kind: 'crm.team' })
521
+ : recordModalHref(list, { kind: 'crm.team', id: String(params.id) }));
521
522
  };
522
523
  export const routes = {
523
524
  '/admin/crm': () => async (url, req) => req.method === 'GET' ? seeOther(inLocale(url, '/admin/crm/pipeline')) : text('GET', { status: 405 }),
@@ -1042,11 +1043,11 @@ export const routes = {
1042
1043
  url.searchParams.get('modal') === 'close';
1043
1044
  const conversionStages = converting ? stagesFor('opportunity') : [];
1044
1045
  const [warehouses, plans, activityTypes, duplicateResult, quotations, products] = await Promise.all([
1045
- ctx.call('stock.listWarehouses', {}, url, req),
1046
- ctx.call('activity.listPlans', {}, url, req),
1047
- ctx.call('activity.listTypes', {}, url, req),
1046
+ allowed(ctx, 'stock.listWarehouses', {}, url, req, []),
1047
+ allowed(ctx, 'activity.listPlans', {}, url, req, { plans: [] }),
1048
+ allowed(ctx, 'activity.listTypes', {}, url, req, []),
1048
1049
  ctx.call('crm.case.detectDuplicates', { id: row.id, email: row.email, phone: row.phone, name: row.name }, url, req),
1049
- ctx.call('crm_sale.sale.listQuotations', { caseId: params.id }, url, req),
1050
+ allowed(ctx, 'crm_sale.sale.listQuotations', { caseId: params.id }, url, req, []),
1050
1051
  row.kind === 'opportunity'
1051
1052
  ? ctx.call('crm_sale.sale.listQuotableProducts', { limit: PRELOAD }, url, req)
1052
1053
  : Promise.resolve([]),
@@ -1227,10 +1228,10 @@ export const routes = {
1227
1228
  : 'mine';
1228
1229
  const [activities, plans, calendar, activityTypes, users] = await Promise.all([
1229
1230
  ctx.call('crm.activity.listMine', { today: new Date().toISOString().slice(0, 10), includeDone: false }, url, req),
1230
- ctx.call('activity.listPlans', {}, url, req),
1231
+ allowed(ctx, 'activity.listPlans', {}, url, req, { plans: [] }),
1231
1232
  ctx.call('crm.calendar.list', { cursor: '0', limit: 100 }, url, req),
1232
- ctx.call('activity.listTypes', {}, url, req),
1233
- ctx.call('user.listUsers', { includeArchived: false, limit: PRELOAD }, url, req),
1233
+ allowed(ctx, 'activity.listTypes', {}, url, req, []),
1234
+ people(ctx, url, req, PRELOAD),
1234
1235
  ]);
1235
1236
  // The target used to be a select over a thousand cases, capped at two
1236
1237
  // hundred by the list function without saying so. A picker searches the
@@ -1290,299 +1291,37 @@ export const routes = {
1290
1291
  const refused = refusePost(req);
1291
1292
  if (refused)
1292
1293
  return refused;
1293
- const _ = ctx.translate(ctx.localeOf(url, req));
1294
- const tab = configurationTabOf(url);
1294
+ if (req.method !== 'GET')
1295
+ return text('GET', { status: 405 });
1296
+ const legacy = legacyConfigurationHref(url);
1297
+ if (legacy)
1298
+ return seeOther(legacy);
1299
+ const section = configurationSectionOf(url);
1295
1300
  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'));
1302
- let errors = [];
1303
- if (req.method === 'POST') {
1304
- if (crossSite(req))
1305
- return text('Forbidden', { status: 403 });
1306
- const form = await readForm(req);
1307
- const call = (name, input) => ctx.call(name, input, url, req);
1308
- const id = form.id || randomUUID();
1309
- const result = await configurationWrite(call, tab, form, id);
1310
- if (result.ok)
1311
- return seeOther(back);
1312
- errors = errorsOf(result, _);
1313
- }
1314
- else if (req.method !== 'GET')
1315
- return text('GET or POST', { status: 405 });
1316
- const [config, tags, users] = await Promise.all([
1301
+ const [config, tags, users, canCreate] = await Promise.all([
1317
1302
  configuration(ctx, url, req),
1318
- ctx.call('crm.tag.list', { includeArchived: true, limit: 200 }, url, req),
1319
- ctx.call('user.listUsers', { includeArchived: false, limit: 200 }, url, req),
1303
+ section === 'tags'
1304
+ ? allowed(ctx, 'crm.tag.list', { includeArchived: true, limit: 200 }, url, req, [])
1305
+ : Promise.resolve([]),
1306
+ people(ctx, url, req, 200),
1307
+ ctx.allows(CONFIGURATION_SAVE_FUNCTIONS[section], url, req),
1320
1308
  ]);
1321
- const allRows = tab === 'tags' ? tags : (config[tab] ?? []);
1309
+ const allRows = section === 'tags' ? tags : (config[section] ?? []);
1322
1310
  const rows = allRows.filter((row) => status === 'all' ? true : status === 'active' ? row.active !== false : row.active === false);
1323
- const asked = url.searchParams.get('edit');
1324
- const editing = asked ? (allRows.find((row) => String(row.id) === asked) ?? null) : null;
1325
- const teams = config.teams ?? [];
1326
1311
  return adminPage(ctx, url, req, {
1327
1312
  title: 'crm_backend.configuration.title',
1328
1313
  body: (_, frame) => configurationScreen(_, frame, {
1329
- tab,
1314
+ section,
1315
+ status,
1330
1316
  rows,
1331
- editing,
1332
- creating: url.searchParams.get('create') === '1' || (req.method === 'POST' && !editing),
1333
- errors,
1334
1317
  locale: localeQuery(url),
1335
- status,
1336
- teams,
1318
+ teams: config.teams ?? [],
1337
1319
  users,
1338
- fields: configurationFields(_, tab, editing, teams, users),
1320
+ canCreate,
1339
1321
  }),
1340
1322
  });
1341
1323
  },
1342
- '/admin/crm/configuration/teams/new': (ctx) => teamConfigurationRoute(ctx, true),
1343
- '/admin/crm/configuration/teams/{id}': (ctx) => teamConfigurationRoute(ctx, false),
1324
+ '/admin/crm/configuration/teams/new': () => teamPageRedirect(true),
1325
+ '/admin/crm/configuration/teams/{id}': () => teamPageRedirect(false),
1344
1326
  };
1345
- /**
1346
- * One write per configuration tab.
1347
- *
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.
1350
- */
1351
- async function configurationWrite(call, tab, form, id) {
1352
- const active = bool(form.active);
1353
- if (tab === 'tags') {
1354
- return call('crm.tag.save', { id, name: form.name ?? '', active });
1355
- }
1356
- const fn = {
1357
- teams: 'crm.team.save',
1358
- stages: 'crm.stage.save',
1359
- assignmentRules: 'crm.assignmentRule.save',
1360
- scoreRules: 'crm.scoreRule.save',
1361
- }[tab];
1362
- const values = {
1363
- ...form,
1364
- id,
1365
- active,
1366
- ...(form.expectedVersion ? { expectedVersion: Number(form.expectedVersion) } : {}),
1367
- ...(['stages', 'assignmentRules'].includes(tab)
1368
- ? { allowedKinds: ['lead', 'opportunity'].filter((kind) => bool(form[`kind_${kind}`])) }
1369
- : {}),
1370
- ...(form.sequence === undefined ? {} : { sequence: Number(form.sequence) }),
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) }),
1379
- };
1380
- delete values.action;
1381
- delete values.kind_lead;
1382
- delete values.kind_opportunity;
1383
- return call(fn, { values, idempotencyKey: randomUUID() });
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
- }
1428
- /** The form for one configuration tab, pre-filled when a row is being edited. */
1429
- function configurationFields(_, tab, editing, teams, users) {
1430
- const value = (name, fallback = '') => String(editing?.[name] ?? fallback);
1431
- const common = [
1432
- { name: 'name', label: _('crm_backend.field.name'), value: value('name'), required: true },
1433
- {
1434
- name: 'active',
1435
- label: _('crm_backend.field.active'),
1436
- type: 'checkbox',
1437
- value: editing ? editing.active !== false : true,
1438
- },
1439
- ];
1440
- if (tab === 'tags')
1441
- return common;
1442
- if (tab === 'teams')
1443
- return [
1444
- ...common,
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
- },
1453
- {
1454
- name: 'assignmentMode',
1455
- label: _('crm_backend.field.assignmentMode'),
1456
- type: 'select',
1457
- value: value('assignmentMode', 'manual'),
1458
- options: ['manual', 'round_robin', 'capacity'].map((item) => ({
1459
- value: item,
1460
- label: _(`crm_backend.assignmentMode.${item}`),
1461
- })),
1462
- },
1463
- ];
1464
- if (tab === 'stages')
1465
- return [
1466
- ...common,
1467
- { name: 'code', label: _('crm_backend.field.code'), value: value('code'), required: true },
1468
- {
1469
- name: 'sequence',
1470
- label: _('crm_backend.field.sequence'),
1471
- type: 'number',
1472
- value: value('sequence', '10'),
1473
- },
1474
- {
1475
- name: 'allowedKinds',
1476
- label: _('crm_backend.field.allowedKinds'),
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)],
1494
- },
1495
- {
1496
- name: 'terminalState',
1497
- label: _('crm_backend.field.state'),
1498
- type: 'select',
1499
- value: value('terminalState', 'open'),
1500
- options: ['open', 'won', 'lost'].map((item) => ({ value: item, label: _(`crm.terminal.${item}`) })),
1501
- },
1502
- {
1503
- name: 'fold',
1504
- label: _('crm_backend.field.fold'),
1505
- type: 'checkbox',
1506
- value: editing?.fold === true,
1507
- },
1508
- ];
1509
- if (tab === 'assignmentRules')
1510
- return [
1511
- ...common,
1512
- {
1513
- name: 'priority',
1514
- label: _('crm_backend.field.priority'),
1515
- type: 'number',
1516
- value: value('priority', '10'),
1517
- },
1518
- {
1519
- name: 'allowedKinds',
1520
- label: _('crm_backend.field.allowedKinds'),
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
- })),
1531
- },
1532
- {
1533
- name: 'teamId',
1534
- label: _('crm_backend.field.team'),
1535
- type: 'select',
1536
- required: true,
1537
- value: value('teamId'),
1538
- options: choices(teams),
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
- },
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
- },
1554
- ];
1555
- return [
1556
- ...common,
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
- },
1568
- {
1569
- name: 'operator',
1570
- label: _('crm_backend.field.operator'),
1571
- type: 'select',
1572
- value: value('operator', 'eq'),
1573
- options: ['eq', 'contains', 'present', 'gte'].map((item) => ({
1574
- value: item,
1575
- label: _(`crm_backend.operator.${item}`),
1576
- })),
1577
- },
1578
- { name: 'value', label: _('crm_backend.field.ruleValue'), value: value('value') },
1579
- { name: 'points', label: _('crm_backend.field.points'), type: 'number', value: value('points', '0') },
1580
- {
1581
- name: 'sequence',
1582
- label: _('crm_backend.field.sequence'),
1583
- type: 'number',
1584
- value: value('sequence', '10'),
1585
- },
1586
- ];
1587
- }
1588
1327
  //# sourceMappingURL=routes.js.map