Merge branch '1757-drag_and_drop_department' of verdnatura/salix into dev
gitea/salix/dev This commit looks good Details

LGTM
This commit is contained in:
Joan Sanchez 2019-10-17 12:13:18 +00:00 committed by Gitea
commit f78fec42cc
27 changed files with 772 additions and 314 deletions

View File

@ -0,0 +1,20 @@
<div class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: !$ctrl.item.sons}"
icon="keyboard_arrow_down"
translate-attr="::{title: 'Toggle'}">
</vn-icon>
<section class="content"></section>
<section class="buttons" ng-if="::!$ctrl.treeview.readOnly">
<vn-icon-button translate-attr="::{title: 'Remove'}"
icon="delete"
ng-click="$ctrl.treeview.onRemove($ctrl.item)"
ng-if="$ctrl.item.parent">
</vn-icon-button>
<vn-icon-button translate-attr="::{title: 'Create'}"
icon="add_circle"
ng-click="$ctrl.treeview.onCreate($ctrl.item)">
</vn-icon-button>
</section>
</div>

View File

@ -2,22 +2,33 @@ import ngModule from '../../module';
class Controller {
constructor($element, $scope, $compile) {
this.$element = $element;
this.$scope = $scope;
this.$compile = $compile;
this.element = $element[0];
this.element.$ctrl = this;
this.element.droppable = true;
this.dropCount = 0;
this.element.classList.add('vn-droppable');
}
$onInit() {
const transcludeElement = this.element.querySelector('.content');
const content = angular.element(transcludeElement);
if (this.item.parent) {
this.treeview.$transclude(($clone, $scope) => {
this.$contentScope = $scope;
$scope.item = this.item;
this.$element.append($clone);
content.append($clone);
});
this.element.draggable = true;
this.element.classList.add('vn-draggable');
} else {
let template = `<span translate>{{$ctrl.treeview.rootLabel}}</span>`;
let $clone = this.$compile(template)(this.$scope);
this.$element.append($clone);
content.append($clone);
}
}
@ -25,10 +36,21 @@ class Controller {
if (this.$contentScope)
this.$contentScope.$destroy();
}
dragEnter() {
this.dropCount++;
if (element != this.dropping) {
this.undrop();
if (element) element.classList.add('dropping');
this.dropping = element;
}
}
}
Controller.$inject = ['$element', '$scope', '$compile'];
ngModule.component('vnTreeviewContent', {
ngModule.component('vnTreeviewChild', {
template: require('./child.html'),
controller: Controller,
bindings: {
item: '<'

View File

@ -1,34 +1,11 @@
<ul ng-if="$ctrl.items">
<li ng-repeat="item in $ctrl.items" >
<div
ng-class="{expanded: item.active}"
ng-click="$ctrl.onClick($event, item)"
class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: !item.sons}"
icon="keyboard_arrow_down"
translate-attr="::{title: 'Toggle'}">
</vn-icon>
<vn-treeview-content
item="::item">
</vn-treeview-content>
<section class="buttons" ng-if="::!$ctrl.treeview.readOnly">
<vn-icon-button translate-attr="::{title: 'Remove'}"
icon="delete"
ng-click="$ctrl.treeview.onRemove(item)"
ng-if="item.parent">
</vn-icon-button>
<vn-icon-button translate-attr="::{title: 'Create'}"
icon="add_circle"
ng-click="$ctrl.treeview.onCreate(item)">
</vn-icon-button>
</section>
</div>
<li ng-repeat="item in $ctrl.items">
<vn-treeview-child item="item" ng-class="{expanded: item.active}"
ng-click="$ctrl.onClick($event, item)">
</vn-treeview-child>
<vn-treeview-childs
items="item.childs"
parent="::item">
items="item.childs">
</vn-treeview-childs>
</li>
</ul>

View File

@ -7,10 +7,6 @@ class Controller extends Component {
event.preventDefault();
this.treeview.onToggle(item);
}
onDrop(item, dragged, dropped) {
this.treeview.onDrop(item, dragged, dropped);
}
}
ngModule.component('vnTreeviewChilds', {

View File

@ -1,18 +1,110 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import './style.scss';
import './childs';
import './content';
import './child';
/**
* Treeview
*/
export default class Treeview extends Component {
constructor($element, $scope, $transclude) {
constructor($element, $scope, $transclude, $window) {
super($element, $scope);
this.$transclude = $transclude;
this.$window = $window;
this.readOnly = true;
this.element.addEventListener('dragstart',
event => this.dragStart(event));
this.element.addEventListener('dragend',
event => this.dragEnd(event));
this.element.addEventListener('dragover',
event => this.dragOver(event));
this.element.addEventListener('drop',
event => this.drop(event));
this.element.addEventListener('dragenter',
event => this.dragEnter(event));
this.element.addEventListener('dragleave',
event => this.dragLeave(event));
this.dropCount = 0;
}
undrop() {
if (!this.dropping) return;
this.dropping.classList.remove('dropping');
this.dropping = null;
}
findDroppable(event) {
let element = event.target;
while (element != this.element && !element.droppable)
element = element.parentNode;
if (element == this.element)
return null;
return element;
}
dragOver(event) {
this.dragClientY = event.clientY;
// Prevents page reload
event.preventDefault();
}
onDragInterval() {
if (this.dragClientY > 0 && this.dragClientY < 75)
this.$window.scrollTo(0, this.$window.scrollY - 25);
}
dragStart(event) {
event.target.classList.add('dragging');
event.dataTransfer.setData('text', event.target.id);
const element = this.findDroppable(event);
this.dragging = element;
this.interval = setInterval(() => this.onDragInterval(), 100);
}
dragEnd(event) {
event.target.classList.remove('dragging');
this.undrop();
this.dropCount = 0;
this.dragging = null;
clearInterval(this.interval);
}
dragEnter(event) {
let element = this.findDroppable(event);
if (element) this.dropCount++;
if (element != this.dropping) {
this.undrop();
if (element) element.classList.add('dropping');
this.dropping = element;
}
}
dragLeave(event) {
let element = this.findDroppable(event);
if (element) {
this.dropCount--;
if (this.dropCount == 0) this.undrop();
}
}
drop(event) {
event.preventDefault();
this.element.classList.remove('dropping');
const $dropped = this.dropping.$ctrl.item;
const $dragged = this.dragging.$ctrl.item;
if ($dropped != $dragged.parent)
this.emit('drop', {$dropped, $dragged});
}
get data() {
@ -109,7 +201,10 @@ export default class Treeview extends Component {
let childs = parent.childs;
if (!childs) childs = [];
childs.push(item);
if (!parent.active)
this.unfold(parent);
else
childs.push(item);
if (this.sortFunc) {
childs = childs.sort((a, b) =>
@ -120,12 +215,30 @@ export default class Treeview extends Component {
if (parent) parent.sons++;
}
onDrop(item, dragged, dropped) {
this.emit('drop', {item, dragged, dropped});
move(item, newParent) {
if (newParent == item) return;
if (item.parent) {
const parent = item.parent;
const childs = parent.childs;
const index = childs.indexOf(item);
parent.sons--;
childs.splice(index, 1);
}
item.parent = newParent;
if (!newParent.active) {
this.unfold(newParent).then(() => {
item.parent.sons++;
});
} else
this.create(item);
}
}
Treeview.$inject = ['$element', '$scope', '$transclude'];
Treeview.$inject = ['$element', '$scope', '$transclude', '$window'];
ngModule.component('vnTreeview', {
template: require('./index.html'),

View File

@ -0,0 +1,260 @@
describe('Component vnTreeview', () => {
let controller;
let $element;
beforeEach(angular.mock.module('vnCore', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(inject(($compile, $rootScope) => {
$element = $compile(`<vn-treeview></vn-treeview>`)($rootScope);
controller = $element.controller('vnTreeview');
const promise = new Promise(() => {
return {name: 'My item'};
});
controller.fetchFunc = () => {
return promise;
};
controller.createFunc = () => {
return promise;
};
controller.removeFunc = () => {
return promise;
};
controller.sortFunc = () => {
return promise;
};
}));
afterEach(() => {
$element.remove();
});
// See how to test DOM element in Jest
xdescribe('undrop()', () => {
it(`should reset all drop events and properties`, () => {
controller.dropping = angular.element(`<vn-treeview-child class="dropping"></vn-treeview-child>`);
spyOn(controller.dropping.classList, 'remove');
controller.undrop();
expect(controller.dropping).toBeNull();
});
});
describe('dragOver()', () => {
it(`should set the dragClientY property`, () => {
const event = new Event('dragover');
event.clientY = 100;
controller.dragOver(event);
expect(controller.dragClientY).toEqual(100);
});
});
describe('data() setter', () => {
it(`should set the items property nested into a root element`, () => {
const items = [{name: 'Item1'}, {name: 'Item2'}];
controller.data = items;
const rootItem = controller.items[0];
expect(rootItem.childs).toEqual(items);
});
});
describe('fetch()', () => {
it(`should call the fetchFunc() method`, () => {
spyOn(controller, 'fetchFunc').and.returnValue(
new Promise(resolve => resolve([{name: 'My item'}]))
);
controller.fetch().then(() => {
expect(controller.data).toBeDefined();
});
expect(controller.fetchFunc).toHaveBeenCalledWith();
});
});
describe('setParent()', () => {
it(`should set the parent property recursively to each element of an item list`, () => {
spyOn(controller, 'setParent').and.callThrough();
const items = [{name: 'Item1'}, {name: 'Item2', childs: [
{name: 'Item3'}
]}];
const rootItem = {name: 'Nested tree', sons: items};
controller.setParent(rootItem, items);
expect(items[0].parent).toEqual(rootItem);
expect(items[1].parent).toEqual(rootItem);
expect(controller.setParent).toHaveBeenCalledWith(rootItem, items[1].childs);
});
});
describe('onToggle()', () => {
it(`should call the fold() or unfold() methods`, () => {
spyOn(controller, 'fold');
spyOn(controller, 'unfold');
const item = {name: 'My item'};
controller.onToggle(item);
item.active = true;
controller.onToggle(item);
expect(controller.unfold).toHaveBeenCalledWith(item);
expect(controller.fold).toHaveBeenCalledWith(item);
});
});
describe('fold()', () => {
it(`should remove the childs and set the active property to false`, () => {
const item = {name: 'My item', childs: [{name: 'Item 1'}], active: true};
controller.fold(item);
expect(item.childs).toBeUndefined();
expect(item.active).toBeFalsy();
});
});
describe('unfold()', () => {
it(`should unfold a parent item`, () => {
const expectedResponse = [{name: 'Item 1'}, {name: 'Item 2'}];
spyOn(controller, 'fetchFunc').and.returnValue(
new Promise(resolve => resolve(expectedResponse))
);
spyOn(controller, 'setParent');
spyOn(controller, 'sortFunc');
const parent = {name: 'My item', sons: 1};
const child = {name: 'Item 1'};
child.parent = parent;
parent.childs = [child];
controller.unfold(parent).then(() => {
expect(controller.fetchFunc).toHaveBeenCalledWith({$item: parent});
expect(controller.setParent).toHaveBeenCalledWith(parent, expectedResponse);
expect(controller.sortFunc).toHaveBeenCalledWith(jasmine.any(Object));
expect(parent.active).toBeTruthy();
});
});
});
describe('onRemove()', () => {
it(`should call the removeFunc() method`, () => {
spyOn(controller, 'removeFunc');
const item = {name: 'My item'};
controller.onRemove(item);
expect(controller.removeFunc).toHaveBeenCalledWith({$item: item});
});
});
describe('remove()', () => {
it(`should remove a child element`, () => {
const parent = {name: 'My item', sons: 1};
const child = {name: 'Item 1'};
child.parent = parent;
parent.childs = [child];
controller.remove(child);
expect(parent.childs).toEqual([]);
expect(parent.sons).toEqual(0);
});
});
describe('onCreate()', () => {
it(`should call the createFunc() method`, () => {
spyOn(controller, 'createFunc');
const parent = {name: 'My item'};
controller.onCreate(parent);
expect(controller.createFunc).toHaveBeenCalledWith({$parent: parent});
});
});
describe('create()', () => {
it(`should unfold an inactive parent and then create a child`, () => {
spyOn(controller, 'unfold');
spyOn(controller, 'sortFunc');
const parent = {name: 'My item', sons: 2, childs: [
{name: 'Item 1'},
{name: 'Item 2'}
]};
const child = {name: 'Item 3'};
child.parent = parent;
parent.childs.push(child);
controller.create(child);
expect(parent.sons).toEqual(3);
expect(controller.unfold).toHaveBeenCalledWith(parent);
expect(controller.sortFunc).toHaveBeenCalledWith(jasmine.any(Object));
expect(controller.sortFunc).toHaveBeenCalledTimes(2);
});
it(`should create a child on an active parent`, () => {
spyOn(controller, 'unfold');
spyOn(controller, 'sortFunc');
const parent = {name: 'My item', sons: 2, childs: [
{name: 'Item 1'},
{name: 'Item 2'}
], active: true};
const child = {name: 'Item 3'};
child.parent = parent;
controller.create(child);
expect(parent.sons).toEqual(3);
expect(controller.unfold).not.toHaveBeenCalledWith(parent);
expect(controller.sortFunc).toHaveBeenCalledWith(jasmine.any(Object));
expect(controller.sortFunc).toHaveBeenCalledTimes(2);
});
});
describe('move()', () => {
it(`should move an item to anocher parent and then unfold the parent`, () => {
spyOn(controller, 'unfold').and.returnValue(
new Promise(resolve => resolve())
);
const newParent = {name: 'My item 2', sons: 0};
const parent = {name: 'My item', sons: 3, childs: [
{name: 'Item 1'},
{name: 'Item 2'}
]};
const child = {name: 'Item 3'};
child.parent = parent;
parent.childs.push(child);
controller.move(child, newParent);
expect(parent.sons).toEqual(2);
expect(controller.unfold).toHaveBeenCalledWith(newParent);
});
it(`should move an item to anocher parent`, () => {
spyOn(controller, 'unfold');
spyOn(controller, 'create');
const newParent = {name: 'My item 2', sons: 0, active: true};
const parent = {name: 'My item', sons: 3, childs: [
{name: 'Item 1'},
{name: 'Item 2'}
]};
const child = {name: 'Item 3'};
child.parent = parent;
parent.childs.push(child);
controller.move(child, newParent);
expect(parent.sons).toEqual(2);
expect(controller.unfold).not.toHaveBeenCalledWith(newParent);
});
});
});

View File

@ -10,22 +10,6 @@ vn-treeview-childs {
li {
list-style: none;
& > .node {
@extend %clickable;
display: flex;
padding: 5px;
align-items: center;
}
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
& > div.expanded > .arrow {
transform: rotate(180deg);
}
ul {
padding-left: 2.2em;
}
@ -45,8 +29,28 @@ vn-treeview-childs {
.node:hover > .buttons {
display: block
}
.content {
flex-grow: 1
}
}
vn-treeview-content {
flex-grow: 1
}
vn-treeview-child {
font-size: 16px;
display: block;
.node {
@extend %clickable;
display: flex;
padding: 5px;
align-items: center;
}
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
&.expanded > div > .arrow {
transform: rotate(180deg);
}
}

View File

@ -1,43 +0,0 @@
import ngModule from '../module';
/**
* Enables a draggable element and his drag events
*
* @return {Object} The directive
*/
export function directive() {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
const element = $element[0];
const isDraggable = $attrs.vnDraggable === 'true';
if (!isDraggable) return;
// Set draggable style properties
element.style.cursor = 'move';
// Enable as draggable element
element.setAttribute('draggable', true);
/**
* Fires when a drag event starts
*/
element.addEventListener('dragstart', event => {
element.style.opacity = 0.5;
event.stopPropagation();
});
/**
* Fires when a drag event ends
*/
element.addEventListener('dragend', event => {
element.style.opacity = 1;
event.stopPropagation();
});
}
};
}
ngModule.directive('vnDraggable', directive);

View File

@ -1,68 +1,43 @@
import ngModule from '../module';
import './droppable.scss';
export function directive($parse) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
const element = $element[0];
const onDropEvent = $parse($attrs.onDrop);
const isDroppable = $attrs.vnDroppable === 'true';
class Controller {
constructor($element, $, $attrs) {
this.element = $element[0];
this.$ = $;
this.$attrs = $attrs;
if (!isDroppable) return;
this.element.addEventListener('dragover',
event => event.preventDefault()); // Prevents page reload
this.element.addEventListener('drop',
event => this.drop(event));
this.element.addEventListener('dragenter',
event => this.dragEnter(event));
this.element.addEventListener('dragleave',
event => this.dragLeave(event));
}
/**
* Captures current dragging element
*/
element.addEventListener('dragstart', () => {
this.dragged = element;
});
dragEnter(event) {
this.droppedElement = event.target;
this.element.classList.add('dropping');
}
/**
* Enter droppable area event
*/
element.addEventListener('dragenter', event => {
element.classList.add('active');
dragLeave(event) {
if (this.droppedElement === event.target)
this.element.classList.remove('dropping');
}
event.stopImmediatePropagation();
event.preventDefault();
}, false);
/**
* Exit droppable area event
*/
element.addEventListener('dragleave', event => {
element.classList.remove('active');
event.stopImmediatePropagation();
event.preventDefault();
});
/**
* Prevent dragover for allowing
* dispatch drop event
*/
element.addEventListener('dragover', event => {
event.stopPropagation();
event.preventDefault();
});
/**
* Fires when a drop events
*/
element.addEventListener('drop', event => {
element.classList.remove('active');
onDropEvent($scope, {event});
event.stopPropagation();
event.preventDefault();
});
}
};
drop(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.element.classList.remove('dropping');
this.$.$eval(this.$attrs.vnDroppable, {$event: event});
}
}
Controller.$inject = ['$element', '$scope', '$attrs'];
directive.$inject = ['$parse'];
ngModule.directive('vnDroppable', directive);
ngModule.directive('vnDroppable', () => {
return {
controller: Controller
};
});

View File

@ -1,11 +1,25 @@
@import "./variables";
.vn-droppable,
.vn-draggable,
[vn-droppable] {
border: 2px dashed transparent;
border-radius: 0.5em;
transition: all 0.5s;
}
&.active {
.vn-droppable,
[vn-droppable] {
display: block;
&.dropping {
background-color: $color-hover-cd;
border: 2px dashed $color-bg-dark;
border-color: $color-bg-dark;
}
}
.vn-draggable.dragging {
background-color: $color-main-light;
border-color: $color-main;
}

View File

@ -11,7 +11,6 @@ import './bind';
import './repeat-last';
import './title';
import './uvc';
import './draggable';
import './droppable';
import './http-click';
import './http-submit';

View File

@ -24,10 +24,12 @@ export default class EventEmitter {
*/
off(callback) {
if (!this.$events) return;
for (let event in this.$events)
for (let i = 0; i < event.length; i++)
for (let event in this.$events) {
for (let i = 0; i < event.length; i++) {
if (event[i].callback === callback)
event.splice(i--, 1);
}
}
}
/**
@ -37,10 +39,12 @@ export default class EventEmitter {
*/
disconnect(thisArg) {
if (!this.$events) return;
for (let event in this.$events)
for (let i = 0; i < event.length; i++)
for (let event in this.$events) {
for (let i = 0; i < event.length; i++) {
if (event[i].thisArg === thisArg)
event.splice(i--, 1);
}
}
}
/**
@ -72,9 +76,10 @@ export default class EventEmitter {
if (this[prop])
this[prop].disconnect(this);
this[prop] = value;
if (value)
if (value) {
for (let event in handlers)
value.on(event, handlers[event], this);
}
}
}
}

View File

@ -109,5 +109,7 @@
"is invalid": "is invalid",
"The postcode doesn't exists. Ensure you put the correct format": "El código postal no existe. Asegúrate de ponerlo con el formato correcto",
"The department name can't be repeated": "El nombre del departamento no puede repetirse",
"You cannot move a parent to any of its sons": "You cannot move a parent to any of its sons",
"You cannot move a parent to its own sons": "You cannot move a parent to its own sons",
"You can't create a claim for a removed ticket": "No puedes crear una reclamación para un ticket eliminado"
}

View File

@ -1,14 +1,14 @@
@import "variables";
vn-treeview-content {
& > vn-check:not(.indeterminate) {
vn-treeview-child {
.content > vn-check:not(.indeterminate) {
color: $color-main;
& > .btn {
border-color: $color-main;
}
}
& > vn-check.checked {
.content > vn-check.checked {
color: $color-main;
}
}

View File

@ -1,121 +1,116 @@
<vn-crud-model auto-load="false"
<vn-crud-model auto-load="true"
vn-id="model"
url="claim/api/ClaimEnds"
filter="$ctrl.filter"
data="$ctrl.salesClaimed">
</vn-crud-model>
<vn-vertical compact>
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-horizontal>
<div class="totalBox" ng-show="$ctrl.salesClaimed.length > 0">
<vn-label-value label="Total claimed"
value="{{$ctrl.claimedTotal | currency: 'EUR':2}}">
</vn-label-value>
</div>
</vn-horizontal>
<vn-horizontal>
<vn-tool-bar class="vn-mb-md">
<vn-button
label="Import claim"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedState"
vn-http-click="$ctrl.importToNewRefundTicket()"p
vn-tooltip="Imports claim details">
</vn-button>
<vn-button
label="Import ticket"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedState"
ng-click="$ctrl.showLastTickets($event)"
vn-tooltip="Imports ticket lines">
</vn-button>
<vn-input-range
vn-one
label="Responsability"
value="$ctrl.claim.responsibility"
max="$ctrl.maxResponsibility"
min="1"
step="1"
vn-acl="salesAssistant"
on-change="$ctrl.saveResponsibility(value)">
</vn-input-range>
</vn-tool-bar>
<vn-one
style = "text-align:right">
<vn-check
vn-one
label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana"
vn-acl="salesAssistant"
on-change="$ctrl.saveMana(value)">
</vn-check>
</vn-one>
</vn-horizontal>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th number>Id</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th>Destination</vn-th>
<vn-th>Landed</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th>Description</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Disc.</vn-th>
<vn-th number>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr
ng-repeat="saleClaimed in $ctrl.salesClaimed"
vn-repeat-last on-last="$ctrl.focusLastInput()">
<vn-td number>
<span
ng-click="$ctrl.showDescriptor($event, saleClaimed.sale.itemFk)"
class="link">
{{saleClaimed.sale.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td number>
<span
class="link"
ng-click="$ctrl.showTicketDescriptor($event, saleClaimed.sale.ticketFk)">
{{::saleClaimed.sale.ticketFk}}
<vn-card class="vn-mb-md vn-pa-lg vn-w-lg" style="text-align: right"
ng-if="$ctrl.salesClaimed.length > 0">
<vn-label-value label="Total claimed"
value="{{$ctrl.claimedTotal | currency: 'EUR':2}}">
</vn-label-value>
</vn-card>
<vn-card class="vn-pa-lg vn-w-lg">
<section class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button
label="Import claim"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedState"
vn-http-click="$ctrl.importToNewRefundTicket()"p
translate-attr="{title: 'Imports claim details'}">
</vn-button>
<vn-button
label="Import ticket"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedState"
ng-click="$ctrl.showLastTickets($event)"
translate-attr="{title: 'Imports ticket lines'}">
</vn-button>
<vn-input-range vn-none
label="Responsability"
value="$ctrl.claim.responsibility"
max="$ctrl.maxResponsibility"
min="1"
step="1"
vn-acl="salesAssistant"
on-change="$ctrl.saveResponsibility(value)">
</vn-input-range>
</vn-tool-bar>
<vn-check vn-one
label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana"
vn-acl="salesAssistant"
on-change="$ctrl.saveMana(value)">
</vn-check>
</section>
<vn-data-viewer model="model">
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th number>Id</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th>Destination</vn-th>
<vn-th>Landed</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th>Description</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Disc.</vn-th>
<vn-th number>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr
ng-repeat="saleClaimed in $ctrl.salesClaimed"
vn-repeat-last on-last="$ctrl.focusLastInput()">
<vn-td number>
<span
ng-click="$ctrl.showDescriptor($event, saleClaimed.sale.itemFk)"
class="link">
{{::saleClaimed.sale.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td>
<vn-autocomplete
vn-one
id="claimDestinationFk"
ng-model="saleClaimed.claimDestinationFk"
url="/claim/api/ClaimDestinations"
fields="['id','description']"
value-field="id"
show-field="description"
on-change="$ctrl.setClaimDestination(saleClaimed.id, value)">
</vn-autocomplete>
</vn-td>
<vn-td>{{saleClaimed.sale.ticket.landed | dateTime: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{saleClaimed.sale.quantity}}</vn-td>
<vn-td expand>{{saleClaimed.sale.concept}}</vn-td>
<vn-td number>{{saleClaimed.sale.price | currency: 'EUR':2}}</vn-td>
<vn-td number>{{saleClaimed.sale.discount}} %</vn-td>
<vn-td number>
{{saleClaimed.sale.quantity * saleClaimed.sale.price *
((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}}
</vn-td>
<vn-td shrink>
<vn-icon-button
vn-tooltip="Remove line"
icon="delete"
ng-click="$ctrl.deleteClaimedSale(saleClaimed.id)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-vertical>
</vn-card>
</vn-td>
<vn-td number>
<span
class="link"
ng-click="$ctrl.showTicketDescriptor($event, saleClaimed.sale.ticketFk)">
{{::saleClaimed.sale.ticketFk}}
</span>
</vn-td>
<vn-td>
<vn-autocomplete vn-one
id="claimDestinationFk"
ng-model="saleClaimed.claimDestinationFk"
url="/claim/api/ClaimDestinations"
fields="['id','description']"
value-field="id"
show-field="description"
on-change="$ctrl.setClaimDestination(saleClaimed.id, value)">
</vn-autocomplete>
</vn-td>
<vn-td>{{::saleClaimed.sale.ticket.landed | dateTime: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::saleClaimed.sale.quantity}}</vn-td>
<vn-td expand>{{::saleClaimed.sale.concept}}</vn-td>
<vn-td number>{{::saleClaimed.sale.price | currency: 'EUR':2}}</vn-td>
<vn-td number>{{::saleClaimed.sale.discount}} %</vn-td>
<vn-td number>
{{saleClaimed.sale.quantity * saleClaimed.sale.price *
((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}}
</vn-td>
<vn-td shrink>
<vn-icon-button
vn-tooltip="Remove line"
icon="delete"
ng-click="$ctrl.deleteClaimedSale(saleClaimed.id)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-data-viewer>
<vn-button-bar>
<vn-button
label="Regularize"
@ -123,11 +118,7 @@
vn-http-click="$ctrl.regularize()">
</vn-button>
</vn-button-bar>
<!-- WIP
<a ng-click="$ctrl.openAddSalesDialog()" vn-tooltip="New item" vn-bind="+" fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
-->
</vn-card>
<!-- Add Lines Dialog -->
<vn-dialog vn-id="addSales">

View File

@ -1,4 +1,19 @@
vn-claim-action {
.header {
display: flex;
justify-content: space-between;
align-items: center;
align-content: center;
vn-tool-bar {
flex: 1
}
vn-check {
flex: none;
}
}
vn-dialog[vn-id=addSales] {
tpl-body {
width: 950px;

View File

@ -5,7 +5,7 @@
data="$ctrl.photos">
</vn-crud-model>
<section class="drop-zone" vn-droppable="true" on-drop="$ctrl.onDrop(event)">
<section class="drop-zone" vn-droppable="$ctrl.onDrop($event)">
<section><vn-icon icon="add_circle"></vn-icon></section>
<section translate>Drag & Drop files here...</section>
</section>

View File

@ -36,8 +36,9 @@ class Controller {
}
}
onDrop(event) {
const files = event.dataTransfer.files;
onDrop($event) {
console.log($event);
const files = $event.dataTransfer.files;
this.setDefaultParams().then(() => {
this.dms.files = files;
this.create();

View File

@ -1,6 +1,6 @@
import './index.js';
fdescribe('Ticket component vnTicketService', () => {
describe('Ticket component vnTicketService', () => {
let controller;
let $httpBackend;
let $httpParamSerializer;

View File

@ -0,0 +1,42 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('moveChild', {
description: 'Changes the parent of a child department',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The department id',
http: {source: 'path'}
}, {
arg: 'parentId',
type: 'Number',
description: 'New parent id',
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/moveChild`,
verb: 'POST'
}
});
Self.moveChild = async(id, parentId = null) => {
const models = Self.app.models;
const child = await models.Department.findById(id);
if (id == parentId) return;
if (parentId) {
const parent = await models.Department.findById(parentId);
if (child.lft < parent.lft && child.rgt > parent.rgt)
throw new UserError('You cannot move a parent to its own sons');
}
return child.updateAttribute('parentFk', parentId);
};
};

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('department createChild()', () => {
let createdChild;
afterAll(async done => {
await createdChild.destroy();
done();
});
it('should create a new child', async() => {
const parentId = null;
createdChild = await app.models.Department.createChild(parentId, 'My new department');
expect(createdChild.name).toEqual('My new department');
expect(createdChild.parentFk).toBeNull();
});
});

View File

@ -0,0 +1,23 @@
const app = require('vn-loopback/server/server');
describe('department moveChild()', () => {
let updatedChild;
afterAll(async done => {
const child = await app.models.Department.findById(updatedChild.id);
await child.updateAttribute('parentFk', null);
done();
});
it('should move a child department to a new parent', async() => {
const childId = 22;
const parentId = 1;
const child = await app.models.Department.findById(childId);
expect(child.parentFk).toBeNull();
updatedChild = await app.models.Department.moveChild(childId, parentId);
expect(updatedChild.parentFk).toEqual(1);
});
});

View File

@ -0,0 +1,21 @@
const app = require('vn-loopback/server/server');
describe('department removeChild()', () => {
let removedChild;
afterAll(async done => {
await app.models.Department.create(removedChild);
done();
});
it('should remove a child department', async() => {
const childId = 1;
removedChild = await app.models.Department.findById(childId);
const result = await app.models.Department.removeChild(childId);
const existsChild = await app.models.Department.findById(childId);
expect(result.count).toEqual(1);
expect(existsChild).toBeNull();
});
});

View File

@ -2,4 +2,5 @@ module.exports = Self => {
require('../methods/department/getLeaves')(Self);
require('../methods/department/createChild')(Self);
require('../methods/department/removeChild')(Self);
require('../methods/department/moveChild')(Self);
};

View File

@ -16,6 +16,15 @@
},
"parentFk": {
"type": "Number"
},
"lft": {
"type": "Number"
},
"rgt": {
"type": "Number"
},
"sons": {
"type": "Number"
}
}
}

View File

@ -10,7 +10,10 @@
fetch-func="$ctrl.onFetch($item)"
remove-func="$ctrl.onRemove($item)"
create-func="$ctrl.onCreate($parent)"
sort-func="$ctrl.onSort($a, $b)">
sort-func="$ctrl.onSort($a, $b)"
on-drop="$ctrl.onDrop($dropped, $dragged)"
on-drag-start="$ctrl.onDragStart(item)"
on-drag-end="$ctrl.onDragEnd(item)">
{{::item.name}}
</vn-treeview>
</vn-card>

View File

@ -23,19 +23,13 @@ class Controller {
return a.name.localeCompare(b.name);
}
/* onDrop(item, dragged, dropped) {
if (dropped.scope.item) {
const droppedItem = dropped.scope.item;
const draggedItem = dragged.scope.item;
if (droppedItem.childs)
droppedItem.childs.push(Object.assign({}, draggedItem));
dragged.element.remove();
this.$scope.$apply();
}
} */
onDrop(dropped, dragged) {
const params = dropped ? {parentId: dropped.id} : null;
const query = `/api/departments/${dragged.id}/moveChild`;
this.$http.post(query, params).then(() => {
this.$.treeview.move(dragged, dropped);
});
}
onCreate(parent) {
this.newChild = {
@ -62,12 +56,8 @@ class Controller {
if (parent && parent.id)
params.parentId = parent.id;
if (!parent.active)
this.$.treeview.unfold(parent);
const query = `/api/departments/createChild`;
this.$http.post(query, params).then(res => {
const parent = this.newChild.parent;
const item = res.data;
item.parent = parent;