diff --git a/.eslintrc.js b/.eslintrc.js
index 636a1ef..6f3adff 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -46,5 +46,11 @@ module.exports = {
files: ['tests/**/*-test.{js,ts}'],
extends: ['plugin:qunit/recommended'],
},
+ {
+ files: ['**/*.gjs'],
+ parser: 'ember-eslint-parser',
+ plugins: ['ember'],
+ extends: ['eslint:recommended', 'plugin:ember/recommended', 'plugin:ember/recommended-gjs'],
+ },
],
};
diff --git a/.prettierrc.js b/.prettierrc.js
index 12f341f..c8fcf65 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -1,9 +1,10 @@
'use strict';
module.exports = {
+ plugins: ['prettier-plugin-ember-template-tag'],
overrides: [
{
- files: '*.{js,ts}',
+ files: '*.{js,ts,gjs,gts}',
options: {
singleQuote: true,
printWidth: 100,
diff --git a/app/components/fmt/unit-code.js b/app/components/fmt/unit-code.js
index 7b54778..34d6b65 100644
--- a/app/components/fmt/unit-code.js
+++ b/app/components/fmt/unit-code.js
@@ -1,15 +1,23 @@
import Component from '@glimmer/component';
export default class FmtUnitCodeComponent extends Component {
+ get label() {
+ if (typeof this.args.value === 'string') {
+ return this.args.value;
+ } else {
+ // ED unitPrice object
+ return this.args.value?.get('label');
+ }
+ }
get htmlLabel() {
- if (this.args.value == 'm1') {
+ if (this.label == 'm1') {
return 'm';
- } else if (this.args.value == 'm2') {
+ } else if (this.label == 'm2') {
return 'm2';
- } else if (this.args.value == 'st') {
+ } else if (this.alabel == 'st') {
return 'stuk';
} else {
- return this.args.value;
+ return this.label;
}
}
}
diff --git a/app/components/fmt/unit-price.hbs b/app/components/fmt/unit-price.hbs
index 12fc2af..4f9b49b 100644
--- a/app/components/fmt/unit-price.hbs
+++ b/app/components/fmt/unit-price.hbs
@@ -1,6 +1,4 @@
-{{#if this.currencyValueLoader.isResolved}}
-
- {{#if @showUnit}}/ {{/if}}
-{{/if}}
\ No newline at end of file
+
+{{#if @showUnit}}/ {{/if}}
\ No newline at end of file
diff --git a/app/components/fmt/unit-price.js b/app/components/fmt/unit-price.js
index d7ec592..eeef3e9 100644
--- a/app/components/fmt/unit-price.js
+++ b/app/components/fmt/unit-price.js
@@ -1,36 +1,27 @@
import Component from '@glimmer/component';
-import { cached } from '@glimmer/tracking';
-import { TrackedAsyncData } from 'ember-async-data';
import { VAT_RATE } from '../../config';
import { calculatePriceTaxIncluded, calculatePriceTaxExcluded } from '../../utils/calculate-price';
-
+import { get } from '@ember/object';
export default class FmtUnitPriceComponent extends Component {
// Note:
- // this.args.model maybe a Proxy object coming from ember-data
+ // this.args.model may be a unit-price-specification Proxy object coming from ember-data
// or a plain javascript object coming as nested object from mu-search.
- @cached
- get currencyValueLoader() {
+ get currencyValue() {
+ // eslint-disable-next-line ember/no-get
+ return get(this.args.model, 'currencyValue');
+ }
+
+ get isTaxIncluded() {
+ // eslint-disable-next-line ember/no-get
+ return `${get(this.args.model, 'valueAddedTaxIncluded')}` === 'true'; // cover for mu-search where 'true' is a string
+ }
+
+ get calculatedCurrencyValue() {
if (this.args.showTaxIncluded) {
- const loadData = async () => {
- const model = await this.args.model;
- return calculatePriceTaxIncluded(
- model.currencyValue,
- VAT_RATE,
- `${model.valueAddedTaxIncluded}` === 'true', // cover for mu-search where 'true' is a string
- );
- };
- return new TrackedAsyncData(loadData());
+ return calculatePriceTaxIncluded(this.currencyValue, VAT_RATE, this.isTaxIncluded);
} else {
- const loadData = async () => {
- const model = await this.args.model;
- return calculatePriceTaxExcluded(
- model.currencyValue,
- VAT_RATE,
- `${model.valueAddedTaxIncluded}` === 'true', // cover for mu-search where 'true' is a string
- );
- };
- return new TrackedAsyncData(loadData());
+ return calculatePriceTaxExcluded(this.currencyValue, VAT_RATE, this.isTaxIncluded);
}
}
}
diff --git a/app/components/main-menu.hbs b/app/components/main-menu.hbs
index 40d5887..88fd595 100644
--- a/app/components/main-menu.hbs
+++ b/app/components/main-menu.hbs
@@ -12,11 +12,18 @@
-
Producten
+ {{#if this.canEditPrice}}
+
+ Prijzenlijst
+
+ {{/if}}
diff --git a/app/components/main-menu.js b/app/components/main-menu.js
index 7ad076b..4d62ecc 100644
--- a/app/components/main-menu.js
+++ b/app/components/main-menu.js
@@ -8,6 +8,9 @@ export default class MainMenuComponent extends Component {
@tracked isOpenMenu = false;
+ get canEditPrice() {
+ return this.userInfo.isPriceAdmin;
+ }
@action
toggleIsOpenMenu() {
this.isOpenMenu = !this.isOpenMenu;
diff --git a/app/components/product/category-tree.hbs b/app/components/product/category-tree.hbs
new file mode 100644
index 0000000..4372f1a
--- /dev/null
+++ b/app/components/product/category-tree.hbs
@@ -0,0 +1,7 @@
+{{#let (or @product.broaderCategory.label @product.category.broader.label) as |broaderCategoryLabel| }}
+ {{#if broaderCategoryLabel}}
+ {{broaderCategoryLabel}}
+
+ {{/if}}
+{{/let}}
+{{@product.category.label}}
\ No newline at end of file
diff --git a/app/components/product/edit.js b/app/components/product/edit.js
index 19579d8..9101163 100644
--- a/app/components/product/edit.js
+++ b/app/components/product/edit.js
@@ -3,10 +3,9 @@ import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { enqueueTask, keepLatestTask, task, timeout } from 'ember-concurrency';
-import roundDecimal from '../../utils/round-decimal';
import constants from '../../config/constants';
-import { VAT_RATE } from '../../config';
import { without } from 'frontend-price-management/utils/array';
+import { recalculateSalesPrice } from '../../utils/product-price';
const { CALCULATION_BASIS } = constants;
@@ -127,28 +126,8 @@ export default class ProductEditComponent extends Component {
}
});
- recalculateSalesPrice = keepLatestTask(async () => {
- const purchaseOffering = await this.args.model.purchaseOffering;
- const purchasePrice = await purchaseOffering.unitPriceSpecification;
- purchasePrice.currencyValue = roundDecimal(purchasePrice.currencyValue);
-
- const salesOffering = await this.args.model.salesOffering;
- const salesPrice = await salesOffering.unitPriceSpecification;
-
- if (salesPrice.calculationBasis == CALCULATION_BASIS.MARGIN) {
- salesPrice.margin = roundDecimal(salesPrice.margin);
- if (salesPrice.valueAddedTaxIncluded) {
- const value = purchasePrice.currencyValue * salesPrice.margin * (1 + VAT_RATE);
- salesPrice.currencyValue = roundDecimal(value);
- } else {
- const value = purchasePrice.currencyValue * salesPrice.margin;
- salesPrice.currencyValue = roundDecimal(value);
- }
- } else {
- salesPrice.currencyValue = roundDecimal(salesPrice.currencyValue);
- const margin = salesPrice.currencyValueTaxExcluded / purchasePrice.currencyValue;
- salesPrice.margin = roundDecimal(margin);
- }
+ recalculateProductSalesPrice = keepLatestTask(async () => {
+ await recalculateSalesPrice(this.args.model);
});
uploadFile = enqueueTask(async (file) => {
@@ -220,7 +199,7 @@ export default class ProductEditComponent extends Component {
const offering = await this.args.model.purchaseOffering;
const price = await offering.unitPriceSpecification;
price.currencyValue = value;
- this.recalculateSalesPrice.perform();
+ this.recalculateProductSalesPrice.perform();
}
@action
@@ -235,7 +214,7 @@ export default class ProductEditComponent extends Component {
const offering = await this.args.model.salesOffering;
const price = await offering.unitPriceSpecification;
price.currencyValue = value;
- this.recalculateSalesPrice.perform();
+ this.recalculateProductSalesPrice.perform();
}
@action
@@ -250,6 +229,6 @@ export default class ProductEditComponent extends Component {
const offering = await this.args.model.salesOffering;
const price = await offering.unitPriceSpecification;
price.margin = value;
- this.recalculateSalesPrice.perform();
+ this.recalculateProductSalesPrice.perform();
}
}
diff --git a/app/components/product/inline-price-edit.gjs b/app/components/product/inline-price-edit.gjs
new file mode 100644
index 0000000..76e00a2
--- /dev/null
+++ b/app/components/product/inline-price-edit.gjs
@@ -0,0 +1,125 @@
+import pick from 'frontend-price-management/helpers/pick';
+import constant from 'frontend-price-management/helpers/constant';
+import Component from '@glimmer/component';
+import { not } from 'ember-truth-helpers';
+import { on } from '@ember/modifier';
+import { fn, uniqueId, hash } from '@ember/helper';
+import DecimalInput from '../rlv/input-field/decimal-input';
+import UnitPrice from '../fmt/unit-price';
+import Property from '../util/property';
+import { hasMarginCalculationBasis, hasPriceOutCalculationBasis } from '../../utils/product-price';
+import { enqueueTask } from 'ember-concurrency';
+import TaskState from '../util/task-state';
+
+const Margin =
+
+;
+
+const PurchasePrice =
+
+;
+
+const SellingPrice =
+
+;
+export default class ProductInlinePriceEditComponent extends Component {
+ adjustPrice = enqueueTask(async (callback, product, value) => {
+ await callback(product, value);
+ });
+
+
+ {{#let (uniqueId) as |uuid|}}
+ {{yield
+ (hash
+ PurchasePrice=(component
+ PurchasePrice product=@product setPriceIn=(fn this.adjustPrice.perform @setPriceIn)
+ )
+ Margin=(component
+ Margin
+ product=@product
+ uuid=uuid
+ setMargin=(fn this.adjustPrice.perform @setMargin)
+ setCalculationBasis=(fn this.adjustPrice.perform @setCalculationBasis)
+ active=(hasMarginCalculationBasis @product.salesOffering.unitPriceSpecification)
+ )
+ SellingPrice=(component
+ SellingPrice
+ product=@product
+ uuid=uuid
+ setPriceOut=(fn this.adjustPrice.perform @setPriceOut)
+ setCalculationBasis=(fn this.adjustPrice.perform @setCalculationBasis)
+ active=(hasPriceOutCalculationBasis @product.salesOffering.unitPriceSpecification)
+ )
+ SavingStateIcon=(component
+ TaskState state=this.adjustPrice labelSuccess='Saved' labelRunning='Saving'
+ )
+ )
+ }}
+ {{/let}}
+
+}
diff --git a/app/components/util/task-state.hbs b/app/components/util/task-state.hbs
new file mode 100644
index 0000000..d6ed112
--- /dev/null
+++ b/app/components/util/task-state.hbs
@@ -0,0 +1,13 @@
+{{#if @state.isRunning}}
+
+
+
+{{else if @state.last.isSuccessful}}
+
+ {{svg-jar "check-fill" class="text-green-600"}}
+
+{{else if @state.last.isError}}
+
+ {{svg-jar "error-warning-line" class="h-6 w-6 text-red-600"}}
+
+{{/if}}
\ No newline at end of file
diff --git a/app/controllers/main/products/index.js b/app/controllers/main/products/list.js
similarity index 90%
rename from app/controllers/main/products/index.js
rename to app/controllers/main/products/list.js
index 8a5f6bb..54152e5 100644
--- a/app/controllers/main/products/index.js
+++ b/app/controllers/main/products/list.js
@@ -3,8 +3,12 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { isBlank } from '@ember/utils';
import { restartableTask, timeout } from 'ember-concurrency';
+import { service } from '@ember/service';
export default class MainProductsIndexController extends Controller {
+ @service store;
+ @service userInfo;
+
@tracked page = 0;
@tracked size = 20;
@tracked sort = 'identifier';
@@ -20,8 +24,6 @@ export default class MainProductsIndexController extends Controller {
@tracked rack;
@tracked availableOnly = true;
- @tracked previewProduct;
-
debounceFilter = restartableTask(async (key, event) => {
const value = event.target.value;
this.filter[key] = isBlank(value) ? undefined : value;
@@ -53,16 +55,6 @@ export default class MainProductsIndexController extends Controller {
}
}
- @action
- showPreview(product) {
- this.previewProduct = product;
- }
-
- @action
- closePreview() {
- this.previewProduct = undefined;
- }
-
@action
toggleAvailableOnly() {
this.availableOnly = !this.availableOnly;
diff --git a/app/controllers/main/products/list/index.js b/app/controllers/main/products/list/index.js
new file mode 100644
index 0000000..f4636b7
--- /dev/null
+++ b/app/controllers/main/products/list/index.js
@@ -0,0 +1,17 @@
+import Controller from '@ember/controller';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+export default class MainProductsListIndexController extends Controller {
+
+ @tracked previewProduct;
+
+ @action
+ showPreview(product) {
+ this.previewProduct = product;
+ }
+
+ @action
+ closePreview() {
+ this.previewProduct = undefined;
+ }
+}
diff --git a/app/controllers/main/products/list/prices.js b/app/controllers/main/products/list/prices.js
new file mode 100644
index 0000000..5750094
--- /dev/null
+++ b/app/controllers/main/products/list/prices.js
@@ -0,0 +1,70 @@
+import Controller from '@ember/controller';
+import { action } from '@ember/object';
+import { service } from '@ember/service';
+import { recalculateSalesPriceAndSave } from '../../../../utils/product-price';
+import { trackedFunction } from 'reactiveweb/function';
+
+export default class MainProductsListPricesController extends Controller {
+ @service store;
+
+ products = trackedFunction(this, async () => {
+ const productObjects = await this.model;
+ await Promise.resolve();
+ const productsED = await this.store.query('product', {
+ filter: { ':id:': productObjects.map((p) => p.id).join(',') },
+ include: [
+ 'category',
+ 'category.broader',
+ 'purchase-offering.unit-price-specification',
+ 'purchase-offering.business-entity',
+ 'sales-offering.unit-price-specification',
+ 'sales-offering.unit-price-specification.unit-code',
+ ].join(','),
+ });
+ return productObjects.map((p) => productsED.find((pED) => p.id == pED.id));
+ });
+
+ // the edit actions might receive ES object or ED
+ // But editing/saving is only possible via ED, so always try to fetch ED object.
+ async getProductById(productId) {
+ await this.products.promise;
+ return this.products.value.find((p) => p.id == productId);
+ }
+
+ @action
+ async setCalculationBasis(product, calculationBasis) {
+ const productED = await this.getProductById(product.id);
+ const offering = await productED.salesOffering;
+ const price = await offering.unitPriceSpecification;
+ price.calculationBasis = calculationBasis;
+ await price.save();
+ }
+
+ @action
+ async setPriceIn(product, value) {
+ const productED = await this.getProductById(product.id);
+ const offering = await productED.purchaseOffering;
+ const price = await offering.unitPriceSpecification;
+ price.currencyValue = value;
+ await price.save();
+ await recalculateSalesPriceAndSave(productED);
+ }
+
+ @action
+ async setPriceOut(product, value) {
+ const productED = await this.getProductById(product.id);
+ const offering = await productED.salesOffering;
+ const price = await offering.unitPriceSpecification;
+ price.currencyValue = value;
+ await recalculateSalesPriceAndSave(productED);
+ }
+
+ @action
+ async setMargin(product, value) {
+ const productED = await this.getProductById(product.id);
+ const offering = await productED.salesOffering;
+ const price = await offering.unitPriceSpecification;
+ price.margin = value;
+ await recalculateSalesPriceAndSave(productED);
+ }
+}
diff --git a/app/helpers/constant.js b/app/helpers/constant.js
new file mode 100644
index 0000000..ea42971
--- /dev/null
+++ b/app/helpers/constant.js
@@ -0,0 +1,6 @@
+import constants from '../config/constants';
+
+export default function constant(constantName) {
+ const path = constantName.split('.');
+ return path.reduce((acc, pathKey) => acc[pathKey], constants);
+}
diff --git a/app/models/unit-price-specification.js b/app/models/unit-price-specification.js
index cd06329..e1b4c12 100644
--- a/app/models/unit-price-specification.js
+++ b/app/models/unit-price-specification.js
@@ -1,10 +1,8 @@
import { attr, belongsTo } from '@ember-data/model';
import ProvenanceModel from './provenance-model';
import { calculatePriceTaxIncluded, calculatePriceTaxExcluded } from '../utils/calculate-price';
-import constants from '../config/constants';
import { VAT_RATE } from '../config';
-
-const { CALCULATION_BASIS } = constants;
+import { hasMarginCalculationBasis, hasPriceOutCalculationBasis } from '../utils/product-price';
export default class UnitPriceSpecificationModel extends ProvenanceModel {
@attr('string') currency;
@@ -25,11 +23,11 @@ export default class UnitPriceSpecificationModel extends ProvenanceModel {
}
get hasPriceOutCalculationBasis() {
- return this.calculationBasis == CALCULATION_BASIS.PRICE_OUT;
+ return hasPriceOutCalculationBasis(this);
}
get hasMarginCalculationBasis() {
- return this.calculationBasis == CALCULATION_BASIS.MARGIN;
+ return hasMarginCalculationBasis(this);
}
get marginPct() {
diff --git a/app/router.js b/app/router.js
index 83903dd..3a6e7b1 100644
--- a/app/router.js
+++ b/app/router.js
@@ -12,6 +12,9 @@ Router.map(function () {
this.route('oops');
this.route('main', { path: '/' }, function () {
this.route('products', function () {
+ this.route('list', function () {
+ this.route('prices');
+ });
this.route('new');
this.route('detail', { path: '/:product_id' }, function () {
this.route('edit');
diff --git a/app/routes/login.js b/app/routes/login.js
index e9bb7a9..8e77020 100644
--- a/app/routes/login.js
+++ b/app/routes/login.js
@@ -5,6 +5,6 @@ export default class LoginRoute extends Route {
@service session;
beforeModel() {
- this.session.prohibitAuthentication('main.products.index');
+ this.session.prohibitAuthentication('main.products.list');
}
}
diff --git a/app/routes/main/index.js b/app/routes/main/index.js
index c6f78a8..e76909b 100644
--- a/app/routes/main/index.js
+++ b/app/routes/main/index.js
@@ -5,6 +5,6 @@ export default class MainIndexRoute extends Route {
@service router;
beforeModel() {
- this.router.transitionTo('main.products.index');
+ this.router.transitionTo('main.products.list');
}
}
diff --git a/app/routes/main/products/index.js b/app/routes/main/products/index.js
index da08650..f16adc0 100644
--- a/app/routes/main/products/index.js
+++ b/app/routes/main/products/index.js
@@ -1,141 +1,10 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
-import { action } from '@ember/object';
-import { isPresent } from '@ember/utils';
-import search, { getWildcardFilterValue, langStringResourceFormat } from '../../../utils/mu-search';
-import Snapshot from '../../../utils/snapshot';
-import { copy } from 'ember-copy';
-import ProductFilter from '../../../models/product-filter';
+export default class MainProductsRoute extends Route {
+ @service router;
-export default class MainProductsIndexRoute extends Route {
- @service store;
-
- queryParams = {
- page: {
- refreshModel: true,
- },
- size: {
- refreshModel: true,
- },
- sort: {
- refreshModel: true,
- },
- name: {
- refreshModel: true,
- },
- category: {
- refreshModel: true,
- },
- broaderCategory: {
- refreshModel: true,
- },
- identifier: {
- refreshModel: true,
- },
- supplier: {
- refreshModel: true,
- },
- supplierIdentifier: {
- refreshModel: true,
- },
- rack: {
- refreshModel: true,
- },
- availableOnly: {
- refreshModel: true,
- },
- };
-
- constructor() {
- super(...arguments);
- this.lastParams = new Snapshot();
- }
-
- async model(params) {
- this.lastParams.stageLive(params);
-
- if (this.lastParams.anyFieldChanged(Object.keys(params).filter((key) => key !== 'page'))) {
- params.page = 0;
- }
-
- const { supplier, category, broaderCategory, availableOnly } = params;
- this.supplier = supplier ? await this.store.findRecordByUri('business-entity', supplier) : null;
- this.category = category
- ? await this.store.findRecordByUri('product-category', category)
- : null;
- this.broaderCategory = broaderCategory
- ? await this.store.findRecordByUri('product-category', broaderCategory)
- : null;
- this.availableOnly = availableOnly;
-
- const filter = {};
-
- filter[':wildcard:searchName'] = getWildcardFilterValue(params.name);
- if (isPresent(params.identifier)) {
- filter['identifier'] = params.identifier;
- }
- filter['category.uuid'] = this.category?.id;
- filter['broaderCategory.uuid'] = this.broaderCategory?.id;
- filter['purchaseOffering.businessEntity.uuid'] = this.supplier?.id;
- filter[':wildcard:purchaseOffering.identifier'] = getWildcardFilterValue(
- params.supplierIdentifier
- );
- filter[':wildcard:warehouseLocation.rack'] = getWildcardFilterValue(params.rack);
-
- if (params.availableOnly) {
- // No validThrough dates in the future assumed.
- // (has-no || gt now) would require custom elastic query
- filter[':has-no:salesOffering.validThrough'] = 't';
- }
-
- this.lastParams.commit();
-
- return search('products', params.page, params.size, params.sort, filter, (product) => {
- const entry = product.attributes;
- entry.id = product.id;
- // create record for convenience of the isValid getter
- // final response stays a pojo
- const offering = this.store.createRecord('offering', {
- validFrom: entry.salesOffering.validFrom,
- validThrough: entry.salesOffering.validThrough,
- });
- entry.salesOffering.isValid = offering.isValid;
- offering.destroyRecord();
-
- entry.name = langStringResourceFormat(entry.name);
- entry.alternateNames = langStringResourceFormat(entry.alternateNames);
-
- return entry;
- });
- }
-
- setupController(controller) {
- super.setupController(...arguments);
-
- const filter = copy(this.lastParams.committed);
- filter.supplier = this.supplier;
- filter.category = this.category;
- filter.broaderCategory = this.broaderCategory;
-
- filter.availableOnly = this.availableOnly;
- if (!controller.filter) {
- controller.filter = new ProductFilter(filter);
- }
-
- controller.page = this.lastParams.committed.page;
- controller.size = this.lastParams.committed.size;
- controller.sort = this.lastParams.committed.sort;
- }
-
- @action
- loading(transition) {
- // eslint-disable-next-line ember/no-controller-access-in-routes
- const controller = this.controllerFor(this.routeName);
- controller.isLoadingModel = true;
- transition.promise.finally(function () {
- controller.isLoadingModel = false;
- });
-
- return true; // bubble the loading event
+ beforeModel() {
+ // index route does not exist, the `.list` route is the main route for the user.
+ this.router.transitionTo('main.products.list');
}
}
diff --git a/app/routes/main/products/list.js b/app/routes/main/products/list.js
new file mode 100644
index 0000000..da08650
--- /dev/null
+++ b/app/routes/main/products/list.js
@@ -0,0 +1,141 @@
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+import { action } from '@ember/object';
+import { isPresent } from '@ember/utils';
+import search, { getWildcardFilterValue, langStringResourceFormat } from '../../../utils/mu-search';
+import Snapshot from '../../../utils/snapshot';
+import { copy } from 'ember-copy';
+import ProductFilter from '../../../models/product-filter';
+
+export default class MainProductsIndexRoute extends Route {
+ @service store;
+
+ queryParams = {
+ page: {
+ refreshModel: true,
+ },
+ size: {
+ refreshModel: true,
+ },
+ sort: {
+ refreshModel: true,
+ },
+ name: {
+ refreshModel: true,
+ },
+ category: {
+ refreshModel: true,
+ },
+ broaderCategory: {
+ refreshModel: true,
+ },
+ identifier: {
+ refreshModel: true,
+ },
+ supplier: {
+ refreshModel: true,
+ },
+ supplierIdentifier: {
+ refreshModel: true,
+ },
+ rack: {
+ refreshModel: true,
+ },
+ availableOnly: {
+ refreshModel: true,
+ },
+ };
+
+ constructor() {
+ super(...arguments);
+ this.lastParams = new Snapshot();
+ }
+
+ async model(params) {
+ this.lastParams.stageLive(params);
+
+ if (this.lastParams.anyFieldChanged(Object.keys(params).filter((key) => key !== 'page'))) {
+ params.page = 0;
+ }
+
+ const { supplier, category, broaderCategory, availableOnly } = params;
+ this.supplier = supplier ? await this.store.findRecordByUri('business-entity', supplier) : null;
+ this.category = category
+ ? await this.store.findRecordByUri('product-category', category)
+ : null;
+ this.broaderCategory = broaderCategory
+ ? await this.store.findRecordByUri('product-category', broaderCategory)
+ : null;
+ this.availableOnly = availableOnly;
+
+ const filter = {};
+
+ filter[':wildcard:searchName'] = getWildcardFilterValue(params.name);
+ if (isPresent(params.identifier)) {
+ filter['identifier'] = params.identifier;
+ }
+ filter['category.uuid'] = this.category?.id;
+ filter['broaderCategory.uuid'] = this.broaderCategory?.id;
+ filter['purchaseOffering.businessEntity.uuid'] = this.supplier?.id;
+ filter[':wildcard:purchaseOffering.identifier'] = getWildcardFilterValue(
+ params.supplierIdentifier
+ );
+ filter[':wildcard:warehouseLocation.rack'] = getWildcardFilterValue(params.rack);
+
+ if (params.availableOnly) {
+ // No validThrough dates in the future assumed.
+ // (has-no || gt now) would require custom elastic query
+ filter[':has-no:salesOffering.validThrough'] = 't';
+ }
+
+ this.lastParams.commit();
+
+ return search('products', params.page, params.size, params.sort, filter, (product) => {
+ const entry = product.attributes;
+ entry.id = product.id;
+ // create record for convenience of the isValid getter
+ // final response stays a pojo
+ const offering = this.store.createRecord('offering', {
+ validFrom: entry.salesOffering.validFrom,
+ validThrough: entry.salesOffering.validThrough,
+ });
+ entry.salesOffering.isValid = offering.isValid;
+ offering.destroyRecord();
+
+ entry.name = langStringResourceFormat(entry.name);
+ entry.alternateNames = langStringResourceFormat(entry.alternateNames);
+
+ return entry;
+ });
+ }
+
+ setupController(controller) {
+ super.setupController(...arguments);
+
+ const filter = copy(this.lastParams.committed);
+ filter.supplier = this.supplier;
+ filter.category = this.category;
+ filter.broaderCategory = this.broaderCategory;
+
+ filter.availableOnly = this.availableOnly;
+ if (!controller.filter) {
+ controller.filter = new ProductFilter(filter);
+ }
+
+ controller.page = this.lastParams.committed.page;
+ controller.size = this.lastParams.committed.size;
+ controller.sort = this.lastParams.committed.sort;
+ }
+
+ @action
+ loading(transition) {
+ // eslint-disable-next-line ember/no-controller-access-in-routes
+ const controller = this.controllerFor(this.routeName);
+ controller.isLoadingModel = true;
+ transition.promise.finally(function () {
+ controller.isLoadingModel = false;
+ });
+
+ return true; // bubble the loading event
+ }
+}
diff --git a/app/routes/main/products/list/prices.js b/app/routes/main/products/list/prices.js
new file mode 100644
index 0000000..f91141d
--- /dev/null
+++ b/app/routes/main/products/list/prices.js
@@ -0,0 +1,15 @@
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class MainProductsListPricesRoute extends Route {
+ @service userInfo;
+ @service router;
+
+ beforeModel() {
+ if(!this.userInfo.isPriceAdmin) {
+ // Only price admins can view this page
+ this.router.transitionTo('forbidden');
+ }
+ }
+}
diff --git a/app/templates/main/products/list.hbs b/app/templates/main/products/list.hbs
new file mode 100644
index 0000000..eec0686
--- /dev/null
+++ b/app/templates/main/products/list.hbs
@@ -0,0 +1,92 @@
+
+
+
+
+ {{svg-jar "search-line" class="shrink-0 mr-2 h-4 w-4 text-gray-500 fill-current"}}
+
+ Zoeken
+
+
+ {{#if this.isLoadingModel}}
+
+
+
+ {{else}}
+
+ ({{this.model.meta.count}} resultaten gevonden)
+
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{outlet}}
+
\ No newline at end of file
diff --git a/app/templates/main/products/index.hbs b/app/templates/main/products/list/index.hbs
similarity index 56%
rename from app/templates/main/products/index.hbs
rename to app/templates/main/products/list/index.hbs
index 5569a55..888b644 100644
--- a/app/templates/main/products/index.hbs
+++ b/app/templates/main/products/list/index.hbs
@@ -1,87 +1,3 @@
-
-
-
-
- {{svg-jar "search-line" class="shrink-0 mr-2 h-4 w-4 text-gray-500 fill-current"}}
-
- Zoeken
-
-
- {{#if this.isLoadingModel}}
-
-
-
- {{else}}
-
- ({{this.model.meta.count}} resultaten gevonden)
-
- {{/if}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -107,7 +23,7 @@
VKP btw in
|
-
+ |
Eenheid
|
@@ -131,11 +47,7 @@
{{product.identifier}}
|
- {{#if product.broaderCategory}}
- {{product.broaderCategory.label}}
-
- {{/if}}
- {{product.category.label}}
+
|
{{get-by-lang product.name}}
@@ -184,14 +96,7 @@
-
+
{{#if this.previewProduct}}
+
+
+
+
+
+
+
+ |
+
+ Nr.
+ |
+
+ Categorie
+ |
+
+ Naam
+ |
+
+ inkoopprijs
+ |
+
+ marge
+ |
+
+ Verkoopprijs
+ |
+
+ Eenheid
+ |
+
+ Leverancier
+ |
+ {{!-- savestate --}} |
+
+
+
+ {{#each (if this.products.isLoading this.model this.products.value) as |product|}}
+
+ {{!-- product might be a mu-search pojo! --}}
+
+
+ {{#if product.salesOffering.isValid}}
+ {{svg-jar "circle-fill" title="Beschikbaar" class="h-3 w-3 text-green-400 fill-current"}}
+ {{else}}
+ {{svg-jar "circle-fill" title="Niet beschikbaar" class="h-3 w-3 text-red-400 fill-current"}}
+ {{/if}}
+ |
+
+ {{product.identifier}}
+ |
+
+
+ |
+
+ {{get-by-lang product.name}}
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ {{product.purchaseOffering.businessEntity.name}}
+ {{#if product.purchaseOffering.identifier}}
+ Nr. {{product.purchaseOffering.identifier}}
+ {{/if}}
+ |
+ |
+
+
+ {{else}}
+
+
+ Geen producten gevonden.
+ |
+
+ {{/each}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/utils/product-price.js b/app/utils/product-price.js
new file mode 100644
index 0000000..150055d
--- /dev/null
+++ b/app/utils/product-price.js
@@ -0,0 +1,47 @@
+import { get } from '@ember/object';
+import { VAT_RATE } from '../config';
+import constants from '../config/constants';
+import roundDecimal from './round-decimal';
+const { CALCULATION_BASIS } = constants;
+
+// recalculate the sales price of a product and returns this adjusted salesPrice (a unitPriceSpecification)
+export async function recalculateSalesPrice(product) {
+ const purchaseOffering = await product.purchaseOffering;
+
+ const purchasePrice = await purchaseOffering.unitPriceSpecification;
+ purchasePrice.currencyValue = roundDecimal(purchasePrice.currencyValue);
+
+ const salesOffering = await product.salesOffering;
+ const salesPrice = await salesOffering.unitPriceSpecification;
+
+ if (hasMarginCalculationBasis(salesPrice)) {
+ salesPrice.margin = roundDecimal(salesPrice.margin);
+ if (salesPrice.valueAddedTaxIncluded) {
+ const value = purchasePrice.currencyValue * salesPrice.margin * (1 + VAT_RATE);
+ salesPrice.currencyValue = roundDecimal(value);
+ } else {
+ const value = purchasePrice.currencyValue * salesPrice.margin;
+ salesPrice.currencyValue = roundDecimal(value);
+ }
+ } else {
+ salesPrice.currencyValue = roundDecimal(salesPrice.currencyValue);
+ const margin = salesPrice.currencyValueTaxExcluded / purchasePrice.currencyValue;
+ salesPrice.margin = roundDecimal(margin);
+ }
+ return salesPrice;
+}
+
+export async function recalculateSalesPriceAndSave(product) {
+ const salesPrice = await recalculateSalesPrice(product);
+ await salesPrice.save();
+}
+
+export function hasPriceOutCalculationBasis(unitPriceSpecification) {
+ // eslint-disable-next-line ember/no-get
+ return get(unitPriceSpecification, 'calculationBasis') == CALCULATION_BASIS.PRICE_OUT;
+}
+
+export function hasMarginCalculationBasis(unitPriceSpecification) {
+ // eslint-disable-next-line ember/no-get
+ return get(unitPriceSpecification, 'calculationBasis') == CALCULATION_BASIS.MARGIN;
+}
diff --git a/config/environment.js b/config/environment.js
index 554bb92..2d36703 100644
--- a/config/environment.js
+++ b/config/environment.js
@@ -31,7 +31,7 @@ module.exports = function (environment) {
},
},
'ember-simple-auth': {
- routeAfterAuthentication: 'main.products.index',
+ routeAfterAuthentication: 'main.products.list',
},
};
diff --git a/package.json b/package.json
index b26a5f4..6fe735e 100644
--- a/package.json
+++ b/package.json
@@ -86,12 +86,13 @@
"ember-simple-auth": "^6.0.0",
"ember-source": "~5.12.0",
"ember-svg-jar": "^2.3.3",
+ "ember-template-imports": "^4.1.3",
"ember-template-lint": "^6.0.0",
"ember-tooltips": "^3.6.0",
"ember-truth-helpers": "^4.0.3",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-ember": "^12.2.1",
+ "eslint-plugin-ember": "^12.3.1",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-qunit": "^8.1.2",
@@ -100,8 +101,10 @@
"postcss-cli": "^10.1.0",
"postcss-import": "^15.1.0",
"prettier": "^3.3.3",
+ "prettier-plugin-ember-template-tag": "^2.0.2",
"qunit": "^2.22.0",
"qunit-dom": "^3.2.1",
+ "reactiveweb": "^1.3.0",
"release-it": "^17.0.1",
"remixicon": "^4.1.0",
"stylelint": "^15.11.0",
|