@ketvietlab/ketsuite 0.1.3 → 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.
- package/dist/cli.js +1 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +3 -0
- package/dist/deployment.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/account_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/account_staff_channel/channel-routes.js +1 -12
- package/dist/modules/account_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/attendance/channel-routes.d.ts.map +1 -1
- package/dist/modules/attendance/channel-routes.js +1 -12
- package/dist/modules/attendance/channel-routes.js.map +1 -1
- package/dist/modules/backend/design/HANDOFF.md +2 -2
- package/dist/modules/backend/design/auth.css +12 -0
- package/dist/modules/backend/design/client/table-selection.d.mts +3 -3
- package/dist/modules/backend/design/client/table-selection.d.mts.map +1 -1
- package/dist/modules/backend/design/client/table-selection.mjs +1 -1
- package/dist/modules/backend/design/client/table-selection.mjs.map +4 -4
- package/dist/modules/backend/design/content.css +43 -0
- package/dist/modules/backend/design/design-system.css +1 -1
- package/dist/modules/backend/design/forms.css +84 -1
- package/dist/modules/backend/design/lists.css +26 -18
- package/dist/modules/backend/design/record.css +5 -2
- package/dist/modules/backend/design/responsive.css +5 -6
- package/dist/modules/backend/design/user-workflow.css +257 -0
- package/dist/modules/backend/forms.d.ts +38 -1
- package/dist/modules/backend/forms.d.ts.map +1 -1
- package/dist/modules/backend/forms.js +29 -0
- package/dist/modules/backend/forms.js.map +1 -1
- package/dist/modules/backend/index.d.ts +3 -2
- package/dist/modules/backend/index.d.ts.map +1 -1
- package/dist/modules/backend/index.js +3 -2
- package/dist/modules/backend/index.js.map +1 -1
- package/dist/modules/backend/messages.d.ts.map +1 -1
- package/dist/modules/backend/messages.js +42 -2
- package/dist/modules/backend/messages.js.map +1 -1
- package/dist/modules/channel_api/core.d.ts +30 -0
- package/dist/modules/channel_api/core.d.ts.map +1 -1
- package/dist/modules/channel_api/core.js +28 -0
- package/dist/modules/channel_api/core.js.map +1 -1
- package/dist/modules/crm/functions.d.ts +5 -0
- package/dist/modules/crm/functions.d.ts.map +1 -1
- package/dist/modules/crm/functions.js +105 -10
- package/dist/modules/crm/functions.js.map +1 -1
- package/dist/modules/crm/jobs.d.ts.map +1 -1
- package/dist/modules/crm/jobs.js +1 -0
- package/dist/modules/crm/jobs.js.map +1 -1
- package/dist/modules/crm/operations.d.ts.map +1 -1
- package/dist/modules/crm/operations.js +2 -1
- package/dist/modules/crm/operations.js.map +1 -1
- package/dist/modules/crm_backend/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/index.js +5 -2
- package/dist/modules/crm_backend/index.js.map +1 -1
- package/dist/modules/crm_backend/messages.d.ts +116 -0
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +122 -6
- package/dist/modules/crm_backend/messages.js.map +1 -1
- package/dist/modules/crm_backend/routes.d.ts.map +1 -1
- package/dist/modules/crm_backend/routes.js +434 -174
- package/dist/modules/crm_backend/routes.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.d.ts +7 -0
- package/dist/modules/crm_backend/screens/case-create.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.js +13 -4
- package/dist/modules/crm_backend/screens/case-create.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.d.ts +6 -0
- package/dist/modules/crm_backend/screens/case-detail.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.js +73 -23
- package/dist/modules/crm_backend/screens/case-detail.js.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.d.ts +21 -3
- package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.js +260 -55
- package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
- package/dist/modules/crm_backend/screens/index.d.ts +3 -3
- package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/index.js +2 -2
- package/dist/modules/crm_backend/screens/index.js.map +1 -1
- package/dist/modules/crm_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/crm_staff_channel/channel-routes.js +1 -12
- package/dist/modules/crm_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/flow/membership.d.ts.map +1 -1
- package/dist/modules/flow/membership.js +7 -1
- package/dist/modules/flow/membership.js.map +1 -1
- package/dist/modules/flow/operations.d.ts +10 -4
- package/dist/modules/flow/operations.d.ts.map +1 -1
- package/dist/modules/flow/operations.js +10 -4
- package/dist/modules/flow/operations.js.map +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.js +1 -20
- package/dist/modules/flow_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/hospitality_core/messages.d.ts.map +1 -1
- package/dist/modules/hospitality_core/messages.js +44 -0
- package/dist/modules/hospitality_core/messages.js.map +1 -1
- package/dist/modules/hospitality_core/night-audit.d.ts.map +1 -1
- package/dist/modules/hospitality_core/night-audit.js +7 -0
- package/dist/modules/hospitality_core/night-audit.js.map +1 -1
- package/dist/modules/hospitality_core/operations.d.ts.map +1 -1
- package/dist/modules/hospitality_core/operations.js +150 -40
- package/dist/modules/hospitality_core/operations.js.map +1 -1
- package/dist/modules/hospitality_core/routes.d.ts.map +1 -1
- package/dist/modules/hospitality_core/routes.js +24 -2
- package/dist/modules/hospitality_core/routes.js.map +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.d.ts +46 -0
- package/dist/modules/hospitality_core/screens/check-out-prep.d.ts.map +1 -0
- package/dist/modules/hospitality_core/screens/check-out-prep.js +133 -0
- package/dist/modules/hospitality_core/screens/check-out-prep.js.map +1 -0
- package/dist/modules/hospitality_core/screens/folio-detail.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/folio-detail.js +2 -4
- package/dist/modules/hospitality_core/screens/folio-detail.js.map +1 -1
- package/dist/modules/hospitality_core/screens/index.d.ts +2 -0
- package/dist/modules/hospitality_core/screens/index.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/index.js +1 -0
- package/dist/modules/hospitality_core/screens/index.js.map +1 -1
- package/dist/modules/hospitality_core/screens/reservation-detail.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/reservation-detail.js +8 -1
- package/dist/modules/hospitality_core/screens/reservation-detail.js.map +1 -1
- package/dist/modules/hospitality_core/screens/shared.d.ts +12 -0
- package/dist/modules/hospitality_core/screens/shared.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/shared.js +25 -4
- package/dist/modules/hospitality_core/screens/shared.js.map +1 -1
- package/dist/modules/hospitality_core/services.d.ts +48 -0
- package/dist/modules/hospitality_core/services.d.ts.map +1 -1
- package/dist/modules/hospitality_core/services.js +70 -0
- package/dist/modules/hospitality_core/services.js.map +1 -1
- package/dist/modules/hospitality_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/hospitality_staff_channel/channel-routes.js +1 -12
- package/dist/modules/hospitality_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/inventory_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/inventory_staff_channel/channel-routes.js +1 -12
- package/dist/modules/inventory_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/loyalty/admin-functions.d.ts +2 -0
- package/dist/modules/loyalty/admin-functions.d.ts.map +1 -1
- package/dist/modules/loyalty/admin-functions.js +96 -20
- package/dist/modules/loyalty/admin-functions.js.map +1 -1
- package/dist/modules/loyalty/engine.d.ts +4 -1
- package/dist/modules/loyalty/engine.d.ts.map +1 -1
- package/dist/modules/loyalty/engine.js +125 -18
- package/dist/modules/loyalty/engine.js.map +1 -1
- package/dist/modules/loyalty/index.d.ts.map +1 -1
- package/dist/modules/loyalty/index.js +4 -3
- package/dist/modules/loyalty/index.js.map +1 -1
- package/dist/modules/loyalty/jobs.d.ts.map +1 -1
- package/dist/modules/loyalty/jobs.js +0 -1
- package/dist/modules/loyalty/jobs.js.map +1 -1
- package/dist/modules/loyalty/membership-functions.d.ts +5 -0
- package/dist/modules/loyalty/membership-functions.d.ts.map +1 -1
- package/dist/modules/loyalty/membership-functions.js +37 -23
- package/dist/modules/loyalty/membership-functions.js.map +1 -1
- package/dist/modules/loyalty/messages.d.ts +14 -0
- package/dist/modules/loyalty/messages.d.ts.map +1 -1
- package/dist/modules/loyalty/messages.js +14 -0
- package/dist/modules/loyalty/messages.js.map +1 -1
- package/dist/modules/loyalty/models.d.ts.map +1 -1
- package/dist/modules/loyalty/models.js +17 -0
- package/dist/modules/loyalty/models.js.map +1 -1
- package/dist/modules/loyalty/order-functions.d.ts.map +1 -1
- package/dist/modules/loyalty/order-functions.js +70 -17
- package/dist/modules/loyalty/order-functions.js.map +1 -1
- package/dist/modules/loyalty/program-design.d.ts +11 -0
- package/dist/modules/loyalty/program-design.d.ts.map +1 -0
- package/dist/modules/loyalty/program-design.js +356 -0
- package/dist/modules/loyalty/program-design.js.map +1 -0
- package/dist/modules/loyalty/stats.d.ts +0 -13
- package/dist/modules/loyalty/stats.d.ts.map +1 -1
- package/dist/modules/loyalty/stats.js +11 -6
- package/dist/modules/loyalty/stats.js.map +1 -1
- package/dist/modules/loyalty/stored-value.d.ts.map +1 -1
- package/dist/modules/loyalty/stored-value.js +7 -2
- package/dist/modules/loyalty/stored-value.js.map +1 -1
- package/dist/modules/loyalty/types.d.ts +1 -1
- package/dist/modules/loyalty/types.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/design-messages.d.ts +221 -0
- package/dist/modules/loyalty_backend/design-messages.d.ts.map +1 -0
- package/dist/modules/loyalty_backend/design-messages.js +221 -0
- package/dist/modules/loyalty_backend/design-messages.js.map +1 -0
- package/dist/modules/loyalty_backend/index.js +224 -412
- package/dist/modules/loyalty_backend/index.js.map +1 -1
- package/dist/modules/loyalty_backend/messages.d.ts +642 -214
- package/dist/modules/loyalty_backend/messages.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/messages.js +19 -13
- package/dist/modules/loyalty_backend/messages.js.map +1 -1
- package/dist/modules/loyalty_backend/program-workspace.d.ts +4 -0
- package/dist/modules/loyalty_backend/program-workspace.d.ts.map +1 -0
- package/dist/modules/loyalty_backend/program-workspace.js +600 -0
- package/dist/modules/loyalty_backend/program-workspace.js.map +1 -0
- package/dist/modules/loyalty_backend/screens/index.d.ts +12 -36
- package/dist/modules/loyalty_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/index.js +148 -292
- package/dist/modules/loyalty_backend/screens/index.js.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.js +2 -2
- package/dist/modules/loyalty_backend/screens/page-frame.js.map +1 -1
- package/dist/modules/loyalty_pos/functions.d.ts.map +1 -1
- package/dist/modules/loyalty_pos/functions.js +1 -0
- package/dist/modules/loyalty_pos/functions.js.map +1 -1
- package/dist/modules/loyalty_pos/index.js +1 -1
- package/dist/modules/loyalty_pos/index.js.map +1 -1
- package/dist/modules/loyalty_sale/functions.d.ts +1 -0
- package/dist/modules/loyalty_sale/functions.d.ts.map +1 -1
- package/dist/modules/loyalty_sale/functions.js +1 -0
- package/dist/modules/loyalty_sale/functions.js.map +1 -1
- package/dist/modules/partner_backend/client/partner.css +3 -1
- package/dist/modules/partner_backend/index.d.ts.map +1 -1
- package/dist/modules/partner_backend/index.js +4 -0
- package/dist/modules/partner_backend/index.js.map +1 -1
- package/dist/modules/partner_backend/routes.d.ts.map +1 -1
- package/dist/modules/partner_backend/routes.js +6 -1
- package/dist/modules/partner_backend/routes.js.map +1 -1
- package/dist/modules/partner_backend/screens/form.d.ts +1 -0
- package/dist/modules/partner_backend/screens/form.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/form.js +1 -0
- package/dist/modules/partner_backend/screens/form.js.map +1 -1
- package/dist/modules/purchase_staff_channel/order-routes.d.ts.map +1 -1
- package/dist/modules/purchase_staff_channel/order-routes.js +1 -12
- package/dist/modules/purchase_staff_channel/order-routes.js.map +1 -1
- package/dist/modules/quality_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/quality_staff_channel/channel-routes.js +1 -12
- package/dist/modules/quality_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/sale/functions.d.ts.map +1 -1
- package/dist/modules/sale/functions.js +27 -17
- package/dist/modules/sale/functions.js.map +1 -1
- package/dist/modules/sale_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/sale_staff_channel/channel-routes.js +1 -12
- package/dist/modules/sale_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/sale_staff_channel/order-routes.d.ts.map +1 -1
- package/dist/modules/sale_staff_channel/order-routes.js +1 -12
- package/dist/modules/sale_staff_channel/order-routes.js.map +1 -1
- package/dist/modules/stock_staff_channel/operation-routes.d.ts.map +1 -1
- package/dist/modules/stock_staff_channel/operation-routes.js +1 -12
- package/dist/modules/stock_staff_channel/operation-routes.js.map +1 -1
- package/dist/modules/user/authorization.d.ts +64 -1
- package/dist/modules/user/authorization.d.ts.map +1 -1
- package/dist/modules/user/authorization.js +445 -53
- package/dist/modules/user/authorization.js.map +1 -1
- package/dist/modules/user/functions.d.ts.map +1 -1
- package/dist/modules/user/functions.js +45 -23
- package/dist/modules/user/functions.js.map +1 -1
- package/dist/modules/user/index.d.ts +2 -1
- package/dist/modules/user/index.d.ts.map +1 -1
- package/dist/modules/user/index.js +1 -1
- package/dist/modules/user/index.js.map +1 -1
- package/dist/modules/user/models.d.ts.map +1 -1
- package/dist/modules/user/models.js +5 -0
- package/dist/modules/user/models.js.map +1 -1
- package/dist/modules/user/roles.d.ts.map +1 -1
- package/dist/modules/user/roles.js +2 -1
- package/dist/modules/user/roles.js.map +1 -1
- package/dist/modules/user_backend/access-routes.d.ts +31 -0
- package/dist/modules/user_backend/access-routes.d.ts.map +1 -0
- package/dist/modules/user_backend/access-routes.js +447 -0
- package/dist/modules/user_backend/access-routes.js.map +1 -0
- package/dist/modules/user_backend/index.d.ts +1 -0
- package/dist/modules/user_backend/index.d.ts.map +1 -1
- package/dist/modules/user_backend/index.js +1 -0
- package/dist/modules/user_backend/index.js.map +1 -1
- package/dist/modules/user_backend/messages.d.ts +4 -0
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +4 -0
- package/dist/modules/user_backend/messages.js.map +1 -1
- package/dist/modules/user_backend/routes.d.ts.map +1 -1
- package/dist/modules/user_backend/routes.js +38 -3
- package/dist/modules/user_backend/routes.js.map +1 -1
- package/dist/modules/user_backend/screens/user-form.d.ts +1 -0
- package/dist/modules/user_backend/screens/user-form.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/user-form.js +28 -0
- package/dist/modules/user_backend/screens/user-form.js.map +1 -1
- package/dist/modules/website/cms.d.ts.map +1 -1
- package/dist/modules/website/cms.js +283 -6
- package/dist/modules/website/cms.js.map +1 -1
- package/dist/modules/website/index.d.ts.map +1 -1
- package/dist/modules/website/index.js +12 -0
- package/dist/modules/website/index.js.map +1 -1
- package/dist/modules/website/media-usage.d.ts +38 -0
- package/dist/modules/website/media-usage.d.ts.map +1 -0
- package/dist/modules/website/media-usage.js +87 -0
- package/dist/modules/website/media-usage.js.map +1 -0
- package/dist/modules/website/paths.js +1 -1
- package/dist/modules/website/paths.js.map +1 -1
- package/dist/modules/website/sections.d.ts.map +1 -1
- package/dist/modules/website/sections.js +12 -1
- package/dist/modules/website/sections.js.map +1 -1
- package/dist/modules/website_backend/index.js +138 -0
- package/dist/modules/website_backend/index.js.map +1 -1
- package/dist/modules/website_backend/menus.d.ts.map +1 -1
- package/dist/modules/website_backend/menus.js +11 -0
- package/dist/modules/website_backend/menus.js.map +1 -1
- package/dist/modules/website_backend/routes.d.ts.map +1 -1
- package/dist/modules/website_backend/routes.js +414 -31
- package/dist/modules/website_backend/routes.js.map +1 -1
- package/dist/modules/website_backend/screens/index.d.ts +98 -6
- package/dist/modules/website_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/index.js +600 -154
- package/dist/modules/website_backend/screens/index.js.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.d.ts +3 -1
- package/dist/modules/website_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.js +1 -1
- package/dist/modules/website_backend/screens/page-frame.js.map +1 -1
- package/dist/modules/website_menu/functions.d.ts.map +1 -1
- package/dist/modules/website_menu/functions.js +51 -0
- package/dist/modules/website_menu/functions.js.map +1 -1
- package/dist/modules/website_menu/index.js +2 -0
- package/dist/modules/website_menu/index.js.map +1 -1
- package/dist/modules/website_search/functions.d.ts.map +1 -1
- package/dist/modules/website_search/functions.js +10 -120
- package/dist/modules/website_search/functions.js.map +1 -1
- package/dist/modules/website_search/index.js +2 -0
- package/dist/modules/website_search/index.js.map +1 -1
- package/dist/modules/website_search/jobs.d.ts +3 -0
- package/dist/modules/website_search/jobs.d.ts.map +1 -0
- package/dist/modules/website_search/jobs.js +101 -0
- package/dist/modules/website_search/jobs.js.map +1 -0
- package/dist/modules/website_search/rebuild.d.ts +33 -0
- package/dist/modules/website_search/rebuild.d.ts.map +1 -0
- package/dist/modules/website_search/rebuild.js +125 -0
- package/dist/modules/website_search/rebuild.js.map +1 -0
- package/dist/permission-catalogue.d.ts.map +1 -1
- package/dist/permission-catalogue.js +21 -1
- package/dist/permission-catalogue.js.map +1 -1
- package/dist/scaffold/index.js +1 -1
- package/dist/ui/chrome.d.ts +8 -0
- package/dist/ui/chrome.d.ts.map +1 -1
- package/dist/ui/chrome.js +9 -1
- package/dist/ui/chrome.js.map +1 -1
- package/dist/ui/client/table-selection-view.d.ts.map +1 -1
- package/dist/ui/client/table-selection-view.js +161 -5
- package/dist/ui/client/table-selection-view.js.map +1 -1
- package/dist/ui/client/user-workflow.d.ts +2 -0
- package/dist/ui/client/user-workflow.d.ts.map +1 -0
- package/dist/ui/client/user-workflow.js +141 -0
- package/dist/ui/client/user-workflow.js.map +1 -0
- package/dist/ui/form.d.ts +9 -0
- package/dist/ui/form.d.ts.map +1 -1
- package/dist/ui/form.js +29 -15
- package/dist/ui/form.js.map +1 -1
- package/dist/ui/icons.d.ts.map +1 -1
- package/dist/ui/icons.js +2 -0
- package/dist/ui/icons.js.map +1 -1
- package/dist/ui/index.d.ts +7 -3
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +6 -3
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +14 -4
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +2 -1
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/state.d.ts +20 -1
- package/dist/ui/state.d.ts.map +1 -1
- package/dist/ui/state.js +17 -0
- package/dist/ui/state.js.map +1 -1
- package/dist/ui/table.js +1 -1
- package/dist/ui/table.js.map +1 -1
- package/dist/ui/user-workflow.d.ts +72 -0
- package/dist/ui/user-workflow.d.ts.map +1 -0
- package/dist/ui/user-workflow.js +144 -0
- package/dist/ui/user-workflow.js.map +1 -0
- 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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
name: '
|
|
118
|
-
label: _('crm_backend.field.
|
|
119
|
-
type: '
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
247
|
+
/** Only CRM views and a Partner record are valid destinations carried through the create form. */
|
|
213
248
|
const caseReturnTo = (url, raw) => {
|
|
214
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
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,
|
|
1141
|
+
cancelHref: inLocale(url, closeOverlay()),
|
|
934
1142
|
control: controls.convertStage,
|
|
935
1143
|
errors,
|
|
936
1144
|
}))
|
|
937
|
-
:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 ? (
|
|
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
|
|
1138
|
-
*
|
|
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
|
|
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
|
-
...(
|
|
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
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
{
|
|
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'),
|