diff --git a/client/client/routes.json b/client/client/routes.json index 173224037..a19b5fde3 100644 --- a/client/client/routes.json +++ b/client/client/routes.json @@ -1,7 +1,7 @@ { "module": "client", "name": "Clients", - "icon": "/static/images/icon_client.png", + "icon": "person", "validations" : true, "routes": [ { diff --git a/client/client/src/address-edit/address-edit.html b/client/client/src/address-edit/address-edit.html index a5830d715..55154a996 100644 --- a/client/client/src/address-edit/address-edit.html +++ b/client/client/src/address-edit/address-edit.html @@ -15,7 +15,11 @@ Address - + + diff --git a/client/client/src/addresses/addresses.html b/client/client/src/addresses/addresses.html index 3b3f26232..2e5bb98c5 100644 --- a/client/client/src/addresses/addresses.html +++ b/client/client/src/addresses/addresses.html @@ -30,7 +30,7 @@ {{::observation.description}} - + diff --git a/client/client/src/billing-data/billing-data.html b/client/client/src/billing-data/billing-data.html index b9d117d31..2a097179a 100644 --- a/client/client/src/billing-data/billing-data.html +++ b/client/client/src/billing-data/billing-data.html @@ -10,30 +10,50 @@ Pay method - - + + + + - + + - + + - + + - + { })); describe('_getClientDebt()', () => { - it(`should call _getClientDebt() and define the client.debt value on the controller`, () => { + it(`should call _getClientDebt() and define the clientDebt value on the controller`, () => { controller.client = {}; let response = {debt: 100}; $httpBackend.whenGET(`/client/api/Clients/101/getDebt`).respond(response); @@ -25,7 +25,7 @@ describe('Descriptor', () => { controller._getClientDebt(101); $httpBackend.flush(); - expect(controller.client.debt).toEqual(100); + expect(controller.clientDebt).toEqual(100); }); }); diff --git a/client/client/src/fiscal-data/fiscal-data.html b/client/client/src/fiscal-data/fiscal-data.html index fdd27a958..b70dd319f 100644 --- a/client/client/src/fiscal-data/fiscal-data.html +++ b/client/client/src/fiscal-data/fiscal-data.html @@ -14,21 +14,21 @@ vn-focus label="Social name" field="$ctrl.client.socialName" - vn-acl="administrative, salesPerson" + vn-acl="administrative, salesAssistant, salesPerson" acl-conditional-to-salesPerson="{{!$ctrl.client.isTaxDataChecked}}"> @@ -37,14 +37,14 @@ vn-two label="Street" field="$ctrl.client.street" - vn-acl="administrative, salesPerson" + vn-acl="administrative, salesAssistant, salesPerson" acl-conditional-to-salesPerson="{{!$ctrl.client.isTaxDataChecked}}"> @@ -53,7 +53,7 @@ vn-one label="Postcode" field="$ctrl.client.postcode" - vn-acl="administrative, salesPerson" + vn-acl="administrative, salesAssistant, salesPerson" acl-conditional-to-salesPerson="{{!$ctrl.client.isTaxDataChecked}}"> @@ -84,21 +84,21 @@ vn-one label="Active" field="$ctrl.client.isActive" - vn-acl="administrative, salesPerson" + vn-acl="administrative, salesAssistant, salesPerson" acl-conditional-to-salesPerson="{{!$ctrl.client.isTaxDataChecked}}"> + vn-acl="administrative, salesAssistant, salesAssistant"> @@ -106,21 +106,21 @@ vn-one label="Has to invoice" field="$ctrl.client.hasToInvoice" - vn-acl="administrative, salesPerson" + vn-acl="administrative, salesAssistant, salesPerson" acl-conditional-to-salesPerson="{{!$ctrl.client.isTaxDataChecked}}"> @@ -128,7 +128,7 @@ diff --git a/client/core/src/components/autocomplete/style.scss b/client/core/src/components/autocomplete/style.scss index 26143c035..2dda27653 100755 --- a/client/core/src/components/autocomplete/style.scss +++ b/client/core/src/components/autocomplete/style.scss @@ -9,13 +9,6 @@ vn-autocomplete > div > .mdl-textfield { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - - &:focus { - outline: none; - } - &::-moz-focus-inner { - border: 0; - } } & > .icons { display: none; diff --git a/client/core/src/components/date-picker/style.scss b/client/core/src/components/date-picker/style.scss index d729ba947..99f1cbc1b 100644 --- a/client/core/src/components/date-picker/style.scss +++ b/client/core/src/components/date-picker/style.scss @@ -1,7 +1,4 @@ vn-date-picker { - div { - outline: none; //remove chrome outline - } .mdl-chip__action { position: absolute; width: auto; diff --git a/client/core/src/components/drop-down/drop-down.html b/client/core/src/components/drop-down/drop-down.html index 02d2d50b2..560ef77fa 100755 --- a/client/core/src/components/drop-down/drop-down.html +++ b/client/core/src/components/drop-down/drop-down.html @@ -1,35 +1,38 @@ -
-
- - - -
- - -
- -
- {{$ctrl.statusText}} + + + + -
\ No newline at end of file + diff --git a/client/core/src/components/drop-down/drop-down.js b/client/core/src/components/drop-down/drop-down.js index c5e11a839..066e26810 100755 --- a/client/core/src/components/drop-down/drop-down.js +++ b/client/core/src/components/drop-down/drop-down.js @@ -4,45 +4,34 @@ import './style.scss'; import './model'; export default class DropDown extends Component { - constructor($element, $scope, $transclude, $timeout, $http, $translate) { + constructor($element, $scope, $transclude, $timeout, $http) { super($element, $scope); this.$transclude = $transclude; this.$timeout = $timeout; - this.$translate = $translate; this.valueField = 'id'; this.showField = 'name'; this._search = undefined; - this._shown = false; this._activeOption = -1; this.showLoadMore = true; this.showFilter = true; - this.input = this.element.querySelector('.search'); - this.body = this.element.querySelector('.body'); - this.container = this.element.querySelector('ul'); - this.list = this.element.querySelector('.list'); - this.docKeyDownHandler = e => this.onDocKeyDown(e); - this.docFocusInHandler = e => this.onDocFocusIn(e); + } - this.element.addEventListener('mousedown', - e => this.onBackgroundMouseDown(e)); - this.element.addEventListener('focusin', - e => this.onFocusIn(e)); - this.list.addEventListener('scroll', - e => this.onScroll(e)); + $postLink() { + this.input = this.element.querySelector('.search'); + this.ul = this.element.querySelector('ul'); + this.list = this.element.querySelector('.list'); + this.list.addEventListener('scroll', e => this.onScroll(e)); } get shown() { - return this._shown; + return this.$.popover.shown; } set shown(value) { - if (value) - this.show(); - else - this.hide(); + this.$.popover.shown = value; } get search() { @@ -97,62 +86,18 @@ export default class DropDown extends Component { * @param {String} search The initial search term or %null */ show(search) { - if (this._shown) return; - this._shown = true; - this._activeOption = -1; this.search = search; + this._activeOption = -1; this.buildList(); - this.element.style.display = 'block'; - this.list.scrollTop = 0; - this.$timeout(() => this.$element.addClass('shown'), 40); - this.document.addEventListener('keydown', this.docKeyDownHandler); - this.document.addEventListener('focusin', this.docFocusInHandler); - this.relocate(); - this.input.focus(); + this.$.popover.parent = this.parent; + this.$.popover.show(); } /** * Hides the drop-down. */ hide() { - if (!this._shown) return; - this._shown = false; - this.element.style.display = ''; - this.$element.removeClass('shown'); - this.document.removeEventListener('keydown', this.docKeyDownHandler); - this.document.removeEventListener('focusin', this.docFocusInHandler); - if (this.parent) - this.parent.focus(); - } - - /** - * Repositions the drop-down to a correct location relative to the parent. - */ - relocate() { - if (!this.parent) return; - - let style = this.body.style; - style.width = ''; - style.height = ''; - - let parentRect = this.parent.getBoundingClientRect(); - let bodyRect = this.body.getBoundingClientRect(); - - let top = parentRect.top + parentRect.height; - let height = bodyRect.height; - let width = Math.max(bodyRect.width, parentRect.width); - - let margin = 10; - - if (top + height + margin > window.innerHeight) - top = Math.max(parentRect.top - height, margin); - - style.top = `${top}px`; - style.left = `${parentRect.left}px`; - style.width = `${width}px`; - - if (height + margin * 2 > window.innerHeight) - style.height = `${window.innerHeight - margin * 2}px`; + this.$.popover.hide(); } /** @@ -190,7 +135,7 @@ export default class DropDown extends Component { let data = this.$.model.data; if (option >= 0 && data && option < data.length) { - this.activeLi = this.container.children[option]; + this.activeLi = this.ul.children[option]; this.activeLi.className = 'active'; } } @@ -224,7 +169,7 @@ export default class DropDown extends Component { } if (!this.multiple) - this.hide(); + this.$.popover.hide(); } refreshModel() { @@ -260,22 +205,14 @@ export default class DropDown extends Component { return undefined; } - onMouseDown(event) { - this.lastMouseEvent = event; + onOpen() { + this.document.addEventListener('keydown', this.docKeyDownHandler); + this.list.scrollTop = 0; + this.input.focus(); } - onBackgroundMouseDown(event) { - if (event != this.lastMouseEvent) - this.hide(); - } - - onFocusIn(event) { - this.lastFocusEvent = event; - } - - onDocFocusIn(event) { - if (event !== this.lastFocusEvent) - this.hide(); + onClose() { + this.document.removeEventListener('keydown', this.docKeyDownHandler); } onClearClick() { @@ -299,7 +236,7 @@ export default class DropDown extends Component { onContainerClick(event) { if (event.defaultPrevented) return; - let index = getPosition(this.container, event); + let index = getPosition(this.ul, event); if (index != -1) this.selectOption(index); } @@ -318,9 +255,6 @@ export default class DropDown extends Component { case 13: // Enter this.selectOption(option); break; - case 27: // Escape - this.hide(); - break; case 38: // Up this.moveToOption(option <= 0 ? nOpts : option - 1); break; @@ -342,8 +276,7 @@ export default class DropDown extends Component { } buildList() { - this.destroyScopes(); - this.scopes = []; + this.destroyList(); let hasTemplate = this.$transclude && this.$transclude.isSlotFilled('tplItem'); @@ -377,23 +310,26 @@ export default class DropDown extends Component { } } - this.container.innerHTML = ''; - this.container.appendChild(fragment); + this.ul.appendChild(fragment); this.activateOption(this._activeOption); - this.relocate(); + this.$.popover.relocate(); } - destroyScopes() { + destroyList() { + this.ul.innerHTML = ''; + if (this.scopes) for (let scope of this.scopes) scope.$destroy(); + + this.scopes = []; } $onDestroy() { - this.destroyScopes(); + this.destroyList(); } } -DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http', '$translate']; +DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http']; /** * Gets the position of an event element relative to a parent. diff --git a/client/core/src/components/drop-down/drop-down.spec.js b/client/core/src/components/drop-down/drop-down.spec.js index fc4028fc8..f0bd93853 100644 --- a/client/core/src/components/drop-down/drop-down.spec.js +++ b/client/core/src/components/drop-down/drop-down.spec.js @@ -7,6 +7,7 @@ describe('Component vnDropDown', () => { let $element; let $scope; let $httpBackend; + let $transitions; let $q; let $filter; let controller; @@ -15,18 +16,23 @@ describe('Component vnDropDown', () => { angular.mock.module('client'); }); - beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$timeout_, _$httpBackend_, _$q_, _$filter_) => { + beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$timeout_, _$httpBackend_, _$q_, _$filter_, _$transitions_) => { $componentController = _$componentController_; $element = angular.element(`
${template}
`); $timeout = _$timeout_; + $transitions = _$transitions_; $q = _$q_; $filter = _$filter_; $scope = $rootScope.$new(); $httpBackend = _$httpBackend_; $httpBackend.when('GET', /\/locale\/\w+\/[a-z]{2}\.json/).respond({}); + let popoverTemplate = require('../popover/popover.html'); + let $popover = angular.element(`
${popoverTemplate}
`); + $scope.popover = $componentController('vnPopover', {$element: $popover, $scope, $timeout, $transitions}); $scope.model = $componentController('vnModel', {$httpBackend, $q, $filter}); controller = $componentController('vnDropDown', {$element, $scope, $transclude: null, $timeout, $httpBackend, $translate: null}); + controller.$postLink(); controller.parent = angular.element('')[0]; })); diff --git a/client/core/src/components/drop-down/style.scss b/client/core/src/components/drop-down/style.scss index 71bfdd296..94521aff8 100755 --- a/client/core/src/components/drop-down/style.scss +++ b/client/core/src/components/drop-down/style.scss @@ -1,30 +1,7 @@ vn-drop-down { - z-index: 10; - position: fixed; - display: none; - top: 0; - left: 0; - right: 0; - bottom: 0; - - &.shown { - & > .body { - transform: translateY(0); - opacity: 1; - } - } - & > .body { - position: fixed; - box-shadow: 0 .1em .4em rgba(1, 1, 1, .4); - border-radius: .1em; - background-color: white; + .dropdown { display: flex; flex-direction: column; - transform: translateY(-.4em); - opacity: 0; - transition-property: opacity, transform; - transition-duration: 250ms; - transition-timing-function: ease-in-out; & > .filter { position: relative; @@ -35,6 +12,7 @@ vn-drop-down { box-sizing: border-box; border: none; border-bottom: 1px solid #ccc; + font-size: inherit; padding: .6em; } & > vn-icon[icon=clear] { @@ -64,10 +42,9 @@ vn-drop-down { ul { padding: 0; margin: 0; + list-style-type: none; } li, .status { - outline: none; - list-style-type: none; padding: .6em; cursor: pointer; white-space: nowrap; diff --git a/client/core/src/components/multi-check/multi-check.scss b/client/core/src/components/multi-check/multi-check.scss index 596a2e6f5..c253bbde6 100644 --- a/client/core/src/components/multi-check/multi-check.scss +++ b/client/core/src/components/multi-check/multi-check.scss @@ -2,10 +2,6 @@ vn-icon{ cursor: pointer; } - &:focus, &:active, &:hover{ - outline: none; - border: none; - } .primaryCheckbox { vn-icon{ font-size: 22px; diff --git a/client/core/src/components/popover/popover.html b/client/core/src/components/popover/popover.html new file mode 100644 index 000000000..3e3e6ca59 --- /dev/null +++ b/client/core/src/components/popover/popover.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/client/core/src/components/popover/popover.js b/client/core/src/components/popover/popover.js index 8430f037c..45a25918f 100644 --- a/client/core/src/components/popover/popover.js +++ b/client/core/src/components/popover/popover.js @@ -1,205 +1,220 @@ import ngModule from '../../module'; -import './style.css'; +import Component from '../../lib/component'; +import './style.scss'; -directive.$inject = ['vnPopover']; -export function directive(vnPopover) { - return { - restrict: 'A', - link: function($scope, $element, $attrs) { - $element.on('click', function(event) { - vnPopover.showComponent($attrs.vnDialog, $scope, $element); - event.preventDefault(); - }); - } - }; -} -ngModule.directive('vnPopover', directive); - -export class Popover { - constructor($document, $compile, $transitions) { - this.document = $document[0]; - this.$compile = $compile; +/** + * A simple popover. + */ +export default class Popover extends Component { + constructor($element, $scope, $timeout, $transitions) { + super($element, $scope); + this.$timeout = $timeout; this.$transitions = $transitions; - this.removeScope = false; - this.popOpens = 0; - } - _init() { - this.docMouseDownHandler = e => this.onDocMouseDown(e); - this.document.addEventListener('mousedown', this.docMouseDownHandler); + this._shown = false; + this.docKeyDownHandler = e => this.onDocKeyDown(e); + this.docFocusInHandler = e => this.onDocFocusIn(e); + + this.element.addEventListener('mousedown', + e => this.onBackgroundMouseDown(e)); + this.element.addEventListener('focusin', + e => this.onFocusIn(e)); + + this.popover = this.element.querySelector('.popover'); + this.popover.addEventListener('mousedown', + e => this.onMouseDown(e)); + + this.arrow = this.element.querySelector('.arrow'); + this.content = this.element.querySelector('.content'); + } + + set child(value) { + this.content.appendChild(value); + } + + get child() { + return this.content.firstChild; + } + + get shown() { + return this._shown; + } + + set shown(value) { + if (value) + this.show(); + else + this.hide(); + } + + /** + * Shows the popover. If a parent is specified it is shown in a visible + * relative position to it. + */ + show() { + if (this._shown) return; + + this._shown = true; + this.element.style.display = 'block'; + this.$timeout.cancel(this.showTimeout); + this.showTimeout = this.$timeout(() => { + this.$element.addClass('shown'); + this.showTimeout = null; + }, 30); + this.document.addEventListener('keydown', this.docKeyDownHandler); - this.deregisterCallback = this.$transitions.onStart({}, - () => this.hideAll()); + this.document.addEventListener('focusin', this.docFocusInHandler); + + this.deregisterCallback = this.$transitions.onStart({}, () => this.hide()); + this.relocate(); + + if (this.onOpen) + this.onOpen(); } - _destroy() { - this.document.removeEventListener('mousedown', this.docMouseDownHandler); + + /** + * Hides the popover. + */ + hide() { + if (!this._shown) return; + + this._shown = false; + this.$element.removeClass('shown'); + this.$timeout.cancel(this.showTimeout); + this.showTimeout = this.$timeout(() => { + this.element.style.display = 'none'; + this.showTimeout = null; + }, 250); + this.document.removeEventListener('keydown', this.docKeyDownHandler); - this.docMouseDownHandler = null; - this.docKeyDownHandler = null; - this.deregisterCallback(); - } - show(childElement, parent, popoverId) { - this.childElement = childElement; - let popover = this.document.createElement('div'); - this.popOpens++; + this.document.removeEventListener('focusin', this.docFocusInHandler); - if (!popoverId) { - popoverId = 'popover-' + this.popOpens; - popover.id = popoverId; - } + if (this.deregisterCallback) + this.deregisterCallback(); - popover.className = 'vn-popover'; - popover.addEventListener('mousedown', - e => this.onPopoverMouseDown(e)); - popover.appendChild(childElement); - this.popover = popover; + if (this.parent) + this.parent.focus(); - let style = popover.style; - - let spacing = 0; - let screenMargin = 20; - let dblMargin = screenMargin * 2; - - let width = popover.offsetWidth; - let height = popover.offsetHeight; - let innerWidth = window.innerWidth; - let innerHeight = window.innerHeight; - - if (width + dblMargin > innerWidth) { - width = innerWidth - dblMargin; - style.width = width + 'px'; - } - - if (height + dblMargin > innerHeight) { - height = innerHeight - dblMargin; - style.height = height + 'px'; - } - - if (parent) { - let parentNode = parent; - let rect = parentNode.getBoundingClientRect(); - let left = rect.left; - let top = rect.top + spacing + parentNode.offsetHeight; - - if (left + width > innerWidth) - left -= (left + width) - innerWidth + margin; - - if (top + height > innerHeight) - top -= height + parentNode.offsetHeight + spacing * 2; - - if (left < 0) - left = screenMargin; - - if (top < 0) - top = screenMargin; - - style.top = (top) + 'px'; - style.left = (left) + 'px'; - style.minWidth = (rect.width) + 'px'; - } - - this.document.body.appendChild(popover); - - if (this.popOpens === 1) { - this._init(); - } - return popoverId; + if (this.onClose) + this.onClose(); } - showComponent(childComponent, $scope, parent) { - let childElement = this.document.createElement(childComponent); - let id = 'popover-' + this.popOpens; - childElement.id = id; - this.removeScope = true; - this.$compile(childElement)($scope.$new()); - this.show(childElement, parent, id); - return childElement; - } + /** + * Repositions the popover to a correct location relative to the parent. + */ + relocate() { + if (!(this.parent && this._shown)) return; - _checkOpens() { - this.popOpens = this.document.querySelectorAll('*[id^="popover-"]').length; - if (this.popOpens === 0) { - this._destroy(); - } - } + let style = this.popover.style; + style.width = ''; + style.height = ''; - _removeElement(val) { - if (!val) return; - let element = angular.element(val); - let parent = val.parentNode; - if (element.scope() && element.scope().$id > 1) { - element.scope().$destroy(); - } - element.remove(); - if (parent.className.indexOf('vn-popover') !== -1) - this._removeElement(parent); - } + let arrowStyle = this.arrow.style; + arrowStyle.top = ''; + arrowStyle.bottom = ''; - hide(id) { - let popover = this.document.querySelector(`#${id}`); - if (popover) { - this._removeElement(popover); - } - this._checkOpens(); - } + let parentRect = this.parent.getBoundingClientRect(); + let popoverRect = this.popover.getBoundingClientRect(); + let arrowRect = this.arrow.getBoundingClientRect(); - hideChilds(id) { - let popovers = this.document.querySelectorAll('*[id^="popover-"]'); - let idNumber = parseInt(id.split('-')[1], 10); - popovers.forEach( - val => { - if (parseInt(val.id.split('-')[1], 10) > idNumber) - this._removeElement(val); - } - ); - this._checkOpens(); - } + let arrowHeight = Math.sqrt(Math.pow(arrowRect.height, 2) * 2) / 2; - hideAll() { - let popovers = this.document.querySelectorAll('*[id^="popover-"]'); - popovers.forEach( - val => { - this._removeElement(val); - } - ); - this._checkOpens(); - } + let top = parentRect.top + parentRect.height + arrowHeight; + let left = parentRect.left; + let height = popoverRect.height; + let width = Math.max(popoverRect.width, parentRect.width); - _findPopOver(node) { - while (node != null) { - if (node.id && node.id.startsWith('popover-')) { - return node.id; - } - node = node.parentNode; - } - return null; - } + let margin = 10; + let showTop = top + height + margin > window.innerHeight; - onDocMouseDown(event) { - let targetId = this._findPopOver(event.target); - if (targetId) { - this.hideChilds(targetId); - } else { - this.hideAll(); - } + if (showTop) + top = Math.max(parentRect.top - height - arrowHeight, margin); + if (left + width + margin > window.innerWidth) + left = window.innerWidth - width - margin; + + if (showTop) + arrowStyle.bottom = `0`; + else + arrowStyle.top = `0`; + + arrowStyle.left = `${(parentRect.left - left) + parentRect.width / 2}px`; + + style.top = `${top}px`; + style.left = `${left}px`; + style.width = `${width}px`; + + if (height + margin * 2 + arrowHeight > window.innerHeight) + style.height = `${window.innerHeight - margin * 2 - arrowHeight}px`; } onDocKeyDown(event) { - if (event.keyCode === 27) { - let targetId = this._findPopOver(this.lastTarget); - if (targetId) { - this.hideChilds(targetId); - } else { - this.hideAll(); - } - this.lastTarget = null; + if (event.defaultPrevented) return; + + if (event.keyCode == 27) { // Esc + event.preventDefault(); + this.hide(); + this.$.$applyAsync(); } } - onPopoverMouseDown(event) { - this.lastTarget = event.target; + onMouseDown(event) { + this.lastMouseEvent = event; + } + + onBackgroundMouseDown(event) { + if (event != this.lastMouseEvent) + this.hide(); + } + + onFocusIn(event) { + this.lastFocusEvent = event; + } + + onDocFocusIn(event) { + if (event !== this.lastFocusEvent) + this.hide(); } } -Popover.$inject = ['$document', '$compile', '$transitions']; +Popover.$inject = ['$element', '$scope', '$timeout', '$transitions']; -ngModule.service('vnPopover', Popover); +ngModule.component('vnPopover', { + template: require('./popover.html'), + controller: Popover, + transclude: true, + bindings: { + onOpen: '&?', + onClose: '&?' + } +}); + +class PopoverService { + constructor($document, $compile, $transitions, $rootScope) { + this.$compile = $compile; + this.$rootScope = $rootScope; + this.$document = $document; + this.stack = []; + } + show(child, parent, $scope) { + let element = this.$compile('')($scope || this.$rootScope)[0]; + let popover = element.$ctrl; + popover.parent = parent; + popover.child = child; + popover.show(); + popover.onClose = () => { + this.$document[0].body.removeChild(element); + if ($scope) $scope.$destroy(); + }; + this.$document[0].body.appendChild(element); + return popover; + } + + showComponent(componentTag, $scope, parent) { + let $newScope = $scope.$new(); + let childElement = this.$compile(`<${componentTag}/>`)($newScope)[0]; + this.show(childElement, parent, $newScope); + return childElement; + } +} +PopoverService.$inject = ['$document', '$compile', '$transitions', '$rootScope']; + +ngModule.service('vnPopover', PopoverService); diff --git a/client/core/src/components/popover/style.css b/client/core/src/components/popover/style.css deleted file mode 100644 index 536359a2f..000000000 --- a/client/core/src/components/popover/style.css +++ /dev/null @@ -1,9 +0,0 @@ -.vn-popover { - position: fixed; - box-shadow: 0 0 .4em rgba(1,1,1,.4); - background-color: white; - z-index: 100; - border-radius: .1em; - top: 0; - left: 0; -} \ No newline at end of file diff --git a/client/core/src/components/popover/style.scss b/client/core/src/components/popover/style.scss new file mode 100644 index 000000000..f8323fb5e --- /dev/null +++ b/client/core/src/components/popover/style.scss @@ -0,0 +1,43 @@ +vn-popover { + display: none; + z-index: 10; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + + opacity: 0; + transform: translateY(-.6em); + transition-property: opacity, transform; + transition-duration: 200ms; + transition-timing-function: ease-in-out; + + &.shown { + transform: translateY(0); + opacity: 1; + } + & > .popover { + position: absolute; + display: flex; + box-shadow: 0 .1em .4em rgba(1, 1, 1, .4); + + & > .arrow { + width: 1em; + height: 1em; + margin: -.5em; + background-color: white; + box-shadow: 0 .1em .4em rgba(1, 1, 1, .4); + position: absolute; + transform: rotate(45deg); + z-index: 0; + } + & > .content { + width: 100%; + border-radius: .1em; + overflow: auto; + background-color: white; + z-index: 1; + } + } +} \ No newline at end of file diff --git a/client/core/src/components/textfield/style.scss b/client/core/src/components/textfield/style.scss index 81e5ca74c..181ad8d8b 100644 --- a/client/core/src/components/textfield/style.scss +++ b/client/core/src/components/textfield/style.scss @@ -1,7 +1,4 @@ vn-textfield { - div { - outline: none; //remove chrome outline - } .mdl-chip__action { position: absolute; width: auto; diff --git a/client/core/src/directives/index.js b/client/core/src/directives/index.js index 02f922149..23392e5e0 100644 --- a/client/core/src/directives/index.js +++ b/client/core/src/directives/index.js @@ -1,6 +1,7 @@ import './id'; import './focus'; import './dialog'; +import './popover'; import './validation'; import './acl'; import './on-error-src'; diff --git a/client/core/src/directives/popover.js b/client/core/src/directives/popover.js new file mode 100644 index 000000000..437d93783 --- /dev/null +++ b/client/core/src/directives/popover.js @@ -0,0 +1,26 @@ +import ngModule from '../module'; +import Popover from '../components/popover/popover'; +import {kebabToCamel} from '../lib/string'; + +/** + * Directive used to open a popover. + * + * @return {Object} The directive + */ +export function directive() { + return { + restrict: 'A', + link: function($scope, $element, $attrs) { + $element.on('click', function(event) { + let popoverKey = kebabToCamel($attrs.vnPopover); + let popover = $scope[popoverKey]; + if (popover instanceof Popover) { + popover.parent = $element[0]; + popover.show(); + } + event.preventDefault(); + }); + } + }; +} +ngModule.directive('vnPopover', directive); diff --git a/client/item/routes.json b/client/item/routes.json index ac1c55820..7c0832f25 100644 --- a/client/item/routes.json +++ b/client/item/routes.json @@ -1,7 +1,7 @@ { "module": "item", "name": "Items", - "icon": "/static/images/icon_item.png", + "icon": "inbox", "validations" : true, "routes": [ { diff --git a/client/item/src/descriptor/item-descriptor.html b/client/item/src/descriptor/item-descriptor.html index e9a225502..38002ed5a 100644 --- a/client/item/src/descriptor/item-descriptor.html +++ b/client/item/src/descriptor/item-descriptor.html @@ -18,7 +18,7 @@ + vn-visible-by="administrative, salesAssistant"> diff --git a/client/production/routes.json b/client/production/routes.json index 1c3abd874..159a80144 100644 --- a/client/production/routes.json +++ b/client/production/routes.json @@ -1,7 +1,7 @@ { "module": "production", "name": "Production", - "icon": "/static/images/icon_production.png", + "icon": "local_florist", "validations" : false, "routes": [ { diff --git a/client/salix/src/components/main-menu/main-menu.html b/client/salix/src/components/main-menu/main-menu.html index 863585baa..72a4bdc06 100644 --- a/client/salix/src/components/main-menu/main-menu.html +++ b/client/salix/src/components/main-menu/main-menu.html @@ -6,29 +6,33 @@ -
    -
  • - - - -
  • -
+ +
    +
  • + + {{::mod.name}} +
  • +
+
-
    -
  • - {{::lang}} -
  • -
+ +
    +
  • + {{::lang}} +
  • +
+
vn-icon { + padding-right: .3em; + vertical-align: middle; + } + &:hover { + background-color: #FF9300; + opacity: 0.7 !important; + } + &:last-child { + margin-bottom: 0; + } } - i { - float: left; - padding-top: 13px; - margin-right: 3px; - } - } - li.mdl-menu__item:hover { - background-color: #FF9300; - opacity: 0.7 !important; - } - li.mdl-menu__item:last-child { - margin-bottom: 0; } } \ No newline at end of file diff --git a/client/salix/src/styles/misc.scss b/client/salix/src/styles/misc.scss index 34fc2063c..e20bda7ed 100644 --- a/client/salix/src/styles/misc.scss +++ b/client/salix/src/styles/misc.scss @@ -3,6 +3,21 @@ @import "colors"; @import "border"; + +a:focus, +input:focus, +button:focus +{ + outline: none; +} +button::-moz-focus-inner, +input[type=submit]::-moz-focus-inner, +input[type=button]::-moz-focus-inner, +input[type=reset]::-moz-focus-inner +{ + border: none; +} + .form { height: 100%; box-sizing: border-box; diff --git a/e2e/helpers/components_selectors.js b/e2e/helpers/components_selectors.js index ea46353d6..488d002e0 100644 --- a/e2e/helpers/components_selectors.js +++ b/e2e/helpers/components_selectors.js @@ -8,7 +8,6 @@ export default { vnSubmit: 'vn-submit > input', vnTopbar: 'vn-topbar > header', vnIcon: 'vn-icon', - vnMainMenu: 'vn-main-menu > div', vnModuleContainer: 'vn-module-container > a', vnSearchBar: 'vn-searchbar > form > vn-horizontal', vnFloatButton: 'vn-float-button > button', diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 1a839a0cb..62e0278ac 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -6,8 +6,8 @@ export default { globalItems: { logOutButton: `#logout`, applicationsMenuButton: `#apps`, - applicationsMenuVisible: `${components.vnMainMenu} .is-visible > div`, - clientsButton: `${components.vnMainMenu} > div > ul > li:nth-child(1)` + applicationsMenuVisible: `vn-main-menu [vn-id="apps-menu"] ul`, + clientsButton: `vn-main-menu [vn-id="apps-menu"] ul > li:nth-child(1)` }, moduleAccessView: { clientsSectionButton: `${components.vnModuleContainer}[ui-sref="clients"]`, diff --git a/e2e/paths/client-module/12_lock_of_verified_data.spec.js b/e2e/paths/client-module/12_lock_of_verified_data.spec.js index a3d8a3dae..764476e62 100644 --- a/e2e/paths/client-module/12_lock_of_verified_data.spec.js +++ b/e2e/paths/client-module/12_lock_of_verified_data.spec.js @@ -280,4 +280,193 @@ describe('lock verified data path', () => { }); }); }); + + describe('as salesAssistant', () => { + beforeAll(() => { + return nightmare + .waitToClick(selectors.globalItems.logOutButton) + .waitForLogin('salesAssistant'); + }); + + it('should navigate to clients index', () => { + return nightmare + .waitToClick(selectors.globalItems.applicationsMenuButton) + .wait(selectors.globalItems.applicationsMenuVisible) + .waitToClick(selectors.globalItems.clientsButton) + .wait(selectors.clientsIndex.createClientButton) + .parsedUrl() + .then(url => { + expect(url.hash).toEqual('#!/clients'); + }); + }); + + it('should search again for the user Petter Parker', () => { + return nightmare + .wait(selectors.clientsIndex.searchResult) + .type(selectors.clientsIndex.searchClientInput, 'Petter Parker') + .click(selectors.clientsIndex.searchButton) + .waitForNumberOfElements(selectors.clientsIndex.searchResult, 1) + .countSearchResults(selectors.clientsIndex.searchResult) + .then(result => { + expect(result).toEqual(1); + }); + }); + + it(`should click on the search result to access to the Petter Parkers fiscal data`, () => { + return nightmare + .waitForTextInElement(selectors.clientsIndex.searchResult, 'Petter Parker') + .waitToClick(selectors.clientsIndex.searchResult) + .waitToClick(selectors.clientFiscalData.fiscalDataButton) + .waitForURL('fiscal-data') + .parsedUrl() + .then(url => { + expect(url.hash).toContain('fiscal-data'); + }); + }); + + it(`should click on the fiscal data button`, () => { + return nightmare + .waitToClick(selectors.clientFiscalData.fiscalDataButton) + .waitForURL('fiscal-data') + .parsedUrl() + .then(url => { + expect(url.hash).toContain('fiscal-data'); + }); + }); + + it('should confirm verified data button is enabled for salesAssistant', () => { + return nightmare + .wait(selectors.clientFiscalData.verifiedDataCheckboxInput) + .evaluate(selector => { + return document.querySelector(selector).className; + }, 'body > vn-app > vn-vertical > vn-vertical > vn-client-card > vn-main-block > vn-horizontal > vn-one > vn-vertical > vn-client-fiscal-data > form > vn-card > div > vn-horizontal:nth-child(5) > vn-check:nth-child(3) > label') + .then(result => { + expect(result).not.toContain('is-disabled'); + }); + }); + + it('should uncheck the Verified data checkbox', () => { + return nightmare + .waitToClick(selectors.clientFiscalData.verifiedDataCheckboxInput) + .waitToClick(selectors.clientFiscalData.saveButton) + .waitForSnackbar() + .then(result => { + expect(result).toEqual('Data saved!'); + }); + }); + + it('should confirm Verified data checkbox is unchecked', () => { + return nightmare + .waitToClick(selectors.clientBasicData.basicDataButton) + .wait(selectors.clientBasicData.nameInput) + .waitToClick(selectors.clientFiscalData.fiscalDataButton) + .wait(selectors.clientFiscalData.verifiedDataCheckboxInput) + .evaluate(selector => { + return document.querySelector(selector).checked; + }, selectors.clientFiscalData.verifiedDataCheckboxInput) + .then(value => { + expect(value).toBeFalsy(); + }); + }); + + it('should again edit the social name', () => { + return nightmare + .wait(selectors.clientFiscalData.socialNameInput) + .clearInput(selectors.clientFiscalData.socialNameInput) + .type(selectors.clientFiscalData.socialNameInput, 'salesAssistant was here') + .click(selectors.clientFiscalData.saveButton) + .waitForSnackbar() + .then(result => { + expect(result).toEqual('Data saved!'); + }); + }); + + it('should confirm the social name have been edited once and for all', () => { + return nightmare + .waitToClick(selectors.clientBasicData.basicDataButton) + .wait(selectors.clientBasicData.nameInput) + .waitToClick(selectors.clientFiscalData.fiscalDataButton) + .wait(selectors.clientFiscalData.socialNameInput) + .getInputValue(selectors.clientFiscalData.socialNameInput) + .then(result => { + expect(result).toEqual('salesAssistant was here'); + }); + }); + }); + + describe('as salesPerson third run', () => { + beforeAll(() => { + return nightmare + .waitToClick(selectors.globalItems.logOutButton) + .waitForLogin('salesPerson'); + }); + + it('should again click on the Clients button of the top bar menu', () => { + return nightmare + .waitToClick(selectors.globalItems.applicationsMenuButton) + .wait(selectors.globalItems.applicationsMenuVisible) + .waitToClick(selectors.globalItems.clientsButton) + .wait(selectors.clientsIndex.createClientButton) + .parsedUrl() + .then(url => { + expect(url.hash).toEqual('#!/clients'); + }); + }); + + it('should once again search for the user Petter Parker', () => { + return nightmare + .wait(selectors.clientsIndex.searchResult) + .type(selectors.clientsIndex.searchClientInput, 'Petter Parker') + .click(selectors.clientsIndex.searchButton) + .waitForNumberOfElements(selectors.clientsIndex.searchResult, 1) + .countSearchResults(selectors.clientsIndex.searchResult) + .then(result => { + expect(result).toEqual(1); + }); + }); + + it(`should click on the search result to access to the client's fiscal data`, () => { + return nightmare + .waitForTextInElement(selectors.clientsIndex.searchResult, 'Petter Parker') + .waitToClick(selectors.clientsIndex.searchResult) + .waitToClick(selectors.clientFiscalData.fiscalDataButton) + .waitForURL('fiscal-data') + .parsedUrl() + .then(url => { + expect(url.hash).toContain('fiscal-data'); + }); + }); + + it(`should click on the fiscal data button to start editing`, () => { + return nightmare + .waitToClick(selectors.clientFiscalData.fiscalDataButton) + .waitForURL('fiscal-data') + .parsedUrl() + .then(url => { + expect(url.hash).toContain('fiscal-data'); + }); + }); + + it('should confirm verified data button is enabled once again', () => { + return nightmare + .wait(selectors.clientFiscalData.verifiedDataCheckboxInput) + .evaluate(selector => { + return document.querySelector(selector).className; + }, 'body > vn-app > vn-vertical > vn-vertical > vn-client-card > vn-main-block > vn-horizontal > vn-one > vn-vertical > vn-client-fiscal-data > form > vn-card > div > vn-horizontal:nth-child(5) > vn-check:nth-child(3) > label') + .then(result => { + expect(result).toContain('is-disabled'); + }); + }); + + it('should confirm the form is enabled for salesPerson', () => { + return nightmare + .wait(selectors.clientFiscalData.socialNameInput) + .evaluate(selector => { + return document.querySelector(selector).className; + }, 'vn-textfield[field="$ctrl.client.socialName"] > div') + .then(result => { + expect(result).not.toContain('is-disabled'); + }); + }); + }); }); diff --git a/gulpfile.js b/gulpfile.js index 7c9be4d95..e1b3df473 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -63,11 +63,11 @@ gulp.task('services-only', async () => { /** * Runs the e2e tests, restoring the fixtures first. */ -gulp.task('e2e', ['docker-rebuild'], async () => { +gulp.task('e2e', ['docker'], async () => { await runSequenceP('e2e-only'); }); -gulp.task('smokes', ['docker-rebuild'], async () => { +gulp.task('smokes', ['docker'], async () => { await runSequenceP('smokes-only'); }); @@ -416,7 +416,7 @@ gulp.task('docker', async () => { * Does the minium effort to start the docker, if it doesn't exists calls * the 'docker-run' task, if it is started does nothing. Keep in mind that when * you do not rebuild the docker you may be using an outdated version of it. - * See the 'docker-rebuild' task for more info. + * See the 'docker' task for more info. */ gulp.task('docker-start', async () => { let state; diff --git a/services/loopback/common/methods/client/createWithUser.js b/services/loopback/common/methods/client/createWithUser.js index 2378406b4..f5220a839 100644 --- a/services/loopback/common/methods/client/createWithUser.js +++ b/services/loopback/common/methods/client/createWithUser.js @@ -1,5 +1,3 @@ -let md5 = require('md5'); - module.exports = function(Self) { Self.remoteMethod('createWithUser', { description: 'Creates both client and its web account', @@ -18,43 +16,33 @@ module.exports = function(Self) { } }); - Self.createWithUser = (data, callback) => { + Self.createWithUser = async data => { let firstEmail = data.email ? data.email.split(',')[0] : null; let user = { name: data.userName, email: firstEmail, - password: md5(parseInt(Math.random() * 100000000000000)) + password: parseInt(Math.random() * 100000000000000) }; let Account = Self.app.models.Account; - Account.beginTransaction({}, (error, transaction) => { - if (error) return callback(error); + let transaction = await Account.beginTransaction({}); - Account.create(user, {transaction}, (error, account) => { - if (error) { - transaction.rollback(); - return callback(error); - } - - let client = { - name: data.name, - fi: data.fi, - socialName: data.socialName, - id: account.id, - email: data.email, - salesPersonFk: data.salesPersonFk - }; - - Self.create(client, {transaction}, (error, newClient) => { - if (error) { - transaction.rollback(); - return callback(error); - } - - transaction.commit(); - callback(null, newClient); - }); - }); - }); + try { + let account = await Account.create(user, {transaction}); + let client = { + id: account.id, + name: data.name, + fi: data.fi, + socialName: data.socialName, + email: data.email, + salesPersonFk: data.salesPersonFk + }; + newClient = await Self.create(client, {transaction}); + await transaction.commit(); + return newClient; + } catch (e) { + transaction.rollback(); + throw e; + } }; }; diff --git a/services/loopback/common/methods/client/specs/createWithUser.spec.js b/services/loopback/common/methods/client/specs/createWithUser.spec.js index eb41d0873..25f6789fb 100644 --- a/services/loopback/common/methods/client/specs/createWithUser.spec.js +++ b/services/loopback/common/methods/client/specs/createWithUser.spec.js @@ -48,45 +48,39 @@ describe('Client Create', () => { .catch(catchErrors(done)); }); - it('should not be able to create a user if exists', done => { - app.models.Client.findOne({where: {name: 'Charles Xavier'}}) - .then(client => { - app.models.Account.findOne({where: {id: client.id}}) - .then(account => { - let formerAccountData = { - name: client.name, - userName: account.name, - email: client.email, - fi: client.fi, - socialName: client.socialName - }; + it('should not be able to create a user if exists', async() => { + let client = await app.models.Client.findOne({where: {name: 'Charles Xavier'}}); + let account = await app.models.Account.findOne({where: {id: client.id}}); - app.models.Client.createWithUser(formerAccountData, (err, client) => { - expect(err.details.codes.name[0]).toEqual('uniqueness'); - done(); - }); - }); - }) - .catch(catchErrors(done)); + let formerAccountData = { + name: client.name, + userName: account.name, + email: client.email, + fi: client.fi, + socialName: client.socialName + }; + + try { + let client = await app.models.Client.createWithUser(formerAccountData); + + expect(client).toBeNull(); + } catch (err) { + expect(err.details.codes.name[0]).toEqual('uniqueness'); + } }); - it('should create a new account', done => { - app.models.Client.createWithUser(newAccountData, (error, client) => { - if (error) return catchErrors(done)(error); - app.models.Account.findOne({where: {name: newAccountData.userName}}) - .then(account => { - expect(account.name).toEqual(newAccountData.userName); - app.models.Client.findOne({where: {name: newAccountData.name}}) - .then(client => { - expect(client.id).toEqual(account.id); - expect(client.name).toEqual(newAccountData.name); - expect(client.email).toEqual(newAccountData.email); - expect(client.fi).toEqual(newAccountData.fi); - expect(client.socialName).toEqual(newAccountData.socialName); - done(); - }); - }) - .catch(catchErrors(done)); - }); + it('should create a new account', async() => { + let client = await app.models.Client.createWithUser(newAccountData); + let account = await app.models.Account.findOne({where: {name: newAccountData.userName}}); + + expect(account.name).toEqual(newAccountData.userName); + + client = await app.models.Client.findOne({where: {name: newAccountData.name}}); + + expect(client.id).toEqual(account.id); + expect(client.name).toEqual(newAccountData.name); + expect(client.email).toEqual(newAccountData.email); + expect(client.fi).toEqual(newAccountData.fi); + expect(client.socialName).toEqual(newAccountData.socialName); }); }); diff --git a/services/loopback/common/models/client.js b/services/loopback/common/models/client.js index b09c38688..f55d23021 100644 --- a/services/loopback/common/models/client.js +++ b/services/loopback/common/models/client.js @@ -35,7 +35,7 @@ module.exports = function(Self) { message: 'Correo electrónico inválido', allowNull: true, allowBlank: true, - with: /^[\w|-|.]+@[\w|-]+(\.[\w|-]+)*(,[\w|-|.]+@[\w|-]+(\.[\w|-]+)*)*$/ + with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/ }); Self.validatesLengthOf('postcode', { allowNull: true,