@ketvietlab/ketsuite 0.1.22 → 0.1.25
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 +9 -3
- package/dist/cli.js.map +1 -1
- package/dist/demo/crm.d.ts +3 -0
- package/dist/demo/crm.d.ts.map +1 -0
- package/dist/demo/crm.js +418 -0
- package/dist/demo/crm.js.map +1 -0
- package/dist/demo/partner.d.ts +8 -0
- package/dist/demo/partner.d.ts.map +1 -0
- package/dist/demo/partner.js +179 -0
- package/dist/demo/partner.js.map +1 -0
- package/dist/demo/product.d.ts +25 -0
- package/dist/demo/product.d.ts.map +1 -0
- package/dist/demo/product.js +579 -0
- package/dist/demo/product.js.map +1 -0
- package/dist/demo/purchase.d.ts +4 -0
- package/dist/demo/purchase.d.ts.map +1 -0
- package/dist/demo/purchase.js +58 -0
- package/dist/demo/purchase.js.map +1 -0
- package/dist/demo/sale.d.ts +4 -0
- package/dist/demo/sale.d.ts.map +1 -0
- package/dist/demo/sale.js +52 -0
- package/dist/demo/sale.js.map +1 -0
- package/dist/demo/seed.d.ts +14 -0
- package/dist/demo/seed.d.ts.map +1 -0
- package/dist/demo/seed.js +88 -0
- package/dist/demo/seed.js.map +1 -0
- package/dist/demo/stock.d.ts +7 -0
- package/dist/demo/stock.d.ts.map +1 -0
- package/dist/demo/stock.js +26 -0
- package/dist/demo/stock.js.map +1 -0
- package/dist/demo/user.d.ts +4 -0
- package/dist/demo/user.d.ts.map +1 -0
- package/dist/demo/user.js +41 -0
- package/dist/demo/user.js.map +1 -0
- package/dist/demo/util.d.ts +7 -0
- package/dist/demo/util.d.ts.map +1 -0
- package/dist/demo/util.js +19 -0
- package/dist/demo/util.js.map +1 -0
- package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
- package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs.map +3 -3
- package/dist/modules/backend/design/forms.css +7 -0
- package/dist/modules/backend/screen.d.ts.map +1 -1
- package/dist/modules/backend/screen.js +11 -2
- package/dist/modules/backend/screen.js.map +1 -1
- package/dist/modules/crm/functions/access.d.ts +3 -0
- package/dist/modules/crm/functions/access.d.ts.map +1 -0
- package/dist/modules/crm/functions/access.js +89 -0
- package/dist/modules/crm/functions/access.js.map +1 -0
- package/dist/modules/crm/functions/activities.d.ts +3 -0
- package/dist/modules/crm/functions/activities.d.ts.map +1 -0
- package/dist/modules/crm/functions/activities.js +174 -0
- package/dist/modules/crm/functions/activities.js.map +1 -0
- package/dist/modules/crm/functions/cases.d.ts +3 -0
- package/dist/modules/crm/functions/cases.d.ts.map +1 -0
- package/dist/modules/crm/functions/cases.js +585 -0
- package/dist/modules/crm/functions/cases.js.map +1 -0
- package/dist/modules/crm/functions/configuration.d.ts +3 -0
- package/dist/modules/crm/functions/configuration.d.ts.map +1 -0
- package/dist/modules/crm/functions/configuration.js +331 -0
- package/dist/modules/crm/functions/configuration.js.map +1 -0
- package/dist/modules/crm/functions/gamification.d.ts +3 -0
- package/dist/modules/crm/functions/gamification.d.ts.map +1 -0
- package/dist/modules/crm/functions/gamification.js +58 -0
- package/dist/modules/crm/functions/gamification.js.map +1 -0
- package/dist/modules/crm/functions/index.d.ts +4 -0
- package/dist/modules/crm/functions/index.d.ts.map +1 -0
- package/dist/modules/crm/functions/index.js +33 -0
- package/dist/modules/crm/functions/index.js.map +1 -0
- package/dist/modules/crm/functions/modal-context.d.ts +3 -0
- package/dist/modules/crm/functions/modal-context.d.ts.map +1 -0
- package/dist/modules/crm/functions/modal-context.js +248 -0
- package/dist/modules/crm/functions/modal-context.js.map +1 -0
- package/dist/modules/crm/functions/previews.d.ts +3 -0
- package/dist/modules/crm/functions/previews.d.ts.map +1 -0
- package/dist/modules/crm/functions/previews.js +20 -0
- package/dist/modules/crm/functions/previews.js.map +1 -0
- package/dist/modules/crm/functions/shared.d.ts +31 -0
- package/dist/modules/crm/functions/shared.d.ts.map +1 -0
- package/dist/modules/crm/functions/shared.js +194 -0
- package/dist/modules/crm/functions/shared.js.map +1 -0
- package/dist/modules/crm/index.d.ts +1 -1
- package/dist/modules/crm/index.d.ts.map +1 -1
- package/dist/modules/crm/index.js +2 -2
- package/dist/modules/crm/index.js.map +1 -1
- package/dist/modules/crm/messages.d.ts +4 -1
- package/dist/modules/crm/messages.d.ts.map +1 -1
- package/dist/modules/crm/messages.js +6 -0
- package/dist/modules/crm/messages.js.map +1 -1
- package/dist/modules/crm/record-modal-labels.d.ts +9 -0
- package/dist/modules/crm/record-modal-labels.d.ts.map +1 -0
- package/dist/modules/crm/record-modal-labels.js +36 -0
- package/dist/modules/crm/record-modal-labels.js.map +1 -0
- package/dist/modules/crm/types.d.ts +5 -0
- package/dist/modules/crm/types.d.ts.map +1 -1
- package/dist/modules/crm/types.js +9 -0
- package/dist/modules/crm/types.js.map +1 -1
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +352 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +2 -0
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +7 -0
- package/dist/modules/crm_backend/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/index.js +5 -1
- package/dist/modules/crm_backend/index.js.map +1 -1
- package/dist/modules/crm_backend/islands.d.ts +27 -0
- package/dist/modules/crm_backend/islands.d.ts.map +1 -1
- package/dist/modules/crm_backend/islands.js +22 -0
- package/dist/modules/crm_backend/islands.js.map +1 -1
- package/dist/modules/crm_backend/messages.d.ts +30 -0
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +30 -0
- package/dist/modules/crm_backend/messages.js.map +1 -1
- package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts +55 -0
- package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts.map +1 -0
- package/dist/modules/crm_backend/modal/configuration-modal-view.js +601 -0
- package/dist/modules/crm_backend/modal/configuration-modal-view.js.map +1 -0
- package/dist/modules/crm_backend/routes.d.ts.map +1 -1
- package/dist/modules/crm_backend/routes.js +69 -359
- package/dist/modules/crm_backend/routes.js.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.d.ts +24 -32
- package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.js +183 -274
- package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
- package/dist/modules/crm_backend/screens/index.d.ts +2 -2
- package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/index.js +1 -1
- package/dist/modules/crm_backend/screens/index.js.map +1 -1
- package/dist/modules/product/functions.d.ts +14 -1
- package/dist/modules/product/functions.d.ts.map +1 -1
- package/dist/modules/product/functions.js +36 -31
- package/dist/modules/product/functions.js.map +1 -1
- package/dist/modules/product/product-modal-context.d.ts +3 -0
- package/dist/modules/product/product-modal-context.d.ts.map +1 -0
- package/dist/modules/product/product-modal-context.js +173 -0
- package/dist/modules/product/product-modal-context.js.map +1 -0
- package/dist/modules/product/product-record-tabs.d.ts +14 -0
- package/dist/modules/product/product-record-tabs.d.ts.map +1 -0
- package/dist/modules/product/product-record-tabs.js +23 -0
- package/dist/modules/product/product-record-tabs.js.map +1 -0
- package/dist/modules/product_backend/client/product-modal.d.mts +144 -0
- package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -0
- package/dist/modules/product_backend/client/product-modal.mjs +2 -0
- package/dist/modules/product_backend/client/product-modal.mjs.map +7 -0
- package/dist/modules/product_backend/index.d.ts.map +1 -1
- package/dist/modules/product_backend/index.js +25 -0
- package/dist/modules/product_backend/index.js.map +1 -1
- package/dist/modules/product_backend/islands.d.ts +1 -0
- package/dist/modules/product_backend/islands.d.ts.map +1 -1
- package/dist/modules/product_backend/islands.js +9 -0
- package/dist/modules/product_backend/islands.js.map +1 -1
- package/dist/modules/product_backend/modal/product-modal-view.d.ts +49 -0
- package/dist/modules/product_backend/modal/product-modal-view.d.ts.map +1 -0
- package/dist/modules/product_backend/modal/product-modal-view.js +609 -0
- package/dist/modules/product_backend/modal/product-modal-view.js.map +1 -0
- package/dist/modules/product_backend/routes.d.ts.map +1 -1
- package/dist/modules/product_backend/routes.js +44 -330
- package/dist/modules/product_backend/routes.js.map +1 -1
- package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/detail.js +6 -108
- package/dist/modules/product_backend/screens/detail.js.map +1 -1
- package/dist/modules/product_backend/screens/index.d.ts +0 -1
- package/dist/modules/product_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/index.js +0 -1
- package/dist/modules/product_backend/screens/index.js.map +1 -1
- package/dist/modules/product_backend/screens/list.d.ts +3 -1
- package/dist/modules/product_backend/screens/list.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/list.js +6 -5
- package/dist/modules/product_backend/screens/list.js.map +1 -1
- package/dist/modules/user/authorization.d.ts.map +1 -1
- package/dist/modules/user/authorization.js +114 -0
- 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 +2 -0
- package/dist/modules/user/functions.js.map +1 -1
- package/dist/modules/user/user-modal-context.d.ts +3 -0
- package/dist/modules/user/user-modal-context.d.ts.map +1 -0
- package/dist/modules/user/user-modal-context.js +151 -0
- package/dist/modules/user/user-modal-context.js.map +1 -0
- package/dist/modules/user_backend/client/user-modal.d.mts +72 -0
- package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -0
- package/dist/modules/user_backend/client/user-modal.mjs +2 -0
- package/dist/modules/user_backend/client/user-modal.mjs.map +7 -0
- package/dist/modules/user_backend/index.d.ts.map +1 -1
- package/dist/modules/user_backend/index.js +9 -0
- package/dist/modules/user_backend/index.js.map +1 -1
- package/dist/modules/user_backend/islands.d.ts +15 -0
- package/dist/modules/user_backend/islands.d.ts.map +1 -0
- package/dist/modules/user_backend/islands.js +14 -0
- package/dist/modules/user_backend/islands.js.map +1 -0
- package/dist/modules/user_backend/messages.d.ts +30 -0
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +30 -0
- package/dist/modules/user_backend/messages.js.map +1 -1
- package/dist/modules/user_backend/modal/user-modal-view.d.ts +26 -0
- package/dist/modules/user_backend/modal/user-modal-view.d.ts.map +1 -0
- package/dist/modules/user_backend/modal/user-modal-view.js +320 -0
- package/dist/modules/user_backend/modal/user-modal-view.js.map +1 -0
- package/dist/modules/user_backend/routes.d.ts.map +1 -1
- package/dist/modules/user_backend/routes.js +15 -3
- package/dist/modules/user_backend/routes.js.map +1 -1
- package/dist/permission-catalogue.d.ts.map +1 -1
- package/dist/permission-catalogue.js +8 -0
- package/dist/permission-catalogue.js.map +1 -1
- package/dist/scaffold/index.js +1 -1
- package/dist/ui/client/record-modal-form.d.ts +67 -0
- package/dist/ui/client/record-modal-form.d.ts.map +1 -0
- package/dist/ui/client/record-modal-form.js +45 -0
- package/dist/ui/client/record-modal-form.js.map +1 -0
- package/dist/ui/client/record-modal.d.ts +103 -6
- package/dist/ui/client/record-modal.d.ts.map +1 -1
- package/dist/ui/client/record-modal.js +333 -65
- package/dist/ui/client/record-modal.js.map +1 -1
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/nav.d.ts +7 -0
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +1 -1
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/record-modal.d.ts +15 -0
- package/dist/ui/record-modal.d.ts.map +1 -1
- package/dist/ui/record-modal.js +12 -0
- package/dist/ui/record-modal.js.map +1 -1
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +1 -1
- package/dist/ui/table.js.map +1 -1
- package/package.json +4 -4
- package/dist/modules/crm/functions.d.ts +0 -10
- package/dist/modules/crm/functions.d.ts.map +0 -1
- package/dist/modules/crm/functions.js +0 -1394
- package/dist/modules/crm/functions.js.map +0 -1
- package/dist/modules/product_backend/screens/create.d.ts +0 -16
- package/dist/modules/product_backend/screens/create.d.ts.map +0 -1
- package/dist/modules/product_backend/screens/create.js +0 -86
- package/dist/modules/product_backend/screens/create.js.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { NAVIGATION_TYPE, encodeListState, fragment, json, parseListState, table, text, validateListState, withHeaders, } from '@ketvietlab/ketjs';
|
|
3
|
-
import { attributesScreen, favoriteModal,
|
|
4
|
-
import {
|
|
3
|
+
import { attributesScreen, favoriteModal, productDetailScreen, productsScreen, VARIANT_DETAIL_TABS, variantScreen, VIEWS, } from './screens/index.js';
|
|
4
|
+
import { uomControl } from './relation-control.js';
|
|
5
5
|
import { PAGE_SIZE, colsHref, colsOf, pager, withParam } from '../backend/paging.js';
|
|
6
6
|
import { backendPage, modalWorkspace } from '../../ui/index.js';
|
|
7
|
+
import { recordModalCreateHref, recordModalHref } from '../../ui/record-modal.js';
|
|
7
8
|
import { receiveAttachment } from '../storage/routes.js';
|
|
8
9
|
import { errorsOf, readForm, seeOther } from '../backend/forms.js';
|
|
9
10
|
import { productListSearch } from '../product/search.js';
|
|
@@ -34,7 +35,6 @@ const refusePost = (req, accepts = 'POST') => req.method !== 'POST'
|
|
|
34
35
|
: null;
|
|
35
36
|
const productTabOf = (url) => url.searchParams.get('tab') || 'general';
|
|
36
37
|
const MEDIA_VARIANT_PAGE_SIZE = 25;
|
|
37
|
-
const VARIANT_PAGE_SIZE = 10;
|
|
38
38
|
const positivePage = (value) => {
|
|
39
39
|
const page = Number.parseInt(value ?? '1', 10);
|
|
40
40
|
return Number.isFinite(page) && page > 0 ? page : 1;
|
|
@@ -42,7 +42,6 @@ const positivePage = (value) => {
|
|
|
42
42
|
const requestedVariantMediaPage = (url) => {
|
|
43
43
|
return positivePage(url.searchParams.get('variantPage'));
|
|
44
44
|
};
|
|
45
|
-
const requestedVariantPage = (url) => positivePage(url.searchParams.get('page'));
|
|
46
45
|
const variantTabOf = (url) => {
|
|
47
46
|
const asked = url.searchParams.get('tab');
|
|
48
47
|
return VARIANT_DETAIL_TABS.includes(asked ?? '')
|
|
@@ -106,17 +105,6 @@ const unitRootOf = (units, uomId) => {
|
|
|
106
105
|
return unit ? (String(unit.parentPath).split('/').filter(Boolean)[0] ?? null) : null;
|
|
107
106
|
};
|
|
108
107
|
const invalidErrors = (url, _) => url.searchParams.has('invalid') ? [_('product_backend.error.invalid')] : undefined;
|
|
109
|
-
const stockEnabled = async (ctx, req) => Boolean((await ctx.live(req)).functions['stock.configureProduct']);
|
|
110
|
-
const TRACKING = ['none', 'lot', 'serial'];
|
|
111
|
-
const validStockForm = (form) => {
|
|
112
|
-
const tracking = form.tracking || 'none';
|
|
113
|
-
return (TRACKING.includes(tracking) && (form.isStorable === '1' || tracking === 'none'));
|
|
114
|
-
};
|
|
115
|
-
const configureStock = (ctx, url, req, templateId, form) => ctx.call('stock.configureProduct', {
|
|
116
|
-
templateId,
|
|
117
|
-
isStorable: form.isStorable === '1',
|
|
118
|
-
tracking: form.tracking || 'none',
|
|
119
|
-
}, url, req);
|
|
120
108
|
const templateRow = (row) => ({
|
|
121
109
|
id: row.id,
|
|
122
110
|
name: row.name,
|
|
@@ -532,12 +520,12 @@ export const routes = {
|
|
|
532
520
|
title: 'KetSuite',
|
|
533
521
|
translate: false,
|
|
534
522
|
body: (_, frame) => {
|
|
535
|
-
const workspace = productsScreen(_, decoratedRows, view, {
|
|
523
|
+
const workspace = productsScreen(_, decoratedRows, view, (id) => recordModalHref(url, { kind: 'product.template', id }), {
|
|
536
524
|
...frame,
|
|
537
525
|
chrome: {
|
|
538
526
|
create: {
|
|
539
527
|
label: _('product_backend.create.title'),
|
|
540
|
-
path:
|
|
528
|
+
path: recordModalCreateHref(url, { kind: 'product.template' }),
|
|
541
529
|
},
|
|
542
530
|
selection,
|
|
543
531
|
search: {
|
|
@@ -557,7 +545,7 @@ export const routes = {
|
|
|
557
545
|
active: v === view,
|
|
558
546
|
})),
|
|
559
547
|
},
|
|
560
|
-
}, { shown: colsOf(url), colsHref: colsHref(url), groups, selection },
|
|
548
|
+
}, { shown: colsOf(url), colsHref: colsHref(url), groups, selection }, count, extensionActions);
|
|
561
549
|
if (url.searchParams.get('modal') !== 'favorite')
|
|
562
550
|
return workspace;
|
|
563
551
|
const returnUrl = new URL(url);
|
|
@@ -639,59 +627,6 @@ export const routes = {
|
|
|
639
627
|
}
|
|
640
628
|
return seeOther(modalHref());
|
|
641
629
|
},
|
|
642
|
-
'/admin/product/templates/new': (ctx) => async (url, req) => {
|
|
643
|
-
const lang = ctx.localeOf(url, req);
|
|
644
|
-
const _ = ctx.translate(lang);
|
|
645
|
-
const hasStock = await stockEnabled(ctx, req);
|
|
646
|
-
if (req.method === 'POST') {
|
|
647
|
-
if (crossSite(req))
|
|
648
|
-
return text('Forbidden', { status: 403 });
|
|
649
|
-
const form = await readForm(req);
|
|
650
|
-
if (hasStock && !validStockForm(form))
|
|
651
|
-
return seeOther(inLocale(url, '/admin/product/templates/new?invalid=1&count=1'));
|
|
652
|
-
const id = randomUUID();
|
|
653
|
-
const result = await ctx.call('product.saveTemplate', {
|
|
654
|
-
id,
|
|
655
|
-
name: form.name ?? '',
|
|
656
|
-
type: form.type || 'goods',
|
|
657
|
-
// Sent as null rather than omitted: an absent key is skipped by the
|
|
658
|
-
// changeset, so leaving it out would make the form's empty "—" option
|
|
659
|
-
// a no-op and the field impossible to clear once set.
|
|
660
|
-
uomId: form.uomId || null,
|
|
661
|
-
categoryId: form.categoryId || null,
|
|
662
|
-
brandId: form.brandId || null,
|
|
663
|
-
origin: form.origin || null,
|
|
664
|
-
description: form.description || null,
|
|
665
|
-
listPrice: form.listPrice || '0',
|
|
666
|
-
saleOk: form.saleOk === '1',
|
|
667
|
-
purchaseOk: form.purchaseOk === '1',
|
|
668
|
-
...(Object.hasOwn(form, 'defaultCode') ? { defaultCode: form.defaultCode || null } : {}),
|
|
669
|
-
...(Object.hasOwn(form, 'barcode') ? { barcode: form.barcode || null } : {}),
|
|
670
|
-
}, url, req);
|
|
671
|
-
if (!result.ok)
|
|
672
|
-
return seeOther(inLocale(url, `/admin/product/templates/new?invalid=1&count=${errorsOf(result).length}`));
|
|
673
|
-
if (hasStock) {
|
|
674
|
-
const stockResult = await configureStock(ctx, url, req, id, form);
|
|
675
|
-
if (!stockResult.ok)
|
|
676
|
-
return seeOther(inLocale(url, `/admin/product/templates/${id}?invalid=1&count=${errorsOf(stockResult).length}`));
|
|
677
|
-
}
|
|
678
|
-
return seeProduct(id, url);
|
|
679
|
-
}
|
|
680
|
-
if (req.method !== 'GET')
|
|
681
|
-
return text('GET or POST', { status: 405 });
|
|
682
|
-
const options = await optionsFor(ctx, url, req);
|
|
683
|
-
const controls = {
|
|
684
|
-
uom: await uomControl(ctx, url, req, _, { id: 'product-create-uom', units: options.uoms }),
|
|
685
|
-
category: await categoryControl(ctx, url, req, _, {
|
|
686
|
-
id: 'product-create-category',
|
|
687
|
-
categories: options.categories,
|
|
688
|
-
}),
|
|
689
|
-
};
|
|
690
|
-
return adminPage(ctx, url, req, {
|
|
691
|
-
title: 'product_backend.create.title',
|
|
692
|
-
body: (_, frame) => newProductScreen(_, { ...options, stockEnabled: hasStock, errors: invalidErrors(url, _), controls }, frame, localeQuery(url)),
|
|
693
|
-
});
|
|
694
|
-
},
|
|
695
630
|
'/admin/product/attributes': (ctx) => async (url, req) => {
|
|
696
631
|
const lang = ctx.localeOf(url, req);
|
|
697
632
|
const _ = ctx.translate(lang);
|
|
@@ -741,117 +676,33 @@ export const routes = {
|
|
|
741
676
|
: seeOther(inLocale(url, '/admin/product/attributes?invalid=1'));
|
|
742
677
|
},
|
|
743
678
|
'/admin/product/templates/{id}': (ctx) => async (url, req, params) => {
|
|
679
|
+
// General and Variants now open in the record-modal (`product.template`);
|
|
680
|
+
// this route only still renders a page for Media, which the modal does not
|
|
681
|
+
// cover yet (see product_backend/modal/product-modal-view.tsx).
|
|
682
|
+
const askedTab = productTabOf(url);
|
|
683
|
+
if (req.method !== 'GET' || askedTab !== 'media') {
|
|
684
|
+
if (req.method !== 'GET')
|
|
685
|
+
return text('GET', { status: 405 });
|
|
686
|
+
return seeOther(recordModalHref(inLocale(url, '/admin/product/templates'), {
|
|
687
|
+
kind: 'product.template',
|
|
688
|
+
id: params.id,
|
|
689
|
+
tab: askedTab === 'variants' ? 'variants' : 'general',
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
744
692
|
const lang = ctx.localeOf(url, req);
|
|
745
693
|
const _ = ctx.translate(lang);
|
|
746
|
-
const live = await ctx.live(req);
|
|
747
|
-
const hasStock = await stockEnabled(ctx, req);
|
|
748
|
-
const hasProductTax = Boolean(live.functions['account.getProductTax'] && live.functions['account.setProductTax']);
|
|
749
|
-
const activeTab = productTabOf(url);
|
|
750
694
|
const locale = localeQuery(url);
|
|
751
|
-
const querySuffix = locale ? locale.replace(/^\?/, '&') : '';
|
|
752
|
-
let savedPartial = false;
|
|
753
|
-
if (req.method === 'POST') {
|
|
754
|
-
if (crossSite(req))
|
|
755
|
-
return text('Forbidden', { status: 403 });
|
|
756
|
-
const partial = isProductPartial(req);
|
|
757
|
-
const form = await readForm(req);
|
|
758
|
-
if (hasStock && !validStockForm(form)) {
|
|
759
|
-
if (partial)
|
|
760
|
-
return json({ ok: false, message: _('product_backend.error.invalid'), errors: ['tracking'] }, { status: 422 });
|
|
761
|
-
return seeOther(inLocale(url, `/admin/product/templates/${params.id}?invalid=1&count=1`));
|
|
762
|
-
}
|
|
763
|
-
const result = await ctx.call('product.saveTemplate', {
|
|
764
|
-
id: params.id,
|
|
765
|
-
name: form.name ?? '',
|
|
766
|
-
type: form.type || 'goods',
|
|
767
|
-
// Sent as null rather than omitted: an absent key is skipped by the
|
|
768
|
-
// changeset, so leaving it out would make the form's empty "—" option
|
|
769
|
-
// a no-op and the field impossible to clear once set.
|
|
770
|
-
uomId: form.uomId || null,
|
|
771
|
-
categoryId: form.categoryId || null,
|
|
772
|
-
brandId: form.brandId || null,
|
|
773
|
-
origin: form.origin || null,
|
|
774
|
-
description: form.description || null,
|
|
775
|
-
listPrice: form.listPrice || '0',
|
|
776
|
-
saleOk: form.saleOk === '1',
|
|
777
|
-
purchaseOk: form.purchaseOk === '1',
|
|
778
|
-
...(Object.hasOwn(form, 'defaultCode') ? { defaultCode: form.defaultCode || null } : {}),
|
|
779
|
-
...(Object.hasOwn(form, 'barcode') ? { barcode: form.barcode || null } : {}),
|
|
780
|
-
}, url, req);
|
|
781
|
-
if (!result.ok) {
|
|
782
|
-
if (partial)
|
|
783
|
-
return json({ ok: false, message: _('product_backend.error.invalid'), errors: errorsOf(result) }, { status: 422 });
|
|
784
|
-
return seeOther(inLocale(url, `/admin/product/templates/${params.id}?invalid=1&count=${errorsOf(result).length}`));
|
|
785
|
-
}
|
|
786
|
-
if (hasStock) {
|
|
787
|
-
const stockResult = await configureStock(ctx, url, req, params.id, form);
|
|
788
|
-
if (!stockResult.ok) {
|
|
789
|
-
if (partial)
|
|
790
|
-
return json({
|
|
791
|
-
ok: false,
|
|
792
|
-
message: _('product_backend.error.invalid'),
|
|
793
|
-
errors: errorsOf(stockResult),
|
|
794
|
-
}, { status: 422 });
|
|
795
|
-
return seeOther(inLocale(url, `/admin/product/templates/${params.id}?invalid=1&count=${errorsOf(stockResult).length}`));
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (hasProductTax && Object.hasOwn(form, 'taxId')) {
|
|
799
|
-
const taxResult = await ctx.call('account.setProductTax', { templateId: params.id, taxId: form.taxId || null }, url, req);
|
|
800
|
-
if (!taxResult.ok) {
|
|
801
|
-
if (partial)
|
|
802
|
-
return json({
|
|
803
|
-
ok: false,
|
|
804
|
-
message: _('product_backend.error.invalid'),
|
|
805
|
-
errors: errorsOf(taxResult),
|
|
806
|
-
}, { status: 422 });
|
|
807
|
-
return seeOther(inLocale(url, `/admin/product/templates/${params.id}?invalid=1&count=${errorsOf(taxResult).length}`));
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
if (!partial)
|
|
811
|
-
return seeProduct(params.id, url);
|
|
812
|
-
savedPartial = true;
|
|
813
|
-
}
|
|
814
|
-
if (req.method !== 'GET' && !savedPartial)
|
|
815
|
-
return text('GET or POST', { status: 405 });
|
|
816
695
|
const row = (await ctx.call('product.getTemplate', { id: params.id }, url, req));
|
|
817
696
|
if (!row)
|
|
818
697
|
return text('Product not found', { status: 404 });
|
|
819
|
-
const
|
|
820
|
-
mediaFor(ctx, url, req, row.id),
|
|
821
|
-
ctx.call('product.listVariants', { templateId: row.id }, url, req),
|
|
822
|
-
optionsFor(ctx, url, req),
|
|
823
|
-
hasStock
|
|
824
|
-
? ctx.call('stock.getProductConfig', { templateId: row.id }, url, req)
|
|
825
|
-
: Promise.resolve(null),
|
|
826
|
-
ctx.call('product.listAttributeLines', { templateId: row.id }, url, req),
|
|
827
|
-
hasProductTax
|
|
828
|
-
? ctx.call('account.getProductTax', { templateId: row.id }, url, req)
|
|
829
|
-
: Promise.resolve(null),
|
|
830
|
-
]);
|
|
831
|
-
const defaultVariant = (row.variants ?? []).find((variant) => String(variant.combinationKey ?? '') === '');
|
|
698
|
+
const listedVariants = (await ctx.call('product.listVariants', { templateId: row.id }, url, req));
|
|
832
699
|
const variants = listedVariants.filter((variant) => String(variant.combinationKey ?? '') !== '');
|
|
833
|
-
const
|
|
834
|
-
const variantPage = Math.min(requestedVariantPage(url), variantPageCount);
|
|
835
|
-
const variantStart = (variantPage - 1) * VARIANT_PAGE_SIZE;
|
|
836
|
-
const visibleVariants = variants.slice(variantStart, variantStart + VARIANT_PAGE_SIZE);
|
|
837
|
-
const stockByVariant = new Map();
|
|
838
|
-
if (activeTab === 'variants' && hasStock && live.functions['stock.forecast']) {
|
|
839
|
-
const forecasts = await Promise.all(visibleVariants.map(async (variant) => ({
|
|
840
|
-
id: variant.id,
|
|
841
|
-
forecast: (await ctx.call('stock.forecast', { productId: variant.id }, url, req)),
|
|
842
|
-
})));
|
|
843
|
-
for (const entry of forecasts)
|
|
844
|
-
stockByVariant.set(String(entry.id), String(entry.forecast.onHand ?? '0'));
|
|
845
|
-
}
|
|
700
|
+
const [mediaRows] = await Promise.all([mediaFor(ctx, url, req, row.id)]);
|
|
846
701
|
const variantMediaPageCount = Math.max(1, Math.ceil(variants.length / MEDIA_VARIANT_PAGE_SIZE));
|
|
847
702
|
const variantMediaPage = Math.min(requestedVariantMediaPage(url), variantMediaPageCount);
|
|
848
703
|
const variantMediaStart = (variantMediaPage - 1) * MEDIA_VARIANT_PAGE_SIZE;
|
|
849
|
-
const visibleMediaVariants =
|
|
850
|
-
|
|
851
|
-
: variants;
|
|
852
|
-
const variantMediaRows = activeTab === 'media'
|
|
853
|
-
? (await ctx.call('product_media.listMediaByProducts', { productIds: visibleMediaVariants.map((variant) => variant.id) }, url, req))
|
|
854
|
-
: [];
|
|
704
|
+
const visibleMediaVariants = variants.slice(variantMediaStart, variantMediaStart + MEDIA_VARIANT_PAGE_SIZE);
|
|
705
|
+
const variantMediaRows = (await ctx.call('product_media.listMediaByProducts', { productIds: visibleMediaVariants.map((variant) => variant.id) }, url, req));
|
|
855
706
|
const variantMedia = visibleMediaVariants.map((variant) => ({
|
|
856
707
|
variantId: variant.id,
|
|
857
708
|
images: variantMediaRows
|
|
@@ -866,22 +717,14 @@ export const routes = {
|
|
|
866
717
|
},
|
|
867
718
|
})),
|
|
868
719
|
}));
|
|
869
|
-
const body = productDetailScreen(_, {
|
|
870
|
-
...row,
|
|
871
|
-
...stockConfig,
|
|
872
|
-
defaultCode: defaultVariant?.defaultCode ?? null,
|
|
873
|
-
barcode: defaultVariant?.barcode ?? null,
|
|
874
|
-
taxId: currentTax?.taxId ?? null,
|
|
875
|
-
}, {
|
|
720
|
+
const body = productDetailScreen(_, { ...row, defaultCode: null, barcode: null, taxId: null }, {
|
|
876
721
|
status: 'ready',
|
|
877
722
|
uploadAction: inLocale(url, `/admin/product/templates/${row.id}/media?tab=media`),
|
|
878
|
-
uploadControl:
|
|
879
|
-
|
|
880
|
-
:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
label: _('product_backend.media.add'),
|
|
884
|
-
}),
|
|
723
|
+
uploadControl: await ctx.joint(url, req, 'product_backend:media.upload', {
|
|
724
|
+
identity: `template:${row.id}`,
|
|
725
|
+
action: inLocale(url, `/admin/product/templates/${row.id}/media?tab=media`),
|
|
726
|
+
label: _('product_backend.media.add'),
|
|
727
|
+
}),
|
|
885
728
|
images: mediaRows.map((image, index) => ({
|
|
886
729
|
id: image.id,
|
|
887
730
|
src: `/files/${image.attachmentId}`,
|
|
@@ -902,158 +745,29 @@ export const routes = {
|
|
|
902
745
|
: {}),
|
|
903
746
|
},
|
|
904
747
|
})),
|
|
905
|
-
extension:
|
|
906
|
-
? ''
|
|
907
|
-
: await ctx.joint(url, req, 'product_backend:template.media', {
|
|
908
|
-
templateId: row.id,
|
|
909
|
-
}),
|
|
748
|
+
extension: await ctx.joint(url, req, 'product_backend:template.media', { templateId: row.id }),
|
|
910
749
|
}, {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
: {}),
|
|
921
|
-
})),
|
|
922
|
-
variantPage: {
|
|
923
|
-
page: variantPage,
|
|
924
|
-
pageSize: VARIANT_PAGE_SIZE,
|
|
925
|
-
total: variants.length,
|
|
926
|
-
},
|
|
750
|
+
uoms: [],
|
|
751
|
+
categories: [],
|
|
752
|
+
brands: [],
|
|
753
|
+
taxes: [],
|
|
754
|
+
variantAttributes: [],
|
|
755
|
+
// The media panel renders exactly the `variants` it is given — the
|
|
756
|
+
// caller does the paging, unlike the variants tab's own table.
|
|
757
|
+
variants: visibleMediaVariants,
|
|
758
|
+
attributeLines: [],
|
|
927
759
|
variantMedia,
|
|
928
760
|
variantMediaPage: {
|
|
929
761
|
page: variantMediaPage,
|
|
930
762
|
pageSize: MEDIA_VARIANT_PAGE_SIZE,
|
|
931
763
|
total: variants.length,
|
|
932
764
|
},
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
actions: savedPartial
|
|
937
|
-
? ''
|
|
938
|
-
: await ctx.joint(url, req, 'product_backend:template.actions', {
|
|
939
|
-
templateId: row.id,
|
|
940
|
-
locale,
|
|
941
|
-
}),
|
|
942
|
-
tabs: savedPartial
|
|
943
|
-
? ''
|
|
944
|
-
: await ctx.joint(url, req, 'product_backend:template.tabs', {
|
|
945
|
-
templateId: row.id,
|
|
946
|
-
activeTab,
|
|
947
|
-
locale,
|
|
948
|
-
querySuffix,
|
|
949
|
-
}),
|
|
950
|
-
panel: savedPartial
|
|
951
|
-
? ''
|
|
952
|
-
: await ctx.joint(url, req, 'product_backend:template.panel', {
|
|
953
|
-
templateId: row.id,
|
|
954
|
-
activeTab,
|
|
955
|
-
locale,
|
|
956
|
-
querySuffix,
|
|
957
|
-
}),
|
|
958
|
-
controls: {
|
|
959
|
-
uom: await uomControl(ctx, url, req, _, {
|
|
960
|
-
id: `product-uom:${row.id}`,
|
|
961
|
-
value: row.uomId,
|
|
962
|
-
units: options.uoms,
|
|
963
|
-
}),
|
|
964
|
-
category: await categoryControl(ctx, url, req, _, {
|
|
965
|
-
id: `product-category:${row.id}`,
|
|
966
|
-
value: row.categoryId,
|
|
967
|
-
categories: options.categories,
|
|
968
|
-
}),
|
|
969
|
-
brand: await brandControl(ctx, url, req, _, {
|
|
970
|
-
id: `product-brand:${row.id}`,
|
|
971
|
-
value: row.brandId,
|
|
972
|
-
brands: options.brands,
|
|
973
|
-
}),
|
|
974
|
-
attribute: await attributeControl(ctx, url, req, _, {
|
|
975
|
-
id: `product-attribute:${row.id}`,
|
|
976
|
-
attributes: options.variantAttributes,
|
|
977
|
-
required: true,
|
|
978
|
-
}),
|
|
979
|
-
// The value picker cannot be scoped to an attribute yet — the two are
|
|
980
|
-
// separate fields and the attribute is only known once chosen — so it
|
|
981
|
-
// lists every value, each labelled with the attribute it belongs to.
|
|
982
|
-
attributeValues: await attributeValuesControl(ctx, url, req, _, {
|
|
983
|
-
id: `product-attribute-values:${row.id}`,
|
|
984
|
-
choices: options.attributeValues,
|
|
985
|
-
required: true,
|
|
986
|
-
}),
|
|
987
|
-
},
|
|
988
|
-
editor: savedPartial
|
|
989
|
-
? ''
|
|
990
|
-
: await ctx.joint(url, req, 'product_backend:template.editor', {
|
|
991
|
-
identity: `template:${row.id}`,
|
|
992
|
-
templateId: row.id,
|
|
993
|
-
lang,
|
|
994
|
-
}),
|
|
995
|
-
}, savedPartial
|
|
996
|
-
? ''
|
|
997
|
-
: await ctx.joint(url, req, 'product_backend:template.collaboration', {
|
|
998
|
-
resModel: 'product.Template',
|
|
999
|
-
resId: row.id,
|
|
1000
|
-
lang,
|
|
1001
|
-
}), savedPartial ? {} : await frameOf(ctx, url, req), locale, activeTab, savedPartial);
|
|
1002
|
-
if (savedPartial)
|
|
1003
|
-
return withHeaders(fragment(body, { type: NAVIGATION_TYPE }), { vary: 'X-Ket-Partial' });
|
|
1004
|
-
return backendPage(ctx, req, {
|
|
765
|
+
}, await ctx.joint(url, req, 'product_backend:template.collaboration', {
|
|
766
|
+
resModel: 'product.Template',
|
|
767
|
+
resId: row.id,
|
|
1005
768
|
lang,
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
});
|
|
1009
|
-
},
|
|
1010
|
-
'/admin/product/templates/{id}/variants/generate': (ctx) => async (url, req, params) => {
|
|
1011
|
-
const denied = refusePost(req);
|
|
1012
|
-
if (denied)
|
|
1013
|
-
return denied;
|
|
1014
|
-
const result = await ctx.call('product.generateVariants', { templateId: params.id }, url, req);
|
|
1015
|
-
return result.ok
|
|
1016
|
-
? seeProduct(params.id, url)
|
|
1017
|
-
: seeOther(inLocale(url, `/admin/product/templates/${params.id}?invalid=1`));
|
|
1018
|
-
},
|
|
1019
|
-
'/admin/product/templates/{id}/attribute-lines': (ctx) => async (url, req, params) => {
|
|
1020
|
-
const denied = refusePost(req);
|
|
1021
|
-
if (denied)
|
|
1022
|
-
return denied;
|
|
1023
|
-
const form = await readForm(req);
|
|
1024
|
-
const attributeId = form.attributeId ?? '';
|
|
1025
|
-
const result = await ctx.call('product.saveAttributeLine', {
|
|
1026
|
-
id: `${params.id}:${attributeId}`,
|
|
1027
|
-
templateId: params.id,
|
|
1028
|
-
attributeId,
|
|
1029
|
-
valueIds: (form.valueIds ?? '')
|
|
1030
|
-
.split(',')
|
|
1031
|
-
.map((value) => value.trim())
|
|
1032
|
-
.filter(Boolean),
|
|
1033
|
-
}, url, req);
|
|
1034
|
-
return result.ok
|
|
1035
|
-
? seeProduct(params.id, url)
|
|
1036
|
-
: seeOther(inLocale(url, `/admin/product/templates/${params.id}?invalid=1`));
|
|
1037
|
-
},
|
|
1038
|
-
'/admin/product/templates/{id}/archive': (ctx) => async (url, req, params) => {
|
|
1039
|
-
const denied = refusePost(req);
|
|
1040
|
-
if (denied)
|
|
1041
|
-
return denied;
|
|
1042
|
-
const form = await readForm(req);
|
|
1043
|
-
await ctx.call('product.archiveTemplate', { id: params.id, active: form.active === '1' }, url, req);
|
|
1044
|
-
return seeProduct(params.id, url);
|
|
1045
|
-
},
|
|
1046
|
-
'/admin/product/templates/{id}/attribute-lines/{lineId}/remove': (ctx) => async (url, req, params) => {
|
|
1047
|
-
const denied = refusePost(req);
|
|
1048
|
-
if (denied)
|
|
1049
|
-
return denied;
|
|
1050
|
-
// The line has to belong to the template in the path, or a POST could take
|
|
1051
|
-
// an attribute off a product the reader never opened.
|
|
1052
|
-
const lines = (await ctx.call('product.listAttributeLines', { templateId: params.id }, url, req));
|
|
1053
|
-
if (!lines.some((line) => line.id === params.lineId))
|
|
1054
|
-
return text('Attribute line not found', { status: 404 });
|
|
1055
|
-
await ctx.call('product.removeAttributeLine', { id: params.lineId }, url, req);
|
|
1056
|
-
return seeProduct(params.id, url);
|
|
769
|
+
}), await frameOf(ctx, url, req), locale, 'media', false);
|
|
770
|
+
return backendPage(ctx, req, { lang, title: row.name, body });
|
|
1057
771
|
},
|
|
1058
772
|
'/admin/product/templates/{id}/variants/{variantId}': (ctx) => async (url, req, params) => {
|
|
1059
773
|
const lang = ctx.localeOf(url, req);
|