Skip to content

Add price editing overview page #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: 18-user-group-roles
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
],
};
3 changes: 2 additions & 1 deletion .prettierrc.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
16 changes: 12 additions & 4 deletions app/components/fmt/unit-code.js
Original file line number Diff line number Diff line change
@@ -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 'm<sup>2</sup>';
} else if (this.args.value == 'st') {
} else if (this.alabel == 'st') {
return 'stuk';
} else {
return this.args.value;
return this.label;
}
}
}
10 changes: 4 additions & 6 deletions app/components/fmt/unit-price.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{{#if this.currencyValueLoader.isResolved}}
<Fmt::Currency
@value={{this.currencyValueLoader.value}}
@currency={{@model.currency}} />
{{#if @showUnit}}/ <Fmt::UnitCode @value={{@model.unitCode}} />{{/if}}
{{/if}}
<Fmt::Currency
@value={{this.calculatedCurrencyValue}}
@currency={{@model.currency}} />
{{#if @showUnit}}/ <Fmt::UnitCode @value={{@model.unitCode}} />{{/if}}
39 changes: 15 additions & 24 deletions app/components/fmt/unit-price.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
9 changes: 8 additions & 1 deletion app/components/main-menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@
</div>
<div class="hidden md:block">
<div class="ml-10 flex items-baseline">
<LinkTo @route="main.products.index"
<LinkTo @route="main.products.list.index"
@activeClass="text-white !bg-red-700"
class="ml-4 px-3 py-2 rounded-md font-medium text-red-100 hover:bg-red-600 focus:outline-none focus:text-white focus:bg-red-600">
Producten
</LinkTo>
{{#if this.canEditPrice}}
<LinkTo @route="main.products.list.prices"
@activeClass="text-white !bg-red-700"
class="ml-4 px-3 py-2 rounded-md font-medium text-red-100 hover:bg-red-600 focus:outline-none focus:text-white focus:bg-red-600">
Prijzenlijst
</LinkTo>
{{/if}}
<LinkTo @route="main.suppliers.index"
@activeClass="text-white !bg-red-700"
class="ml-4 px-3 py-2 rounded-md font-medium text-red-100 hover:bg-red-600 focus:outline-none focus:text-white focus:bg-red-600">
Expand Down
3 changes: 3 additions & 0 deletions app/components/main-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions app/components/product/category-tree.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{#let (or @product.broaderCategory.label @product.category.broader.label) as |broaderCategoryLabel| }}
{{#if broaderCategoryLabel}}
{{broaderCategoryLabel}}
<br> <div class="w-3 h-3 ml-2 mr-1 inline-block border-b-2 border-l-2 border-gray-300"></div>
{{/if}}
{{/let}}
{{@product.category.label}}
33 changes: 6 additions & 27 deletions app/components/product/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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();
}
}
125 changes: 125 additions & 0 deletions app/components/product/inline-price-edit.gjs
Original file line number Diff line number Diff line change
@@ -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 = <template>
<div
class='p-4 grid grid-cols-8 gap-y-6 gap-x-4 rounded-md bg-gray-50 border
{{if @active "border-green-300" "border-gray-200"}}'
>
<div class='col-span-1'>
<input
id='calc-base-margin{{@uuid}}'
type='radio'
name='price-calc-basis{{@uuid}}'
value={{constant 'CALCULATION_BASIS.MARGIN'}}
checked={{@active}}
{{on 'input' (pick 'target.value' (fn @setCalculationBasis @product))}}
class='focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300'
/>
</div>
<div class='col-span-7 pl-2'>
<DecimalInput
@label='Factor'
@value={{@product.salesOffering.unitPriceSpecification.margin}}
@onChange={{fn @setMargin @product}}
class='focus:ring-red-200 focus:border-red-200 block w-full border-gray-300 rounded-md sm:text-sm
{{unless @active "disabled:bg-gray-200"}}'
disabled={{not @active}}
/>
</div>
</div>
</template>;

const PurchasePrice = <template>
<DecimalInput
@label=''
@leading='&euro;'
@value={{@product.purchaseOffering.unitPriceSpecification.currencyValue}}
@onChange={{fn @setPriceIn @product}}
class='focus:ring-red-200 focus:border-red-200 block w-24 pl-7 border-gray-300 rounded-md sm:text-sm'
/>
</template>;

const SellingPrice = <template>
<div
class='relative p-4 grid grid-cols-8 gap-x-4 rounded-md bg-gray-50 border
{{if @active "border-green-300" "border-gray-200"}}'
>
<div class='absolute top-0 right-0 mt-2 mr-2'>
<input
id='calc-base-price-out{{@uuid}}'
type='radio'
name='price-calc-basis{{@uuid}}'
value={{constant 'CALCULATION_BASIS.PRICE_OUT'}}
checked={{@active}}
{{on 'input' (pick 'target.value' (fn @setCalculationBasis @product))}}
class='focus:ring-green-500 h-4 w-4 text-green-600 border-gray-300'
/>
</div>
<div class='pb-4 col-span-8'>
<Property @label='Prijs excl. btw'>
<UnitPrice
@model={{@product.salesOffering.unitPriceSpecification}}
@showTaxIncluded={{false}}
/>
</Property>
</div>
<div class='col-span-8'>
<DecimalInput
@label='Prijs incl. btw'
@leading='&euro;'
@value={{@product.salesOffering.unitPriceSpecification.currencyValue}}
@onChange={{fn @setPriceOut @product}}
class='focus:ring-red-200 focus:border-red-200 block w-24 pl-7 border-gray-300 rounded-md sm:text-sm
{{unless @active "disabled:bg-gray-200"}}'
disabled={{not @active}}
/>
</div>
</div>
</template>;
export default class ProductInlinePriceEditComponent extends Component {
adjustPrice = enqueueTask(async (callback, product, value) => {
await callback(product, value);
});

<template>
{{#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}}
</template>
}
13 changes: 13 additions & 0 deletions app/components/util/task-state.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{#if @state.isRunning}}
<div class="items-center space-x-2" title={{@labelRunning}}>
<Util::LoadingSpinner @label="" />
</div>
{{else if @state.last.isSuccessful}}
<div class="items-center space-x-2" title={{@labelSuccess}}>
{{svg-jar "check-fill" class="text-green-600"}}
</div>
{{else if @state.last.isError}}
<div class="items-center space-x-2" title={{@state.last.error}}>
{{svg-jar "error-warning-line" class="h-6 w-6 text-red-600"}}
</div>
{{/if}}
Loading