Merge pull request 'fix: refs #6896 remove files' (!2899) from 6896-ordersMigration into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #2899
Reviewed-by: Alex Moreno <alexm@verdnatura.es>
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
Carlos Satorres 2024-10-04 06:40:19 +00:00
commit 505fff82a9
62 changed files with 10 additions and 3666 deletions

View File

@ -1,46 +0,0 @@
import getBrowser from '../../helpers/puppeteer';
const $ = {
id: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(1) span',
alias: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(2) span',
consignee: 'vn-order-summary vn-one:nth-child(2) > vn-label-value:nth-child(6) span',
subtotal: 'vn-order-summary vn-one.taxes > p:nth-child(1)',
vat: 'vn-order-summary vn-one.taxes > p:nth-child(2)',
total: 'vn-order-summary vn-one.taxes > p:nth-child(3)',
sale: 'vn-order-summary vn-tbody > vn-tr',
};
describe('Order summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('16');
});
afterAll(async() => {
await browser.close();
});
it('should reach the order summary section and check data', async() => {
await page.waitForState('order.card.summary');
const id = await page.innerText($.id);
const alias = await page.innerText($.alias);
const consignee = await page.innerText($.consignee);
const subtotal = await page.innerText($.subtotal);
const vat = await page.innerText($.vat);
const total = await page.innerText($.total);
const sale = await page.countElement($.sale);
expect(id).toEqual('16');
expect(alias).toEqual('Many places');
expect(consignee).toEqual('address 26 - Gotham (Province one)');
expect(subtotal.length).toBeGreaterThan(1);
expect(vat.length).toBeGreaterThan(1);
expect(total.length).toBeGreaterThan(1);
expect(sale).toBeGreaterThan(0);
});
});

View File

@ -1,69 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
const $ = {
form: 'vn-order-basic-data form',
observation: 'vn-order-basic-data form [vn-name="note"]',
saveButton: `vn-order-basic-data form button[type=submit]`,
acceptButton: '.vn-confirm.shown button[response="accept"]'
};
describe('Order edit basic data path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('1');
await page.accessToSection('order.card.basicData');
});
afterAll(async() => {
await browser.close();
});
describe('when confirmed order', () => {
it('should not be able to change the client', async() => {
const message = await page.sendForm($.form, {
client: 'Tony Stark',
address: 'Tony Stark',
});
expect(message.text).toContain(`You can't make changes on the basic data`);
});
});
describe('when new order', () => {
it('should create an order and edit its basic data', async() => {
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
await page.waitToClick($.acceptButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.ordersIndex.createOrderButton);
await page.waitForState('order.create');
await page.autocompleteSearch(selectors.createOrderView.client, 'Jessica Jones');
await page.pickDate(selectors.createOrderView.landedDatePicker);
await page.autocompleteSearch(selectors.createOrderView.agency, 'Other agency');
await page.waitToClick(selectors.createOrderView.createButton);
await page.waitForState('order.card.catalog');
await page.accessToSection('order.card.basicData');
const values = {
client: 'Tony Stark',
address: 'Tony Stark',
agencyMode: 'Other agency'
};
const message = await page.sendForm($.form, values);
await page.reloadSection('order.card.basicData');
const formValues = await page.fetchForm($.form, Object.keys(values));
expect(message.isSuccess).toBeTrue();
expect(formValues).toEqual(values);
});
});
});

View File

@ -1,48 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order lines', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('8');
await page.accessToSection('order.card.line');
});
afterAll(async() => {
await browser.close();
});
it('should check the order subtotal', async() => {
const result = await page
.waitToGetProperty(selectors.orderLine.orderSubtotal, 'innerText');
expect(result).toContain('112.30');
});
it('should delete the first line in the order', async() => {
await page.waitToClick(selectors.orderLine.firstLineDeleteButton);
await page.waitToClick(selectors.orderLine.confirmButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm the order subtotal has changed', async() => {
await page.waitForTextInElement(selectors.orderLine.orderSubtotal, '92.80');
const result = await page
.waitToGetProperty(selectors.orderLine.orderSubtotal, 'innerText');
expect(result).toContain('92.80');
});
it('should confirm the whole order and redirect to ticket index filtered by clientFk', async() => {
await page.waitToClick(selectors.orderLine.confirmOrder);
await page.expectURL('ticket/index');
await page.expectURL('clientFk');
});
});

View File

@ -1,97 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order catalog', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
});
afterAll(async() => {
await browser.close();
});
it('should open the create new order form', async() => {
await page.waitToClick(selectors.ordersIndex.createOrderButton);
await page.waitForState('order.create');
});
it('should create a new order', async() => {
await page.autocompleteSearch(selectors.createOrderView.client, 'Tony Stark');
await page.pickDate(selectors.createOrderView.landedDatePicker);
await page.autocompleteSearch(selectors.createOrderView.agency, 'Other agency');
await page.waitToClick(selectors.createOrderView.createButton);
await page.waitForState('order.card.catalog');
});
it('should add the realm and type filters and obtain results', async() => {
await page.waitToClick(selectors.orderCatalog.plantRealmButton);
await page.autocompleteSearch(selectors.orderCatalog.type, 'Anthurium');
await page.waitForNumberOfElements('section.product', 4);
const result = await page.countElement('section.product');
expect(result).toEqual(4);
});
it('should perfom an "OR" search for the item tag colors silver and brown', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Color');
await page.autocompleteSearch(selectors.orderCatalog.firstTagAutocomplete, 'silver');
await page.waitToClick(selectors.orderCatalog.addTagButton);
await page.autocompleteSearch(selectors.orderCatalog.secondTagAutocomplete, 'brown');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 4);
});
it('should perfom an "OR" search for the item tag tallos 2 and 9', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Tallos');
await page.write(selectors.orderCatalog.firstTagValue, '2');
await page.waitToClick(selectors.orderCatalog.addTagButton);
await page.write(selectors.orderCatalog.secondTagValue, '9');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 2);
});
it('should perform a general search for category', async() => {
await page.write(selectors.orderCatalog.itemTagValue, 'concussion');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements('section.product', 2);
});
it('should perfom an "AND" search for the item tag tallos 2', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Tallos');
await page.write(selectors.orderCatalog.firstTagValue, '2');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 1);
});
it('should remove the tag filters and have 4 results', async() => {
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.sixthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.fifthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.fourthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.thirdFilterRemoveButton);
await page.waitForNumberOfElements('.product', 4);
const result = await page.countElement('section.product');
expect(result).toEqual(4);
});
it('should search for an item by id', async() => {
await page.accessToSearchResult('2');
await page.waitForNumberOfElements('section.product', 1);
const result = await page.countElement('section.product');
expect(result).toEqual(1);
});
});

View File

@ -1,34 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order Index', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
});
afterAll(async() => {
await browser.close();
});
it(`should check the second search result doesn't contain a total of 0€`, async() => {
await page.waitToClick(selectors.globalItems.searchButton);
const result = await page.waitToGetProperty(selectors.ordersIndex.secondSearchResultTotal, 'innerText');
expect(result).not.toContain('0.00');
});
it('should search including empty orders', async() => {
await page.waitToClick(selectors.ordersIndex.openAdvancedSearch);
await page.waitToClick(selectors.ordersIndex.advancedSearchShowEmptyCheckbox);
await page.waitToClick(selectors.ordersIndex.advancedSearchButton);
await page.waitForTextInElement(selectors.ordersIndex.secondSearchResultTotal, '0.00');
const result = await page.waitToGetProperty(selectors.ordersIndex.secondSearchResultTotal, 'innerText');
expect(result).toContain('0.00');
});
});

View File

@ -1,88 +0,0 @@
<mg-ajax path="Orders/{{patch.params.id}}/updateBasicData" options="vnPatch"></mg-ajax>
<vn-crud-model
autoload="true"
url="Addresses"
data="address"
order="nickname"
vn-id="address-model">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="$ctrl.order"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete
vn-one
url="Clients"
label="Client"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
show-field="name"
value-field="id"
ng-model="$ctrl.order.clientFk"
vn-name="client"
selection="$ctrl.selection"
fields="['defaultAddressFk']">
<tpl-item>
<div>{{::name}}</div>
<div class="text-secondary text-caption">#{{::id}}</div>
</tpl-item>
</vn-autocomplete>
<vn-autocomplete
vn-one
fields="['id', 'nickname']"
data="address"
label="Address"
search-function="$search"
show-field="nickname"
value-field="id"
ng-model="$ctrl.order.addressFk"
vn-name="address"
on-change="$ctrl.getAvailableAgencies()">
<tpl-item>{{::nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="Landed"
ng-model="$ctrl.order.landed"
vn-name="landed"
on-change="$ctrl.getAvailableAgencies()">
</vn-date-picker>
<vn-autocomplete
disabled="!$ctrl.order.addressFk || !$ctrl.order.landed"
data="$ctrl._availableAgencies"
label="Agency"
show-field="agencyMode"
value-field="agencyModeFk"
ng-model="$ctrl.order.agencyModeFk"
vn-name="agencyMode">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Notes"
ng-model="$ctrl.order.note"
vn-name="note"
rule>
</vn-textarea>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>

View File

@ -1,57 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
let isDirty = false;
this.$.$watch('$ctrl.selection', newValue => {
if (newValue) {
this.$.addressModel.where = {clientFk: newValue.id};
this.$.addressModel.refresh();
if (isDirty)
this.order.addressFk = newValue.defaultAddressFk;
isDirty = true;
} else {
this.$.addressModel.clear();
if (isDirty)
this.order.addressFk = null;
}
});
}
set order(value = {}) {
this._order = value;
const agencyModeFk = value.agencyModeFk;
this.getAvailableAgencies();
this._order.agencyModeFk = agencyModeFk;
}
get order() {
return this._order;
}
getAvailableAgencies() {
const order = this.order;
order.agencyModeFk = null;
const params = {
addressFk: order.addressFk,
landed: order.landed
};
if (params.landed && params.addressFk) {
this.$http.get(`Agencies/landsThatDay`, {params})
.then(res => this._availableAgencies = res.data);
}
}
}
ngModule.vnComponent('vnOrderBasicData', {
controller: Controller,
template: require('./index.html'),
bindings: {
order: '<'
}
});

View File

@ -1,67 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderBasicData', () => {
let $httpBackend;
let $httpParamSerializer;
let controller;
let $scope;
beforeEach(ngModule('order'));
beforeEach(inject(($compile, _$httpBackend_, $rootScope, _$httpParamSerializer_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$scope = $rootScope.$new();
$httpBackend.whenRoute('GET', 'Addresses')
.respond([{id: 2, nickname: 'address 2'}]);
$httpBackend.whenRoute('GET', 'Clients')
.respond([{id: 1, defaultAddressFk: 1}]);
$scope.order = {clientFk: 1, addressFk: 1};
let $element = $compile('<vn-order-basic-data order="order"></vn-order-basic-data>')($scope);
$httpBackend.flush();
controller = $element.controller('vnOrderBasicData');
}));
afterAll(() => {
$scope.$destroy();
$element.remove();
});
describe('constructor()', () => {
it('should update the address after the client changes', async() => {
const addressId = 999;
const id = 444;
controller.selection = {id: id, defaultAddressFk: addressId};
$scope.$digest();
expect(controller.order.addressFk).toEqual(addressId);
});
});
describe('getAvailableAgencies()', () => {
it('should set agencyModeFk to null and get the available agencies if the order has landed and client', async() => {
controller.order.agencyModeFk = 999;
controller.order.addressFk = 999;
controller.order.landed = Date.vnNew();
const expectedAgencies = [{id: 1}, {id: 2}];
const paramsObj = {
addressFk: controller.order.addressFk,
landed: controller.order.landed
};
const serializedParams = $httpParamSerializer(paramsObj);
$httpBackend.expect('GET', `Agencies/landsThatDay?${serializedParams}`).respond(expectedAgencies);
controller.getAvailableAgencies();
$httpBackend.flush();
expect(controller.order.agencyModeFk).toBeDefined();
expect(controller._availableAgencies).toEqual(expectedAgencies);
});
});
});
});

View File

@ -1 +0,0 @@
This form has been disabled because there are lines in this order or it's confirmed: Este formulario ha sido deshabilitado por que esta orden contiene líneas o está confirmada

View File

@ -1,9 +0,0 @@
vn-order-basic-data {
.disabledForm {
text-align: center;
color: red;
span {
margin: 0 auto;
}
}
}

View File

@ -1,5 +0,0 @@
<vn-portal slot="menu">
<vn-order-descriptor order="$ctrl.order"></vn-order-descriptor>
<vn-left-menu source="card"></vn-left-menu>
</vn-portal>
<ui-view></ui-view>

View File

@ -1,58 +0,0 @@
import ngModule from '../module';
import ModuleCard from 'salix/components/module-card';
class Controller extends ModuleCard {
reload() {
let filter = {
include: [
{
relation: 'agencyMode',
scope: {
fields: ['name']
}
}, {
relation: 'address',
scope: {
fields: ['nickname']
}
}, {
relation: 'rows',
scope: {
fields: ['id']
}
}, {
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked'
],
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
]
};
return this.$q.all([
this.$http.get(`Orders/${this.$params.id}`, {filter})
.then(res => this.order = res.data),
this.$http.get(`Orders/${this.$params.id}/getTotal`)
.then(res => ({total: res.data}))
]).then(res => {
this.order = Object.assign.apply(null, res);
});
}
}
ngModule.vnComponent('vnOrderCard', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,31 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderCard', () => {
let controller;
let $httpBackend;
let data = {id: 1, name: 'fooName'};
let total = 10.5;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_, $stateParams) => {
$httpBackend = _$httpBackend_;
let $element = angular.element('<div></div>');
controller = $componentController('vnOrderCard', {$element});
$stateParams.id = data.id;
$httpBackend.whenRoute('GET', 'Orders/:id').respond(data);
$httpBackend.whenRoute('GET', 'Orders/:id/getTotal').respond(200, total);
}));
it('should request data and total, merge them, and set it on the controller', () => {
controller.reload();
$httpBackend.flush();
expect(controller.order).toEqual(Object.assign({}, data, {total}));
});
});
});

View File

@ -1,54 +0,0 @@
<div class="vn-pa-lg" style="min-width: 18em">
<form name="form" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-autocomplete
vn-id="tag"
vn-one
selection="filter.tagSelection"
ng-model="filter.tagFk"
data="$ctrl.resultTags"
show-field="name"
label="Tag"
on-change="itemTag.value = null">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal ng-repeat="tagValue in filter.values">
<vn-textfield
vn-one
ng-show="tag.selection.isFree != false"
vn-id="text"
label="Value"
ng-model="tagValue.value">
</vn-textfield>
<vn-autocomplete
vn-one
ng-show="tag.selection.isFree == false"
url="{{'Tags/' + tag.selection.id + '/filterValue'}}"
search-function="{value: $search}"
label="Value"
ng-model="tagValue.value"
show-field="value"
value-field="value">
</vn-autocomplete>
<vn-icon-button
vn-none
vn-tooltip="Remove tag"
icon="delete"
ng-click="filter.values.splice($index, 1)"
tabindex="-1">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal>
<vn-icon-button
vn-none
vn-bind="+"
vn-tooltip="Add value"
icon="add_circle"
ng-click="$ctrl.addValue()">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -1,38 +0,0 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($element, $) {
super($element, $);
this.filter = {};
}
get filter() {
return this.$.filter;
}
set filter(value) {
if (!value)
value = {};
if (!value.values)
value.values = [{}];
this.$.filter = value;
}
addValue() {
this.filter.values.push({});
setTimeout(() => this.parentPopover.relocate());
}
}
ngModule.vnComponent('vnOrderCatalogSearchPanel', {
template: require('./index.html'),
controller: Controller,
bindings: {
onSubmit: '&?',
parentPopover: '<?',
resultTags: '<?'
}
});

View File

@ -1,81 +0,0 @@
<vn-data-viewer
model="$ctrl.model">
<vn-horizontal class="catalog-list">
<section ng-repeat="item in $ctrl.model.data" class="product">
<vn-card>
<div class="image">
<div ng-if="::item.hex != null" class="item-color-background">
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div>
<img
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/>
</div>
<div class="description">
<h3 class="link"
ng-click="itemDescriptor.show($event, item.id)">
{{::item.name}}
</h3>
<h4 class="ellipsize">
<span translate-attr="::{title: item.subName}">{{::item.subName}}</span>
</h4>
<div class="tags">
<vn-label-value
ng-if="::item.value5"
label="{{::item.tag5}}"
value="{{::item.value5}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value6"
label="{{::item.tag6}}"
value="{{::item.value6}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value7"
label="{{::item.tag7}}"
value="{{::item.value7}}">
</vn-label-value>
</div>
<vn-horizontal
class="text-right text-caption alert vn-mr-xs"
ng-if="::item.minQuantity">
<vn-one>
<vn-icon
icon="production_quantity_limits"
translate-attr="{title: 'Minimal quantity'}"
class="text-subtitle1">
</vn-icon>
</vn-one>
{{::item.minQuantity}}
</vn-horizontal>
<div class="footer">
<div class="price">
<vn-one>
<span>{{::item.available}}</span>
<span translate>to</span>
<span>{{::item.price | currency:'EUR':2}}</span>
</vn-one>
<vn-icon-button vn-none
icon="add_circle"
ng-click="pricesPopover.show($event, item)"
vn-tooltip="Add">
</vn-icon-button>
</div>
<div class="priceKg" ng-show="::item.priceKg">
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
</div>
</div>
</div>
</vn-card>
</section>
</vn-horizontal>
</vn-data-viewer>
<vn-order-prices-popover
vn-id="prices-popover"
order="$ctrl.order">
</vn-order-prices-popover>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>

View File

@ -1,12 +0,0 @@
import ngModule from '../module';
import Component from 'core/lib/component';
import './style.scss';
ngModule.vnComponent('vnOrderCatalogView', {
template: require('./index.html'),
controller: Component,
bindings: {
order: '<',
model: '<'
}
});

View File

@ -1 +0,0 @@
Order created: Orden creada

View File

@ -1,50 +0,0 @@
@import "variables";
vn-order-catalog {
.catalog-header {
border-bottom: $border-thin;
padding: $spacing-md;
align-items: center;
& > vn-one {
display: flex;
flex: 1;
span {
color: $color-font-secondary
}
}
& > vn-auto {
width: 448px;
display: flex;
overflow: hidden;
& > * {
padding-left: $spacing-md;
}
}
}
.catalog-list {
padding-top: $spacing-sm;
}
.item-color-background {
background: linear-gradient($color-bg-panel, $color-main);
border-radius: 50%;
margin-left: 140px;
margin-top: 140px;
width: 40px;
height: 40px;
position: absolute;
}
.item-color {
margin: auto;
margin-top: 5px;
border-radius: 50%;
width: 30px;
height: 30px;
position: relative;
}
.alert {
color: $color-alert;
}
}

View File

@ -1,166 +0,0 @@
<vn-crud-model
url="ItemCategories"
data="categories"
auto-load="true">
</vn-crud-model>
<vn-crud-model
vn-id="model"
url="Orders/CatalogFilter"
params="{orderFk: $ctrl.$params.id}"
limit="50"
data="$ctrl.items">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar vn-id="searchbar"
auto-state="false"
info="Search by item id or name"
on-search="$ctrl.onSearch($params)">
</vn-searchbar>
</vn-portal>
<vn-order-catalog-view
model="model"
order="$ctrl.order">
</vn-order-catalog-view>
<vn-side-menu side="right">
<vn-horizontal class="item-category">
<vn-autocomplete vn-id="category"
data="categories"
ng-model="$ctrl.categoryId"
show-field="name"
value-field="id"
label="Category">
</vn-autocomplete>
<vn-one ng-repeat="category in categories">
<vn-icon
ng-class="{'active': $ctrl.categoryId == category.id}"
icon="{{::category.icon}}"
vn-tooltip="{{::category.name}}"
ng-click="$ctrl.changeCategory(category.id)">
</vn-icon>
</vn-one>
</vn-horizontal>
<vn-vertical class="input">
<vn-autocomplete vn-id="type"
data="$ctrl.itemTypes"
ng-model="$ctrl.typeId"
show-field="name"
value-field="id"
label="Type"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{categoryName}}
</div>
</tpl-item>
</vn-autocomplete>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-autocomplete
vn-id="field"
data="$ctrl.orderFields"
ng-model="$ctrl.orderField"
selection="$ctrl.orderSelection"
translate-fields="['name']"
order="priority DESC"
show-field="name"
value-field="field"
label="Order by"
disabled="!model.data">
</vn-autocomplete>
<vn-autocomplete
data="$ctrl.orderWays"
ng-model="$ctrl.orderWay"
translate-fields="['name']"
show-field="name"
value-field="way"
label="Order"
disabled="!model.data">
</vn-autocomplete>
<div ng-if="false && model.moreRows">
<span translate>More than</span> {{model.limit}} <span translate>results</span>
</div>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-textfield vn-one
vn-id="search"
ng-keyUp="$ctrl.onSearchByTag($event)"
label="Search tag">
<prepend>
<vn-icon icon="search"></vn-icon>
</prepend>
<append>
<vn-icon
icon="keyboard_arrow_down"
ng-click="$ctrl.openPanel($event)"
style="cursor: pointer;">
</vn-icon>
</append>
</vn-textfield>
</vn-vertical>
<vn-popover
vn-id="popover"
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
on-submit="$ctrl.onPanelSubmit($filter)"
parent-popover="popover"
result-tags="$ctrl.resultTags">
</vn-order-catalog-search-panel>
</vn-popover>
<div class="chips">
<vn-chip
ng-if="$ctrl.itemId"
removable="true"
vn-tooltip="Item id"
on-remove="$ctrl.removeItemId()"
class="colored">
<span>Id: {{$ctrl.itemId}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.itemName"
removable="true"
vn-tooltip="Item"
on-remove="$ctrl.removeItemName()"
class="colored">
<div>
<span>
<span translate>Name</span>:
</span>
<span>{{$ctrl.itemName}}</span>
</div>
</vn-chip>
<vn-chip
ng-if="category.selection"
removable="true"
vn-tooltip="Category"
on-remove="$ctrl.categoryId = null"
class="colored">
<span translate>{{category.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="type.selection"
removable="true"
vn-tooltip="Type"
on-remove="$ctrl.typeId = null"
class="colored">
<span translate>{{type.selection.name}}</span>
</vn-chip>
<vn-chip
ng-repeat="tagGroup in $ctrl.tagGroups"
removable="true"
on-remove="$ctrl.remove($index)"
vn-tooltip="{{::$ctrl.formatTooltip(tagGroup)}}"
class="colored">
<div>
<span ng-if="::tagGroup.tagFk">
<span translate>{{::tagGroup.tagSelection.name}}</span>:
</span>
<span ng-repeat="tagValue in tagGroup.values">
<span ng-if="$index > 0">,</span>
<span>"{{::tagValue.value}}"</span>
</span>
</div>
</vn-chip>
</div>
</vn-side-menu>

View File

@ -1,377 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.itemTypes = [];
this._tagGroups = [];
// Static autocomplete data
this.orderWays = [
{way: 'ASC', name: 'Ascendant'},
{way: 'DESC', name: 'Descendant'},
];
this.defaultOrderFields = [
{field: 'relevancy DESC, name', name: 'Relevancy', priority: 999},
{field: 'showOrder, price', name: 'Color and price', priority: 999},
{field: 'name', name: 'Name', priority: 999},
{field: 'price', name: 'Price', priority: 999}
];
this.orderFields = [].concat(this.defaultOrderFields);
this._orderWay = this.orderWays[0].way;
this.orderField = this.orderFields[0].field;
}
$onChanges() {
this.getData().then(() => {
if (this.order && this.order.isConfirmed)
this.$state.go('order.card.line');
});
}
getData() {
return this.$http.get(`Orders/${this.$params.id}`)
.then(res => this.order = res.data);
}
/**
* Fills order autocomplete with tags
* obtained from last filtered
*/
get order() {
return this._order;
}
/**
* Sets filter values from state params
*
* @param {Object} value - Order data
*/
set order(value) {
this._order = value;
if (!value) return;
this.$.$applyAsync(() => {
if (this.$params.categoryId)
this.categoryId = parseInt(this.$params.categoryId);
if (this.$params.typeId)
this.typeId = parseInt(this.$params.typeId);
if (this.$params.tagGroups)
this.tagGroups = JSON.parse(this.$params.tagGroups);
});
}
get items() {
return this._items;
}
set items(value) {
this._items = value;
if (!value) return;
this.fetchResultTags(value);
this.buildOrderFilter();
}
get categoryId() {
return this._categoryId;
}
set categoryId(value = null) {
this._categoryId = value;
this.itemTypes = [];
this.typeId = null;
this.updateStateParams();
if (this.tagGroups.length > 0)
this.applyFilters();
if (value)
this.updateItemTypes();
}
changeCategory(id) {
if (this._categoryId == id) id = null;
this.categoryId = id;
}
get typeId() {
return this._typeId;
}
set typeId(value) {
this._typeId = value;
this.updateStateParams();
if (value || this.tagGroups.length > 0)
this.applyFilters();
}
get tagGroups() {
return this._tagGroups;
}
set tagGroups(value) {
this._tagGroups = value;
this.updateStateParams();
if (value.length)
this.applyFilters();
}
/**
* Get order way ASC/DESC
*/
get orderWay() {
return this._orderWay;
}
set orderWay(value) {
this._orderWay = value;
if (value) this.applyOrder();
}
/**
* Returns the order way selection
*/
get orderSelection() {
return this._orderSelection;
}
set orderSelection(value) {
this._orderSelection = value;
if (value) this.applyOrder();
}
/**
* Apply order to model
*/
applyOrder() {
if (this.typeId || this.tagGroups.length > 0 || this.itemName)
this.$.model.addFilter(null, {orderBy: this.getOrderBy()});
}
/**
* Returns order param
*
* @return {Object} - Order param
*/
getOrderBy() {
const isTag = !!(this.orderSelection && this.orderSelection.isTag);
return {
field: this.orderField,
way: this.orderWay,
isTag: isTag
};
}
/**
* Refreshes item type dropdown data
*/
updateItemTypes() {
let params = {
itemCategoryId: this.categoryId
};
const query = `Orders/${this.order.id}/getItemTypeAvailable`;
this.$http.get(query, {params}).then(res =>
this.itemTypes = res.data);
}
/**
* Search by tag value
* @param {object} event
*/
onSearchByTag(event) {
const value = this.$.search.value;
if (event.key !== 'Enter' || !value) return;
this.tagGroups.push({values: [{value: value}]});
this.$.search.value = null;
this.updateStateParams();
this.applyFilters();
}
remove(index) {
this.tagGroups.splice(index, 1);
this.updateStateParams();
if (this.tagGroups.length >= 0 || this.itemId || this.typeId)
this.applyFilters();
}
removeItemId() {
this.itemId = null;
this.$.searchbar.doSearch({}, 'bar');
}
removeItemName() {
this.itemName = null;
this.$.searchbar.doSearch({}, 'bar');
}
applyFilters(filter = {}) {
let newParams = {};
let newFilter = Object.assign({}, filter);
const model = this.$.model;
if (this.categoryId)
newFilter.categoryFk = this.categoryId;
if (this.typeId)
newFilter.typeFk = this.typeId;
newParams = {
orderFk: this.$params.id,
orderBy: this.getOrderBy(),
tagGroups: this.tagGroups,
};
return model.applyFilter({where: newFilter}, newParams);
}
openPanel(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.panelFilter = {};
this.$.popover.show(this.$.search.element);
}
onPanelSubmit(filter) {
this.$.popover.hide();
const values = filter.values;
const nonEmptyValues = values.filter(tagValue => {
return tagValue.value;
});
filter.values = nonEmptyValues;
if (filter.tagFk && nonEmptyValues.length) {
this.tagGroups.push(filter);
this.updateStateParams();
this.applyFilters();
}
}
/**
* Updates url state params from filter values
*/
updateStateParams() {
const params = {};
params.categoryId = undefined;
if (this.categoryId)
params.categoryId = this.categoryId;
params.typeId = undefined;
if (this.typeId)
params.typeId = this.typeId;
params.tagGroups = undefined;
if (this.tagGroups && this.tagGroups.length)
params.tagGroups = JSON.stringify(this.sanitizedTagGroupParam());
this.$state.go(this.$state.current.name, params);
}
sanitizedTagGroupParam() {
const tagGroups = [];
for (let tagGroup of this.tagGroups) {
const tagParam = {values: []};
for (let tagValue of tagGroup.values)
tagParam.values.push({value: tagValue.value});
if (tagGroup.tagFk)
tagParam.tagFk = tagGroup.tagFk;
if (tagGroup.tagSelection) {
tagParam.tagSelection = {
name: tagGroup.tagSelection.name
};
}
tagGroups.push(tagParam);
}
return tagGroups;
}
fetchResultTags(items) {
const resultTags = [];
for (let item of items) {
for (let itemTag of item.tags) {
const alreadyAdded = resultTags.findIndex(tag => {
return tag.tagFk == itemTag.tagFk;
});
if (alreadyAdded == -1)
resultTags.push({...itemTag, priority: 1});
else
resultTags[alreadyAdded].priority += 1;
}
}
this.resultTags = resultTags;
}
buildOrderFilter() {
const filter = [].concat(this.defaultOrderFields);
for (let tag of this.resultTags)
filter.push({...tag, field: tag.id, isTag: true});
this.orderFields = filter;
}
onSearch(params) {
if (!params) return;
this.itemId = null;
this.itemName = null;
if (params.search) {
if (/^\d+$/.test(params.search)) {
this.itemId = params.search;
return this.applyFilters({
'i.id': params.search
});
} else {
this.itemName = params.search;
return this.applyFilters({
'i.name': {like: `%${params.search}%`}
});
}
} else return this.applyFilters();
}
formatTooltip(tagGroup) {
const tagValues = tagGroup.values;
let title = '';
if (tagGroup.tagFk) {
const tagName = tagGroup.tagSelection.name;
title += `${tagName}: `;
}
for (let [i, tagValue] of tagValues.entries()) {
if (i > 0) title += ', ';
title += `"${tagValue.value}"`;
}
return `${title}`;
}
}
ngModule.vnComponent('vnOrderCatalog', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,386 +0,0 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Order', () => {
describe('Component vnOrderCatalog', () => {
let $scope;
let $state;
let controller;
let $httpBackend;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$state_, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.model = crudModel;
$scope.search = {};
$scope.itemId = {};
$state = _$state_;
$state.current.name = 'my.current.state';
const $element = angular.element('<vn-order-catalog></vn-order-catalog>');
controller = $componentController('vnOrderCatalog', {$element, $scope});
controller._order = {id: 4};
controller.$params = {
categoryId: 1,
typeId: 2,
id: 4
};
}));
describe('getData()', () => {
it(`should make a query an fetch the order data`, () => {
controller._order = null;
$httpBackend.expect('GET', `Orders/4`).respond(200, {id: 4, isConfirmed: true});
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.getData();
$httpBackend.flush();
const order = controller.order;
expect(order.id).toEqual(4);
expect(order.isConfirmed).toBeTruthy();
});
});
describe('order() setter', () => {
it(`should call scope $applyAsync() method and apply filters from state params`, () => {
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.order = {id: 4};
$scope.$apply();
expect(controller.categoryId).toEqual(1);
expect(controller.typeId).toEqual(2);
});
});
describe('items() setter', () => {
it(`should return an object with order params`, () => {
jest.spyOn(controller, 'fetchResultTags');
jest.spyOn(controller, 'buildOrderFilter');
const expectedResult = [{field: 'showOrder, price', name: 'Color and price', priority: 999}];
const items = [{id: 1, name: 'My Item', tags: [
{tagFk: 4, name: 'Length'},
{tagFk: 5, name: 'Color'}
]}];
controller.items = items;
expect(controller.orderFields.length).toEqual(6);
expect(controller.orderFields).toEqual(jasmine.arrayContaining(expectedResult));
expect(controller.fetchResultTags).toHaveBeenCalledWith(items);
expect(controller.buildOrderFilter).toHaveBeenCalledWith();
});
});
describe('categoryId() setter', () => {
it(`should set category property to null, call updateStateParams() method and not call applyFilters()`, () => {
jest.spyOn(controller, 'updateStateParams');
controller.categoryId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
jest.spyOn(controller, 'updateStateParams');
controller.categoryId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
});
describe('changeCategory()', () => {
it(`should set categoryId property to null if the new value equals to the old one`, () => {
controller.categoryId = 2;
controller.changeCategory(2);
expect(controller.categoryId).toBeNull();
});
it(`should set categoryId property`, () => {
controller.categoryId = 2;
controller.changeCategory(1);
expect(controller.categoryId).toEqual(1);
});
});
describe('typeId() setter', () => {
it(`should set type property to null, call updateStateParams() method and not call applyFilters()`, () => {
jest.spyOn(controller, 'updateStateParams');
jest.spyOn(controller, 'applyFilters');
controller.typeId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
jest.spyOn(controller, 'updateStateParams');
jest.spyOn(controller, 'applyFilters');
controller.typeId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('tagGroups() setter', () => {
it(`should set tagGroups property and then call updateStateParams() and applyFilters() methods`, () => {
jest.spyOn(controller, 'updateStateParams');
jest.spyOn(controller, 'applyFilters');
controller.tagGroups = [{tagFk: 11, values: [{value: 'Brown'}]}];
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearchByTag()', () => {
it(`should not add a new tag if the event key code doesn't equals to 'Enter'`, () => {
jest.spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.$.search.value = 'Brown';
controller.onSearchByTag({key: 'Tab'});
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should add a new tag if the event key code equals to 'Enter' an then call applyFilters()`, () => {
jest.spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.$.search.value = 'Brown';
controller.onSearchByTag({key: 'Enter'});
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearch()', () => {
it(`should apply a filter by item id an then call the applyFilters method`, () => {
jest.spyOn(controller, 'applyFilters');
const itemId = 1;
controller.onSearch({search: itemId});
expect(controller.applyFilters).toHaveBeenCalledWith({
'i.id': itemId
});
});
it(`should apply a filter by item name an then call the applyFilters method`, () => {
jest.spyOn(controller, 'applyFilters');
const itemName = 'Bow';
controller.onSearch({search: itemName});
expect(controller.applyFilters).toHaveBeenCalledWith({
'i.name': {like: `%${itemName}%`}
});
});
});
describe('applyFilters()', () => {
it(`should call model applyFilter() method with a new filter`, () => {
jest.spyOn(controller.$.model, 'applyFilter');
controller._categoryId = 2;
controller._typeId = 4;
controller.applyFilters();
expect(controller.$.model.applyFilter).toHaveBeenCalledWith(
{where: {categoryFk: 2, typeFk: 4}},
{orderFk: 4, orderBy: controller.getOrderBy(), tagGroups: []});
});
});
describe('remove()', () => {
it(`should remove a tag from tags property`, () => {
jest.spyOn(controller, 'applyFilters');
controller.tagGroups = [
{tagFk: 1, values: [{value: 'Brown'}]},
{tagFk: 67, values: [{value: 'Concussion'}]}
];
controller.remove(0);
const firstTag = controller.tagGroups[0];
expect(controller.tagGroups.length).toEqual(1);
expect(firstTag.tagFk).toEqual(67);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
it(`should remove a tag from tags property and call applyFilters() if there's no more tags`, () => {
jest.spyOn(controller, 'applyFilters');
controller._categoryId = 1;
controller._typeId = 1;
controller.tagGroups = [{tagFk: 1, values: [{value: 'Blue'}]}];
controller.remove(0);
expect(controller.tagGroups.length).toEqual(0);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('updateStateParams()', () => {
it(`should call state go() method passing category and type state params`, () => {
jest.spyOn(controller.$state, 'go');
controller._categoryId = 2;
controller._typeId = 4;
controller._tagGroups = [
{tagFk: 67, values: [{value: 'Concussion'}], tagSelection: {name: 'Category'}}
];
const tagGroups = JSON.stringify([
{values: [{value: 'Concussion'}], tagFk: 67, tagSelection: {name: 'Category'}}
]);
const expectedResult = {categoryId: 2, typeId: 4, tagGroups: tagGroups};
controller.updateStateParams();
expect(controller.$state.go).toHaveBeenCalledWith('my.current.state', expectedResult);
});
});
describe('getOrderBy()', () => {
it(`should return an object with order params`, () => {
controller.orderField = 'relevancy DESC, name';
controller.orderWay = 'DESC';
let expectedResult = {
field: 'relevancy DESC, name',
way: 'DESC',
isTag: false
};
let result = controller.getOrderBy();
expect(result).toEqual(expectedResult);
});
});
describe('applyOrder()', () => {
it(`should apply order param to model calling getOrderBy()`, () => {
jest.spyOn(controller, 'getOrderBy');
jest.spyOn(controller.$.model, 'addFilter');
controller.field = 'relevancy DESC, name';
controller.way = 'ASC';
controller._categoryId = 1;
controller._typeId = 1;
let expectedOrder = {orderBy: controller.getOrderBy()};
controller.applyOrder();
expect(controller.getOrderBy).toHaveBeenCalledWith();
expect(controller.$.model.addFilter).toHaveBeenCalledWith(null, expectedOrder);
});
});
describe('fetchResultTags()', () => {
it(`should create an array of non repeated tags then set the resultTags property`, () => {
const items = [
{
id: 1, name: 'My Item 1', tags: [
{tagFk: 4, name: 'Length', value: 1},
{tagFk: 5, name: 'Color', value: 'red'}
]
},
{
id: 2, name: 'My Item 2', tags: [
{tagFk: 4, name: 'Length', value: 1},
{tagFk: 5, name: 'Color', value: 'blue'}
]
}];
controller.fetchResultTags(items);
expect(controller.resultTags.length).toEqual(2);
});
});
describe('buildOrderFilter()', () => {
it(`should create an array of non repeated tags plus default filters and then set the orderFields property`, () => {
const items = [
{
id: 1, name: 'My Item 1', tags: [
{tagFk: 4, name: 'Length'},
{tagFk: 5, name: 'Color'}
]
},
{
id: 2, name: 'My Item 2', tags: [
{tagFk: 5, name: 'Color'},
{tagFk: 6, name: 'Relevancy'}
]
}];
controller.fetchResultTags(items);
controller.buildOrderFilter();
expect(controller.orderFields.length).toEqual(7);
});
});
describe('formatTooltip()', () => {
it(`should return a formatted text with the tag name and values`, () => {
const tagGroup = {
values: [{value: 'Silver'}, {value: 'Brown'}],
tagFk: 1,
tagSelection: {
name: 'Color'
}
};
const result = controller.formatTooltip(tagGroup);
expect(result).toEqual(`Color: "Silver", "Brown"`);
});
it(`should return a formatted text with the tag value`, () => {
const tagGroup = {
values: [{value: 'Silver'}]
};
const result = controller.formatTooltip(tagGroup);
expect(result).toEqual(`"Silver"`);
});
});
describe('sanitizedTagGroupParam()', () => {
it(`should return an array of tags`, () => {
const dirtyTagGroups = [{
values: [{value: 'Silver'}, {value: 'Brown'}],
tagFk: 1,
tagSelection: {
name: 'Color',
$orgRow: {name: 'Color'}
},
$orgIndex: 1
}];
controller.tagGroups = dirtyTagGroups;
const expectedResult = [{
values: [{value: 'Silver'}, {value: 'Brown'}],
tagFk: 1,
tagSelection: {
name: 'Color'
}
}];
const result = controller.sanitizedTagGroupParam();
expect(result).toEqual(expect.objectContaining(expectedResult));
});
});
});
});

View File

@ -1,3 +0,0 @@
Name: Nombre
Search by item id or name: Buscar por id de artículo o nombre
OR: O

View File

@ -1,54 +0,0 @@
@import "variables";
vn-order-catalog vn-side-menu div {
& > .input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin;
}
.item-category {
padding: $spacing-sm;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
vn-autocomplete[vn-id="category"] {
display: none
}
& > vn-one {
padding: $spacing-sm;
min-width: 33.33%;
text-align: center;
box-sizing: border-box;
& > vn-icon {
padding: $spacing-sm;
background-color: $color-font-secondary;
border-radius: 50%;
cursor: pointer;
&.active {
background-color: $color-main;
color: #FFF
}
& > i:before {
font-size: 2.6rem;
width: 16px;
height: 16px;
}
}
}
}
.chips {
display: flex;
flex-wrap: wrap;
padding: $spacing-md;
overflow: hidden;
max-width: 100%;
}
vn-autocomplete[vn-id="type"] .list {
max-height: 320px
}
}

View File

@ -1,38 +0,0 @@
<vn-autocomplete
vn-focus
vn-id="client"
url="Clients"
label="Client"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
show-field="name"
value-field="id"
ng-model="$ctrl.clientFk"
vn-name="client"
order="id">
<tpl-item>{{id}}: {{name}}</tpl-item>
</vn-autocomplete>
<vn-autocomplete
disabled="!$ctrl.clientFk"
url="{{ $ctrl.clientFk ? 'Clients/'+ $ctrl.clientFk +'/addresses' : null }}"
fields="['nickname', 'street', 'city']"
ng-model="$ctrl.addressFk"
vn-name="address"
show-field="nickname"
value-field="id"
label="Address">
<tpl-item>{{nickname}}: {{street}}, {{city}}</tpl-item>
</vn-autocomplete>
<vn-date-picker
label="Landed"
ng-model="$ctrl.landed"
vn-name="landed">
</vn-date-picker>
<vn-autocomplete
disabled="!$ctrl.clientFk || !$ctrl.landed"
data="$ctrl._availableAgencies"
label="Agency"
show-field="agencyMode"
value-field="agencyModeFk"
ng-model="$ctrl.order.agencyModeFk"
vn-name="agencyMode">
</vn-autocomplete>

View File

@ -1,114 +0,0 @@
import ngModule from '../module';
import Component from 'core/lib/component';
class Controller extends Component {
constructor($element, $) {
super($element, $);
this.order = {};
this.clientFk = this.$params.clientFk;
}
$onInit() {
if (this.$params && this.$params.clientFk)
this.clientFk = this.$params.clientFk;
}
set order(value) {
if (value)
this._order = value;
}
get order() {
return this._order;
}
set clientFk(value) {
this.order.clientFk = value;
if (value) {
let filter = {
include: {
relation: 'defaultAddress',
scope: {
fields: 'id'
}
},
where: {id: value}
};
filter = encodeURIComponent(JSON.stringify(filter));
let query = `Clients?filter=${filter}`;
this.$http.get(query).then(res => {
if (res.data) {
let client = res.data[0];
let defaultAddress = client.defaultAddress;
this.addressFk = defaultAddress.id;
}
});
} else
this.addressFk = null;
}
get clientFk() {
return this.order.clientFk;
}
set addressFk(value) {
this.order.addressFk = value;
this.getAvailableAgencies();
}
get addressFk() {
return this.order.addressFk;
}
set landed(value) {
this.order.landed = value;
this.getAvailableAgencies();
}
get landed() {
return this.order.landed;
}
get warehouseFk() {
return this.order.warehouseFk;
}
getAvailableAgencies() {
let order = this.order;
order.agencyModeFk = null;
let params = {
addressFk: order.addressFk,
landed: order.landed
};
if (params.landed && params.addressFk) {
this.$http.get(`Agencies/landsThatDay`, {params})
.then(res => this._availableAgencies = res.data);
}
}
onSubmit() {
this.createOrder();
}
createOrder() {
let params = {
landed: this.order.landed,
addressId: this.order.addressFk,
agencyModeId: this.order.agencyModeFk
};
this.$http.post(`Orders/new`, params).then(res => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$state.go('order.card.catalog', {id: res.data});
});
}
}
ngModule.vnComponent('vnOrderCreateCard', {
template: require('./card.html'),
controller: Controller,
bindings: {
order: '<?'
}
});

View File

@ -1,104 +0,0 @@
import './card.js';
describe('Order', () => {
describe('Component vnOrderCreateCard', () => {
let controller;
let $httpBackend;
let $scope;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_, _vnApp_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
const $element = angular.element('<vn-order-create-card></vn-order-create-card>');
controller = $componentController('vnOrderCreateCard', {$element, $scope});
controller.item = {id: 3};
}));
describe('set order', () => {
it(`should set order if the value given is not null`, () => {
controller.order = 1;
expect(controller.order).toEqual(1);
});
});
describe('set clientFk', () => {
it(`should set addressFk to null and clientFk to a value and set addressFk to a value given`, () => {
let filter = {
include: {
relation: 'defaultAddress',
scope: {
fields: 'id'
}
},
where: {id: 2}
};
filter = encodeURIComponent(JSON.stringify(filter));
let response = [
{
defaultAddress: {id: 1}
}
];
$httpBackend.whenGET(`Clients?filter=${filter}`).respond(response);
$httpBackend.expectGET(`Clients?filter=${filter}`);
controller.clientFk = 2;
$httpBackend.flush();
expect(controller.clientFk).toEqual(2);
expect(controller.order.addressFk).toBe(1);
});
});
describe('set addressFk', () => {
it(`should set agencyModeFk property to null and addressFk to a value`, () => {
controller.addressFk = 1101;
expect(controller.addressFk).toEqual(1101);
expect(controller.order.agencyModeFk).toBe(null);
});
});
describe('getAvailableAgencies()', () => {
it(`should make a query if landed and addressFk exists`, () => {
controller.order.addressFk = 1101;
controller.order.landed = 1101;
$httpBackend.whenRoute('GET', 'Agencies/landsThatDay')
.respond({data: 1});
controller.getAvailableAgencies();
$httpBackend.flush();
});
});
describe('onSubmit()', () => {
it(`should call createOrder()`, () => {
jest.spyOn(controller, 'createOrder');
controller.onSubmit();
expect(controller.createOrder).toHaveBeenCalledWith();
});
});
describe('createOrder()', () => {
it(`should make a query, call vnApp.showSuccess and $state.go if the response is defined`, () => {
controller.order.landed = 1101;
controller.order.addressFk = 1101;
controller.order.agencyModeFk = 1101;
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$state, 'go');
$httpBackend.expect('POST', 'Orders/new', {landed: 1101, addressId: 1101, agencyModeId: 1101}).respond(200, 1);
controller.createOrder();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalledWith('order.card.catalog', {id: 1});
});
});
});
});

View File

@ -1,16 +0,0 @@
<div class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-order-create-card vn-id="card" on-save=""></vn-order-create-card>
</vn-card>
<vn-button-bar>
<vn-submit
ng-click="$ctrl.onSubmit()"
label="Create">
</vn-submit>
<vn-button
class="cancel"
label="Cancel"
ui-sref="order.index">
</vn-button>
</vn-button-bar>
</div>

View File

@ -1,14 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
async onSubmit() {
let newOrderID = await this.$.card.createOrder();
this.$state.go('order.card.summary', {id: newOrderID});
}
}
ngModule.vnComponent('vnOrderCreate', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,34 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderCreate', () => {
let $scope;
let controller;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$scope.card = {createOrder: () => {}};
const $element = angular.element('<vn-order-create></vn-order-create>');
controller = $componentController('vnOrderCreate', {$element, $scope});
}));
describe('onSubmit()', () => {
it(`should call createOrder()`, () => {
jest.spyOn(controller.$.card, 'createOrder');
controller.onSubmit();
expect(controller.$.card.createOrder).toHaveBeenCalledWith();
});
it(`should call go()`, async() => {
jest.spyOn(controller.$state, 'go');
await controller.onSubmit();
expect(controller.$state.go).toHaveBeenCalledWith('order.card.summary', {id: undefined});
});
});
});
});

View File

@ -1,6 +0,0 @@
You can't create an order for a frozen client: No puedes crear una orden a un cliente congelado
You can't create an order for an inactive client: No puedes crear una orden a un cliente inactivo
You can't create an order for a client that doesn't has tax data verified:
No puedes crear una orden a un cliente cuyos datos fiscales no han sido verificados
You can't create an order for a client that has a debt: No puedes crear una orden a un cliente que tiene deuda
New order: Nueva orden

View File

@ -1,78 +0,0 @@
<vn-descriptor-content
module="order"
description="$ctrl.order.client.name"
summary="$ctrl.$.summary">
<slot-menu>
<vn-item
ng-click="deleteOrderConfirmation.show()"
translate>
Delete order
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
<vn-label-value
label="State"
value="{{$ctrl.$t($ctrl.order.isConfirmed ? 'Confirmed' : 'Not confirmed')}}">
</vn-label-value>
<vn-label-value
label="Sales person">
<span
ng-click="workerDescriptor.show($event, $ctrl.order.client.salesPersonFk)"
class="link">
{{$ctrl.order.client.salesPersonUser.name}}
</span>
</vn-label-value>
<vn-label-value
label="Landed"
value="{{$ctrl.order.landed | date: 'dd/MM/yyyy' }}">
</vn-label-value>
<vn-label-value
label="Agency"
value="{{$ctrl.order.agencyMode.name}}">
</vn-label-value>
<vn-label-value
label="Alias"
value="{{$ctrl.order.address.nickname}}">
</vn-label-value>
<vn-label-value
label="Items"
value="{{$ctrl.order.rows.length || 0}}">
</vn-label-value>
<vn-label-value
label="Total"
value="{{$ctrl.order.total | currency: 'EUR': 2}}">
</vn-label-value>
</div>
<div class="quicklinks">
<div ng-transclude="btnOne">
<vn-quick-link
tooltip="Order ticket list"
state="['ticket.index', {q: $ctrl.ticketFilter}]"
icon="icon-ticket">
</vn-quick-link>
</div>
<div ng-transclude="btnTwo">
<vn-quick-link
tooltip="Client card"
state="['client.card.summary', {id: $ctrl.order.clientFk}]"
icon="person">
</vn-quick-link>
</div>
<div ng-transclude="btnThree">
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-confirm
vn-id="deleteOrderConfirmation"
on-accept="$ctrl.deleteOrder()"
message="You are going to delete this order"
question="continue anyway?">
</vn-confirm>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="summary">
<vn-order-summary order="$ctrl.order"></vn-order-summary>
</vn-popup>

View File

@ -1,32 +0,0 @@
import ngModule from '../module';
import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor {
get order() {
return this.entity;
}
set order(value) {
this.entity = value;
}
get ticketFilter() {
return JSON.stringify({orderFk: this.id});
}
deleteOrder() {
return this.$http.delete(`Orders/${this.id}`)
.then(() => {
this.$state.go('order.index');
this.vnApp.showSuccess(this.$t('Order deleted'));
});
}
}
ngModule.vnComponent('vnOrderDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
});

View File

@ -1,29 +0,0 @@
import './index.js';
describe('Order Component vnOrderDescriptor', () => {
let $httpBackend;
let controller;
const order = {id: 1};
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
controller = $componentController('vnOrderDescriptor', {$element: null}, {order});
}));
describe('deleteOrder()', () => {
it(`should perform a DELETE query`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$state, 'go');
$httpBackend.expectDELETE(`Orders/${order.id}`).respond();
controller.deleteOrder();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalledWith('order.index');
});
});
});

View File

@ -1,12 +0,0 @@
Client: Cliente
Confirmed: Confirmado
Not confirmed: Sin confirmar
State: Estado
Landed: F. entrega
Items: Articulos
Agency: Agencia
Sales person: Comercial
Order ticket list: Ticket del pedido
Delete order: Eliminar pedido
You are going to delete this order: El pedido se eliminará
continue anyway?: ¿Continuar de todos modos?

View File

@ -1,17 +1,3 @@
export * from './module';
import './main';
import './index/';
import './card';
import './descriptor';
import './search-panel';
import './catalog-search-panel';
import './catalog-view';
import './catalog';
import './summary';
import './line';
import './prices-popover';
import './volume';
import './create';
import './create/card';
import './basic-data';

View File

@ -1,90 +0,0 @@
<vn-auto-search
model="model">
</vn-auto-search>
<vn-data-viewer
model="model"
class="vn-mb-xl">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="id" number>Id</vn-th>
<vn-th field="salesPersonFk">Sales person</vn-th>
<vn-th field="clientFk">Client</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th>
<vn-th field="created" center expand>Created</vn-th>
<vn-th field="landed" shrink-date>Landed</vn-th>
<vn-th field="created" center>Hour</vn-th>
<vn-th field="agencyName" center>Agency</vn-th>
<vn-th field="total" center>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a
ng-repeat="order in model.data"
class="clickable search-result vn-tr"
ui-sref="order.card.summary({id: {{::order.id}}})">
<vn-td number>{{::order.id}}</vn-td>
<vn-td expand>
<span
vn-click-stop="workerDescriptor.show($event, order.salesPersonFk)"
class="link" >
{{::order.name | dashIfEmpty}}
</span>
</vn-td>
<vn-td>
<span
vn-click-stop="clientDescriptor.show($event, order.clientFk)"
class="link">
{{::order.clientName}}
</span>
</vn-td>
<vn-td center>
<vn-check
ng-model="order.isConfirmed"
disabled="true">
</vn-check>
</vn-td>
<vn-td shrink-datetime>{{::order.created | date: 'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td shrink-date>
<span class="chip {{$ctrl.compareDate(order.landed)}}">
{{::order.landed | date:'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td shrink>{{::(order.hourTheoretical
? order.hourTheoretical
: order.hourEffective) | dashIfEmpty
}}</vn-td>
<vn-td expand>{{::order.agencyName}}</vn-td>
<vn-td number>{{::order.total | currency: 'EUR': 2 | dashIfEmpty}}</vn-td>
<vn-td shrink>
<vn-icon-button
vn-click-stop="$ctrl.preview(order)"
icon="preview"
vn-tooltip="Preview">
</vn-icon-button>
</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<a
ui-sref="order.create"
vn-bind="+"
vn-tooltip="New order"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="summary">
<vn-order-summary
order="$ctrl.selectedOrder">
</vn-order-summary>
</vn-popup>

View File

@ -1,26 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
preview(order) {
this.selectedOrder = order;
this.$.summary.show();
}
compareDate(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
date = new Date(date);
date.setHours(0, 0, 0, 0);
const timeDifference = today - date;
if (timeDifference == 0) return 'warning';
if (timeDifference < 0) return 'success';
}
}
ngModule.vnComponent('vnOrderIndex', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,67 +0,0 @@
import './index.js';
describe('Component vnOrderIndex', () => {
let controller;
let $window;
let orders = [{
id: 1,
clientFk: 1,
isConfirmed: false
}, {
id: 2,
clientFk: 1,
isConfirmed: false
}, {
id: 3,
clientFk: 1,
isConfirmed: true
}];
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$window_) => {
$window = _$window_;
const $element = angular.element('<vn-order-index></vn-order-index>');
controller = $componentController('vnOrderIndex', {$element});
}));
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let curDate = Date.vnNew();
let result = controller.compareDate(curDate);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = Date.vnNew();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = Date.vnNew();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('preview()', () => {
it('should show the dialog summary', () => {
controller.$.summary = {show: () => {}};
jest.spyOn(controller.$.summary, 'show');
let event = new MouseEvent('click', {
view: $window,
bubbles: true,
cancelable: true
});
controller.preview(event, orders[0]);
expect(controller.$.summary.show).toHaveBeenCalledWith();
});
});
});

View File

@ -1,96 +0,0 @@
<vn-data-viewer data="$ctrl.rows" class="vn-w-lg">
<vn-card class="vn-pa-lg header" ng-if="$ctrl.rows.length > 0">
<div>
<vn-label translate>Subtotal</vn-label>
{{$ctrl.subtotal | currency: 'EUR':2}}
</div>
<div>
<vn-label translate>VAT</vn-label>
{{$ctrl.VAT | currency: 'EUR':2}}
</div>
<div>
<vn-label>Total</vn-label>
{{$ctrl.order.total | currency: 'EUR':2}}
</div>
</vn-card>
<vn-card class="vn-mt-md">
<vn-table>
<vn-thead>
<vn-tr>
<vn-th></vn-th>
<vn-th number>Id</vn-th>
<vn-th>Description</vn-th>
<vn-th>Warehouse</vn-th>
<vn-th>Shipped</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Amount</vn-th>
<vn-th ng-if="::!$ctrl.order.isConfirmed"></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in $ctrl.rows">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', row.item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', row.item.id)}}"
on-error-src/>
</vn-td>
<vn-td number>
<span ng-click="itemDescriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td vn-fetched-tags>
<div>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::row.item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td>{{::row.warehouse.name}}</vn-td>
<vn-td>{{::row.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>
<vn-td number>
{{::row.price | currency: 'EUR':2}}
</vn-td>
<vn-td number>
{{::row.price * row.quantity | currency: 'EUR':2}}
</vn-td>
<vn-td shrink ng-if="::!$ctrl.order.isConfirmed">
<vn-icon-button
vn-tooltip="Remove item"
icon="delete"
ng-click="deleteRow.show($index)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-float-button
icon="check"
vn-tooltip="Confirm"
ng-click="$ctrl.save()"
ng-if="!$ctrl.order.isConfirmed"
fixed-bottom-right>
</vn-float-button>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
<vn-confirm
vn-id="delete-row"
on-accept="$ctrl.deleteRow($data)"
question="Delete row"
message="Are you sure you want to delete this row?">
</vn-confirm>

View File

@ -1,70 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
$onInit() {
this.getRows();
}
set order(value) {
this._order = value;
this.getVAT();
}
get order() {
return this._order;
}
get subtotal() {
return this.order ? this.order.total - this.VAT : 0;
}
getRows() {
let filter = {
where: {orderFk: this.$params.id},
include: [
{relation: 'item'},
{relation: 'warehouse'}
]
};
this.$http.get(`OrderRows`, {filter})
.then(res => this.rows = res.data);
}
getVAT() {
this.$http.get(`Orders/${this.$params.id}/getVAT`)
.then(res => this.VAT = res.data);
}
deleteRow(index) {
let [row] = this.rows.splice(index, 1);
let params = {
rows: [row.id],
actualOrderId: this.$params.id
};
return this.$http.post(`OrderRows/removes`, params)
.then(() => this.card.reload())
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
save() {
this.$http.post(`Orders/${this.$params.id}/confirm`).then(() => {
this.vnApp.showSuccess(this.$t('Order confirmed'));
this.$state.go(`ticket.index`, {
q: JSON.stringify({clientFk: this.order.clientFk})
});
});
}
}
ngModule.vnComponent('vnOrderLine', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
},
require: {
card: '^vnOrderCard'
}
});

View File

@ -1,66 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderLine', () => {
let $state;
let controller;
let $httpBackend;
const vat = 10.5;
const rows = [
{
quantity: 4,
price: 10.5
}, {
quantity: 3,
price: 2.4
}
];
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$state_, _$httpBackend_) => {
$state = _$state_;
$httpBackend = _$httpBackend_;
$state.params.id = 1;
$httpBackend.whenGET(`OrderRows`).respond(rows);
$httpBackend.whenRoute('GET', `Orders/:id/getVAT`).respond(200, vat);
controller = $componentController('vnOrderLine', {$element: null});
}));
describe('getRows()', () => {
it('should make a query to get the rows of a given order', () => {
controller.getRows();
$httpBackend.flush();
expect(controller.rows).toEqual(rows);
});
});
describe('getVAT()', () => {
it('should make a query to get the VAT of a given order', () => {
controller.getVAT();
$httpBackend.flush();
expect(controller.VAT).toBe(vat);
});
});
describe('deleteRow()', () => {
it('should remove a row from rows and add save the data if the response is accept', () => {
controller.getRows();
$httpBackend.flush();
controller.card = {reload: jasmine.createSpy('reload')};
$httpBackend.expectPOST(`OrderRows/removes`).respond();
controller.deleteRow(0);
$httpBackend.flush();
expect(controller.rows.length).toBe(1);
expect(controller.card.reload).toHaveBeenCalled();
});
});
});
});

View File

@ -1,3 +0,0 @@
Delete row: Eliminar linea
Order confirmed: Pedido confirmado
Are you sure you want to delete this row?: ¿Estas seguro de que quieres eliminar esta línea?

View File

@ -1,18 +0,0 @@
@import "./variables";
vn-order-line {
vn-table {
img {
border-radius: 50%;
width: 50px;
height: 50px;
}
}
.header {
text-align: right;
& > div {
margin-bottom: $spacing-xs;
}
}
}

View File

@ -2,8 +2,12 @@ import ngModule from '../module';
import ModuleMain from 'salix/components/module-main';
export default class Order extends ModuleMain {
$postLink() {
this.filter = {showEmpty: false};
constructor($element, $) {
super($element, $);
}
async $onInit() {
this.$state.go('home');
window.location.href = await this.vnApp.getUrl(`order/`);
}
}

View File

@ -1,52 +0,0 @@
<default>
<form name="form" class="prices">
<vn-table>
<vn-tbody>
<vn-tr ng-repeat="price in $ctrl.prices">
<vn-td class="warehouse" expand>
<span
class="text"
title="{{::price.warehouse}}">
{{::price.warehouse}}
</span>
</vn-td>
<vn-td number expand>
<div>
<span
ng-click="$ctrl.addQuantity(price)"
class="link unselectable">{{::price.grouping}}</span>
<span> x {{::price.price | currency: 'EUR': 2}}</span>
</div>
<div class="price-kg" ng-show="::price.priceKg">
{{::price.priceKg | currency: 'EUR'}}/Kg
</div>
</vn-td>
<vn-td shrink>
<!-- Focus first element -->
<vn-input-number ng-if="$index === 0"
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
class="dense"
vn-focus>
</vn-input-number>
<vn-input-number ng-if="$index > 0"
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
class="dense">
</vn-input-number>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div class="footer vn-pa-md">
<vn-submit
label="Add"
ng-click="$ctrl.submit()">
</vn-submit>
</div>
</form>
</default>

View File

@ -1,115 +0,0 @@
import ngModule from '../module';
import Popover from 'core/components/popover';
import './style.scss';
class Controller extends Popover {
constructor(...args) {
super(...args);
this.totalBasquet = 0;
}
set prices(value) {
this._prices = value;
if (value && value[0].grouping)
this.getTotalQuantity();
}
get prices() {
return this._prices;
}
show(parent, item) {
this.id = item.id;
this.item = JSON.parse(JSON.stringify(item));
this.maxQuantity = this.item.available;
this.prices = this.item.prices;
super.show(parent);
}
onClose() {
this.id = null;
this.item = {};
this.tags = {};
this._prices = {};
this.totalQuantity = 0;
super.onClose();
}
getTotalQuantity() {
let total = 0;
for (let price of this.prices) {
if (!price.quantity) price.quantity = 0;
total += price.quantity;
}
this.totalQuantity = total;
}
addQuantity(price) {
this.getTotalQuantity();
const quantity = this.totalQuantity + price.grouping;
if (quantity <= this.maxQuantity)
price.quantity += price.grouping;
}
getGroupings() {
const filledRows = [];
for (let priceOption of this.prices) {
if (priceOption.quantity && priceOption.quantity > 0) {
const priceMatch = filledRows.find(row => {
return row.warehouseFk == priceOption.warehouseFk
&& row.price == priceOption.price;
});
if (!priceMatch)
filledRows.push(Object.assign({}, priceOption));
else priceMatch.quantity += priceOption.quantity;
}
}
return filledRows;
}
submit() {
const filledRows = this.getGroupings();
try {
const hasInvalidGropings = filledRows.some(row =>
row.quantity % row.grouping != 0
);
if (filledRows.length <= 0)
throw new Error('First you must add some quantity');
if (hasInvalidGropings)
throw new Error(`The amounts doesn't match with the grouping`);
const params = {
orderFk: this.order.id,
items: filledRows
};
this.$http.post(`OrderRows/addToOrder`, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.hide();
if (this.card) this.card.reload();
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
}
ngModule.vnComponent('vnOrderPricesPopover', {
slotTemplate: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
},
require: {
card: '?^vnOrderCard'
}
});

View File

@ -1,171 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderPricesPopover', () => {
let controller;
let $httpBackend;
let orderId = 16;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $scope = $rootScope.$new();
const $element = angular.element('<vn-order-prices-popover></vn-order-prices-popover>');
const $transclude = {
$$boundTransclude: {
$$slots: []
}
};
controller = $componentController('vnOrderPricesPopover', {$element, $scope, $transclude});
controller._prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 100}
];
controller.item = {available: 1000};
controller.maxQuantity = 1000;
controller.order = {id: orderId};
}));
describe('prices() setter', () => {
it('should call to the getTotalQuantity() method', () => {
controller.getTotalQuantity = jest.fn();
controller.prices = [
{grouping: 10, quantity: 0},
{grouping: 100, quantity: 0},
{grouping: 1000, quantity: 0},
];
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
});
});
describe('getTotalQuantity()', () => {
it('should set the totalQuantity property', () => {
controller.getTotalQuantity();
expect(controller.totalQuantity).toEqual(100);
});
});
describe('addQuantity()', () => {
it('should call to the getTotalQuantity() method and NOT set the quantity property', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{grouping: 10, quantity: 0},
{grouping: 100, quantity: 0},
{grouping: 1000, quantity: 1000},
];
const oneThousandGrouping = controller.prices[2];
expect(oneThousandGrouping.quantity).toEqual(1000);
controller.addQuantity(oneThousandGrouping);
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
expect(oneThousandGrouping.quantity).toEqual(1000);
});
it('should call to the getTotalQuantity() method and then set the quantity property', () => {
jest.spyOn(controller, 'getTotalQuantity');
const oneHandredGrouping = controller.prices[1];
controller.addQuantity(oneHandredGrouping);
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
expect(oneHandredGrouping.quantity).toEqual(200);
});
});
describe('getGroupings()', () => {
it('should return a row with the total filled quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 10},
{warehouseFk: 1, grouping: 100, quantity: 100},
{warehouseFk: 1, grouping: 1000, quantity: 1000},
];
const rows = controller.getGroupings();
const firstRow = rows[0];
expect(rows.length).toEqual(1);
expect(firstRow.quantity).toEqual(1110);
});
it('should return two filled rows with a quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 10},
{warehouseFk: 2, grouping: 10, quantity: 10},
{warehouseFk: 1, grouping: 100, quantity: 0},
{warehouseFk: 1, grouping: 1000, quantity: 1000},
];
const rows = controller.getGroupings();
const firstRow = rows[0];
const secondRow = rows[1];
expect(rows.length).toEqual(2);
expect(firstRow.quantity).toEqual(1010);
expect(secondRow.quantity).toEqual(10);
});
});
describe('submit()', () => {
it('should throw an error if none of the rows contains a quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showError');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 0}
];
controller.submit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`First you must add some quantity`);
});
it(`should throw an error if the quantity doesn't match the grouping value`, () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showError');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 1101}
];
controller.submit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The amounts doesn't match with the grouping`);
});
it('should should make an http query and then show a success message', () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 100}
];
const params = {
orderFk: orderId,
items: [{warehouseFk: 1, grouping: 100, quantity: 100}]
};
$httpBackend.expectPOST('OrderRows/addToOrder', params).respond(200);
controller.submit();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith(`Data saved!`);
});
});
});
});

View File

@ -1,3 +0,0 @@
Qty.: Cant.
First you must add some quantity: Primero debes agregar alguna cantidad
The amounts doesn't match with the grouping: Las cantidades no coinciden con el grouping

View File

@ -1,18 +0,0 @@
@import "variables";
.vn-order-prices-popover .content {
.prices {
vn-table {
.price-kg {
color: $color-font-secondary;
font-size: .75rem
}
.vn-input-number {
width: 80px;
}
}
.footer {
text-align: center;
}
}
}

View File

@ -49,48 +49,6 @@
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/catalog?q&categoryId&typeId&tagGroups",
"state": "order.card.catalog",
"component": "vn-order-catalog",
"description": "Catalog",
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/volume",
"state": "order.card.volume",
"component": "vn-order-volume",
"description": "Volume",
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/line",
"state": "order.card.line",
"component": "vn-order-line",
"description": "Lines",
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/create?clientFk",
"state": "order.create",
"component": "vn-order-create",
"description": "New order"
},
{
"url": "/basic-data",
"state": "order.card.basicData",
"component": "vn-order-basic-data",
"description": "Basic data",
"params": {
"order": "$ctrl.order"
}
}
]
}

View File

@ -1,86 +0,0 @@
<div class="search-panel">
<form ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-textfield
vn-one
label="General search"
ng-model="filter.search"
info="Search orders by ticket id"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Client id"
ng-model="filter.clientFk">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="Agency"
ng-model="filter.agencyModeFk"
url="AgencyModes/isActive"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-worker-autocomplete
vn-one
ng-model="filter.workerFk"
departments="['VT']"
show-field="nickname"
label="Sales person">
</vn-worker-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From landed"
ng-model="filter.from">
</vn-date-picker>
<vn-date-picker
vn-one
label="To landed"
ng-model="filter.to">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Order id"
ng-model="filter.orderFk">
</vn-textfield>
<vn-autocomplete
vn-one
label="Application"
ng-model="filter.sourceApp"
url="Orders/getSourceValues"
show-field="value"
value-field="value">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="My team"
ng-model="filter.myTeam"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="Order confirmed"
triple-state="true"
ng-model="filter.isConfirmed">
</vn-check>
<vn-check
vn-one
label="Show empty"
ng-model="filter.showEmpty">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -1,7 +0,0 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
ngModule.vnComponent('vnOrderSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
});

View File

@ -1,11 +0,0 @@
Order id: Id cesta
Client id: Id cliente
From landed: Desde f. entrega
To landed: Hasta f. entrega
To: Hasta
Agency: Agencia
Application: Aplicación
SalesPerson: Comercial
Order confirmed: Pedido confirmado
Show empty: Mostrar vacías
Search orders by ticket id: Buscar pedido por id ticket

View File

@ -1,131 +0,0 @@
<vn-card class="summary">
<h5>
<a ng-if="::$ctrl.summary.id"
vn-tooltip="Go to the order"
ui-sref="order.card.summary({id: {{::$ctrl.summary.id}}})"
name="goToSummary">
<vn-icon-button icon="launch"></vn-icon-button>
</a>
<span>
<span translate>Basket</span> #{{$ctrl.summary.id}} - {{$ctrl.summary.client.name}}
({{$ctrl.summary.client.id}})
</span>
<vn-button
disabled="$ctrl.order.isConfirmed"
class="flat"
style="color: inherit;"
label="Confirm"
ng-click="$ctrl.save()"
vn-tooltip="Confirm lines">
</vn-button>
</h5>
<vn-horizontal class="ticketSummary__data">
<vn-one>
<vn-label-value label="Id"
value="{{$ctrl.summary.id}}">
</vn-label-value>
<vn-label-value label="Nickname">
<span
ng-click="clientDescriptor.show($event, $ctrl.summary.clientFk)"
class="link">
{{$ctrl.summary.address.nickname}}
</span>
</vn-label-value>
<vn-label-value label="Company"
value="{{$ctrl.summary.address.companyFk}}">
</vn-label-value>
<vn-check label="Confirmed" disabled="true"
ng-model="$ctrl.summary.isConfirmed">
</vn-check>
</vn-one>
<vn-one>
<vn-label-value label="Created"
value="{{$ctrl.summary.created | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value>
<vn-label-value label="Confirmed"
value="{{$ctrl.summary.confirmed | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value>
<vn-label-value label="Landed"
value="{{$ctrl.summary.landed | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value>
<vn-label-value label="Phone">
<vn-link-phone
phone-number="$ctrl.summary.address.phone"
></vn-link-phone>
</vn-label-value>
<vn-label-value label="Created from"
value="{{$ctrl.summary.sourceApp}}">
</vn-label-value>
<vn-label-value label="Address" no-ellipsize
value="{{$ctrl.formattedAddress}}">
</vn-label-value>
</vn-one>
<vn-one>
<vn-label-value label="Notes" no-ellipsize
value="{{$ctrl.summary.note}}">
</vn-label-value>
</vn-one>
<vn-one class="taxes">
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.summary.subTotal | currency: 'EUR':2}}</p>
<p><vn-label translate>VAT</vn-label> {{$ctrl.summary.VAT | currency: 'EUR':2}}</p>
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.summary.total | currency: 'EUR':2}}</strong></p>
</vn-one>
<vn-auto>
<vn-table>
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th shrink>Item</vn-th>
<vn-th>Description</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Amount</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in $ctrl.summary.rows track by row.id">
<vn-td shrink>
<vn-icon
ng-show="row.visible || row.available"
color-main
icon="warning"
vn-tooltip="Visible: {{::row.visible || 0}} <br> {{::$ctrl.translate.instant('Available')}} {{::row.available || 0}}">
</vn-icon>
<vn-icon ng-show="row.reserved" icon="icon-reserva"></vn-icon>
</vn-td>
<vn-td shrink>
<span
ng-click="itemDescriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td vn-fetched-tags>
<div>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::row.item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>
<vn-td number>{{::row.price | currency: 'EUR':2}}</vn-td>
<vn-td number>{{::row.quantity * row.price | currency: 'EUR':2}}</vn-td>
</vn-tr>
</vn-tbody>
</table>
</vn-auto>
</vn-horizontal>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>

View File

@ -1,41 +0,0 @@
import ngModule from '../module';
import Summary from 'salix/components/summary';
import './style.scss';
class Controller extends Summary {
setSummary() {
this.$http.get(`Orders/${this.order.id}/summary`)
.then(res => this.summary = res.data);
}
get formattedAddress() {
if (!this.summary) return null;
let address = this.summary.address;
let province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${address.city} ${province}`;
}
$onChanges() {
if (this.order && this.order.id)
this.setSummary();
}
save() {
this.$http.post(`Orders/${this.order.id}/confirm`).then(() => {
this.vnApp.showSuccess(this.$t('Order confirmed'));
this.$state.go(`ticket.index`, {
q: JSON.stringify({clientFk: this.order.clientFk})
});
});
}
}
ngModule.vnComponent('vnOrderSummary', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
});

View File

@ -1,47 +0,0 @@
import './index';
describe('Order', () => {
describe('Component vnOrderSummary', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-order-summary></vn-order-summary>');
controller = $componentController('vnOrderSummary', {$element});
controller.order = {id: 1};
}));
describe('getSummary()', () => {
it('should now perform a GET query and define the summary property', () => {
let res = {
id: 1,
nickname: 'Batman'
};
$httpBackend.expectGET(`Orders/1/summary`).respond(res);
controller.setSummary();
$httpBackend.flush();
expect(controller.summary).toEqual(res);
});
});
describe('formattedAddress()', () => {
it('should return a full fromatted address with city and province', () => {
controller.summary = {
address: {
province: {
name: 'Gotham'
},
street: '1007 Mountain Drive',
city: 'Gotham'
}
};
expect(controller.formattedAddress).toEqual('1007 Mountain Drive - Gotham (Gotham)');
});
});
});
});

View File

@ -1,20 +0,0 @@
@import "./variables";
vn-order-summary .summary{
max-width: $width-lg;
& > vn-horizontal > vn-one {
min-width: 160px;
&.taxes {
border: $border-thin-light;
text-align: right;
padding: 8px;
& > p {
font-size: 1.2rem;
margin: 3px;
}
}
}
}

View File

@ -1,66 +0,0 @@
<vn-crud-model
auto-load="true"
vn-id="model"
url="OrderRows"
filter="::$ctrl.filter"
link="{orderFk: $ctrl.$params.id}"
limit="20"
data="rows"
on-data-change="$ctrl.onDataChange()">
</vn-crud-model>
<mg-ajax path="Orders/{{$ctrl.$params.id}}/getTotalVolume" options="mgEdit"></mg-ajax>
<vn-data-viewer model="model" class="header vn-w-lg">
<vn-card class="vn-pa-lg">
<vn-label-value
label="Total"
value="{{::edit.model.totalVolume}} M³">
</vn-label-value>
<vn-label-value
label="Cajas"
value="{{::edit.model.totalBoxes | dashIfEmpty}} U">
</vn-label-value>
</vn-card>
<vn-card class="vn-mt-md">
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink field="itemFk" number>Item</vn-th>
<vn-th>Description</vn-th>
<vn-th shrink field="quantity" number>Quantity</vn-th>
<vn-th shrink number>m³ per quantity</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in rows">
<vn-td shrink number>
<span
ng-click="itemDescriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td vn-fetched-tags>
<div>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::row.item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink number>{{::row.quantity}}</vn-td>
<vn-td shrink number>{{::row.volume | number:3}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>

View File

@ -1,37 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filter = {
include: {
relation: 'item'
},
order: 'itemFk'
};
this.order = {};
this.ticketVolumes = [];
}
onDataChange() {
this.$http.get(`Orders/${this.$params.id}/getVolumes`)
.then(res => {
this.$.model.data.forEach(order => {
res.data.volumes.forEach(volume => {
if (order.itemFk === volume.itemFk)
order.volume = volume.volume;
});
});
});
}
}
ngModule.vnComponent('vnOrderVolume', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
});

View File

@ -1,42 +0,0 @@
import './index';
describe('Order', () => {
describe('Component vnOrderVolume', () => {
let controller;
let $httpBackend;
let $scope;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $state, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.model = {
data: [
{itemFk: 1},
{itemFk: 2}
]
};
$state.params.id = 1;
const $element = angular.element('<vn-order-volume></vn-order-volume>');
controller = $componentController('vnOrderVolume', {$element, $scope});
}));
it('should join the sale volumes to its respective sale', () => {
let response = {
volumes: [
{itemFk: 1, volume: 0.008},
{itemFk: 2, volume: 0.003}
]
};
$httpBackend.expectGET(`Orders/1/getVolumes`).respond(response);
controller.onDataChange();
$httpBackend.flush();
expect(controller.$.model.data[0].volume).toBe(0.008);
expect(controller.$.model.data[1].volume).toBe(0.003);
});
});
});

View File

@ -1,12 +0,0 @@
@import "./variables";
vn-order-volume {
.header {
text-align: right;
& > div {
margin-bottom: $spacing-xs;
}
}
}