This commit is contained in:
Juan Ferrer 2019-10-08 23:57:54 +02:00
commit fc09d76422
46 changed files with 941 additions and 581 deletions

View File

@ -0,0 +1,83 @@
ALTER TABLE `vn2008`.`department`
ADD COLUMN `parentFk` INT UNSIGNED NULL AFTER `sons`,
ADD COLUMN `path` VARCHAR(255) NULL AFTER `parentFk`,
CHANGE COLUMN `sons` `sons` DECIMAL(10,0) NOT NULL DEFAULT '0' ;
USE `vn`;
CREATE
OR REPLACE ALGORITHM = UNDEFINED
DEFINER = `root`@`%`
SQL SECURITY DEFINER
VIEW `department` AS
SELECT
`b`.`department_id` AS `id`,
`b`.`name` AS `name`,
`b`.`production` AS `isProduction`,
`b`.`parentFk` AS `parentFk`,
`b`.`path` AS `path`,
`b`.`lft` AS `lft`,
`b`.`rgt` AS `rgt`,
`b`.`isSelected` AS `isSelected`,
`b`.`depth` AS `depth`,
`b`.`sons` AS `sons`
FROM
`vn2008`.`department` `b`;
DROP TRIGGER IF EXISTS `vn2008`.`department_AFTER_DELETE`;
DELIMITER $$
USE `vn2008`$$
CREATE DEFINER = CURRENT_USER TRIGGER `vn2008`.`department_AFTER_DELETE`
AFTER DELETE ON `department` FOR EACH ROW
BEGIN
UPDATE vn.department_recalc SET isChanged = TRUE;
END$$
DELIMITER ;
DROP TRIGGER IF EXISTS `vn2008`.`department_BEFORE_INSERT`;
DELIMITER $$
USE `vn2008`$$
CREATE DEFINER = CURRENT_USER TRIGGER `vn2008`.`department_BEFORE_INSERT`
BEFORE INSERT ON `department` FOR EACH ROW
BEGIN
UPDATE vn.department_recalc SET isChanged = TRUE;
END$$
DELIMITER ;
DROP TRIGGER IF EXISTS `vn2008`.`department_AFTER_UPDATE`;
DELIMITER $$
USE `vn2008`$$
CREATE DEFINER = CURRENT_USER TRIGGER `vn2008`.`department_AFTER_UPDATE`
AFTER UPDATE ON `department` FOR EACH ROW
BEGIN
IF !(OLD.parentFk <=> NEW.parentFk) THEN
UPDATE vn.department_recalc SET isChanged = TRUE;
END IF;
END$$
DELIMITER ;
CREATE TABLE `vn`.`department_recalc` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`isChanged` TINYINT(4) NOT NULL,
PRIMARY KEY (`id`));
INSERT INTO `vn`.`department_recalc` (`id`, `isChanged`) VALUES ('1', '0');
ALTER TABLE `vn2008`.`department`
CHANGE COLUMN `lft` `lft` INT(11) NULL ,
CHANGE COLUMN `rgt` `rgt` INT(11) NULL ;
ALTER TABLE `vn2008`.`department`
DROP INDEX `rgt_UNIQUE` ,
DROP INDEX `lft_UNIQUE` ;
;
ALTER TABLE `vn2008`.`department`
ADD INDEX `lft_rgt_depth_idx` (`lft` ASC, `rgt` ASC, `depth` ASC);
;
UPDATE vn.department SET lft = NULL, rgt = NULL;

View File

@ -0,0 +1,37 @@
USE `vn`;
DROP procedure IF EXISTS `department_calcTree`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_calcTree`()
BEGIN
/**
* Calculates the #path, #lft, #rgt, #sons and #depth columns of
* the #department table. To build the tree, it uses the #parentFk
* column.
*/
DECLARE vIndex INT DEFAULT 0;
DECLARE vSons INT;
DROP TEMPORARY TABLE IF EXISTS tNestedTree;
CREATE TEMPORARY TABLE tNestedTree
SELECT id, path, lft, rgt, depth, sons
FROM department LIMIT 0;
SET max_sp_recursion_depth = 5;
CALL department_calcTreeRec(NULL, '/', 0, vIndex, vSons);
SET max_sp_recursion_depth = 0;
UPDATE department z
JOIN tNestedTree t ON t.id = z.id
SET z.path = t.path,
z.lft = t.lft,
z.rgt = t.rgt,
z.depth = t.depth,
z.sons = t.sons;
DROP TEMPORARY TABLE tNestedTree;
END$$
DELIMITER ;

View File

@ -0,0 +1,75 @@
USE `vn`;
DROP procedure IF EXISTS `department_calcTreeRec`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_calcTreeRec`(
vSelf INT,
vPath VARCHAR(255),
vDepth INT,
INOUT vIndex INT,
OUT vSons INT
)
BEGIN
/**
* Calculates and sets the #path, #lft, #rgt, #sons and #depth
* columns for all children of the passed node. Once calculated
* the last node rgt index and the number of sons are returned.
* To update it's children, this procedure calls itself recursively
* for each one.
*
* @vSelf The node identifier
* @vPath The initial path
* @vDepth The initial depth
* @vIndex The initial lft index
* @vSons The number of direct sons
*/
DECLARE vChildFk INT;
DECLARE vLft INT;
DECLARE vMySons INT;
DECLARE vDone BOOL;
DECLARE vChildren CURSOR FOR
SELECT id FROM department
WHERE (vSelf IS NULL AND parentFk IS NULL)
OR (vSelf IS NOT NULL AND parentFk = vSelf);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
SET vSons = 0;
OPEN vChildren;
myLoop: LOOP
SET vDone = FALSE;
FETCH vChildren INTO vChildFk;
IF vDone THEN
LEAVE myLoop;
END IF;
SET vIndex = vIndex + 1;
SET vLft = vIndex;
SET vSons = vSons + 1;
CALL department_calcTreeRec(
vChildFk,
CONCAT(vPath, vChildFk, '/'),
vDepth + 1,
vIndex,
vMySons
);
SET vIndex = vIndex + 1;
INSERT INTO tNestedTree
SET id = vChildFk,
path = vPath,
lft = vLft,
rgt = vIndex,
depth = vDepth,
sons = vMySons;
END LOOP;
CLOSE vChildren;
END$$
DELIMITER ;

View File

@ -0,0 +1,35 @@
USE `vn`;
DROP procedure IF EXISTS `department_doCalc`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_doCalc`()
proc: BEGIN
/**
* Recalculates the department tree.
*/
DECLARE vIsChanged BOOL;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
DO RELEASE_LOCK('vn.department_doCalc');
RESIGNAL;
END;
IF !GET_LOCK('vn.department_doCalc', 0) THEN
LEAVE proc;
END IF;
SELECT isChanged INTO vIsChanged
FROM department_recalc;
IF vIsChanged THEN
UPDATE department_recalc SET isChanged = FALSE;
CALL vn.department_calcTree;
END IF;
DO RELEASE_LOCK('vn.department_doCalc');
END$$
DELIMITER ;

View File

@ -0,0 +1,84 @@
USE `vn`;
DROP procedure IF EXISTS `department_getLeaves`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `department_getLeaves`(
vParentFk INT,
vSearch VARCHAR(255)
)
BEGIN
DECLARE vIsNumber BOOL;
DECLARE vIsSearch BOOL DEFAULT vSearch IS NOT NULL AND vSearch != '';
DROP TEMPORARY TABLE IF EXISTS tNodes;
CREATE TEMPORARY TABLE tNodes
(UNIQUE (id))
ENGINE = MEMORY
SELECT id FROM department LIMIT 0;
IF vIsSearch THEN
SET vIsNumber = vSearch REGEXP '^[0-9]+$';
INSERT INTO tNodes
SELECT id FROM department
WHERE (vIsNumber AND `name` = vSearch)
OR (!vIsNumber AND `name` LIKE CONCAT('%', vSearch, '%'))
LIMIT 1000;
END IF;
IF vParentFk IS NULL THEN
DROP TEMPORARY TABLE IF EXISTS tChilds;
CREATE TEMPORARY TABLE tChilds
ENGINE = MEMORY
SELECT id FROM tNodes;
DROP TEMPORARY TABLE IF EXISTS tParents;
CREATE TEMPORARY TABLE tParents
ENGINE = MEMORY
SELECT id FROM department LIMIT 0;
myLoop: LOOP
DELETE FROM tParents;
INSERT INTO tParents
SELECT parentFk id
FROM department g
JOIN tChilds c ON c.id = g.id
WHERE g.parentFk IS NOT NULL;
INSERT IGNORE INTO tNodes
SELECT id FROM tParents;
IF ROW_COUNT() = 0 THEN
LEAVE myLoop;
END IF;
DELETE FROM tChilds;
INSERT INTO tChilds
SELECT id FROM tParents;
END LOOP;
DROP TEMPORARY TABLE
tChilds,
tParents;
END IF;
IF !vIsSearch THEN
INSERT IGNORE INTO tNodes
SELECT id FROM department
WHERE parentFk <=> vParentFk;
END IF;
SELECT d.id,
d.`name`,
d.parentFk,
d.sons
FROM department d
JOIN tNodes n ON n.id = d.id
ORDER BY depth, `name`;
DROP TEMPORARY TABLE tNodes;
END$$
DELIMITER ;

View File

@ -260,6 +260,7 @@ export default {
submitItemTagsButton: `vn-item-tags ${components.vnSubmit}` submitItemTagsButton: `vn-item-tags ${components.vnSubmit}`
}, },
itemTax: { itemTax: {
undoChangesButton: 'vn-item-tax vn-button-bar > vn-button[label="Undo changes"]',
firstClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(1) > vn-autocomplete[field="tax.taxClassFk"]', firstClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(1) > vn-autocomplete[field="tax.taxClassFk"]',
secondClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(2) > vn-autocomplete[field="tax.taxClassFk"]', secondClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(2) > vn-autocomplete[field="tax.taxClassFk"]',
thirdClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(3) > vn-autocomplete[field="tax.taxClassFk"]', thirdClassAutocomplete: 'vn-item-tax vn-horizontal:nth-child(3) > vn-autocomplete[field="tax.taxClassFk"]',

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js'; import selectors from '../../helpers/selectors.js';
import createNightmare from '../../helpers/nightmare'; import createNightmare from '../../helpers/nightmare';
// #1702 Autocomplete no siempre refresca al cancelar formulario describe('Item edit tax path', () => {
xdescribe('Item edit tax path', () => {
const nightmare = createNightmare(); const nightmare = createNightmare();
beforeAll(() => { beforeAll(() => {
@ -14,9 +13,9 @@ xdescribe('Item edit tax path', () => {
it(`should add the item tax to all countries`, async() => { it(`should add the item tax to all countries`, async() => {
const result = await nightmare const result = await nightmare
.autocompleteSearch(selectors.itemTax.firstClassAutocomplete, 'Reduced VAT') .autocompleteSearch(selectors.itemTax.firstClassAutocomplete, 'General VAT')
.autocompleteSearch(selectors.itemTax.secondClassAutocomplete, 'General VAT') .autocompleteSearch(selectors.itemTax.secondClassAutocomplete, 'General VAT')
.autocompleteSearch(selectors.itemTax.thirdClassAutocomplete, 'Reduced VAT') .autocompleteSearch(selectors.itemTax.thirdClassAutocomplete, 'General VAT')
.waitToClick(selectors.itemTax.submitTaxButton) .waitToClick(selectors.itemTax.submitTaxButton)
.waitForLastSnackbar(); .waitForLastSnackbar();
@ -28,7 +27,7 @@ xdescribe('Item edit tax path', () => {
.reloadSection('item.card.tax') .reloadSection('item.card.tax')
.waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value'); .waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value');
expect(firstVatType).toEqual('Reduced VAT'); expect(firstVatType).toEqual('General VAT');
}); });
it(`should confirm the second item tax class was edited`, async() => { it(`should confirm the second item tax class was edited`, async() => {
@ -42,6 +41,22 @@ xdescribe('Item edit tax path', () => {
const thirdVatType = await nightmare const thirdVatType = await nightmare
.waitToGetProperty(`${selectors.itemTax.thirdClassAutocomplete} input`, 'value'); .waitToGetProperty(`${selectors.itemTax.thirdClassAutocomplete} input`, 'value');
expect(thirdVatType).toEqual('Reduced VAT'); expect(thirdVatType).toEqual('General VAT');
});
it(`should edit the first class without saving the form`, async() => {
const firstVatType = await nightmare
.autocompleteSearch(selectors.itemTax.firstClassAutocomplete, 'Reduced VAT')
.waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value');
expect(firstVatType).toEqual('Reduced VAT');
});
it(`should now click the undo changes button and see the changes works`, async() => {
const firstVatType = await nightmare
.waitToClick(selectors.itemTax.undoChangesButton)
.waitToGetProperty(`${selectors.itemTax.firstClassAutocomplete} input`, 'value');
expect(firstVatType).toEqual('General VAT');
}); });
}); });

View File

@ -121,7 +121,7 @@ export default class Autocomplete extends Field {
return; return;
const selection = this.fetchSelection(); const selection = this.fetchSelection();
if (!this.selection)
this.selection = selection; this.selection = selection;
} }

View File

@ -3,23 +3,28 @@ import './style.scss';
export default class IconButton { export default class IconButton {
constructor($element) { constructor($element) {
if ($element[0].getAttribute('tabindex') == null) this.element = $element[0];
$element[0].tabIndex = 0;
$element.on('keyup', event => this.onKeyDown(event, $element)); if (this.element.getAttribute('tabindex') == null)
let button = $element[0].querySelector('button'); this.element.tabIndex = 0;
$element[0].addEventListener('click', event => {
this.element.addEventListener('keyup', e => this.onKeyup(e));
this.element.addEventListener('click', e => this.onClick(e));
}
onKeyup(event) {
if (event.code == 'Space')
this.onClick(event);
}
onClick(event) {
if (event.defaultPrevented) return;
event.preventDefault();
// FIXME: Don't use Event.stopPropagation()
let button = this.element.querySelector('button');
if (this.disabled || button.disabled) if (this.disabled || button.disabled)
event.stopImmediatePropagation(); event.stopImmediatePropagation();
});
}
onKeyDown(event, $element) {
if (event.defaultPrevented) return;
if (event.keyCode == 13) {
event.preventDefault();
$element.triggerHandler('click');
}
} }
} }

View File

@ -45,4 +45,3 @@ import './table';
import './td-editable'; import './td-editable';
import './th'; import './th';
import './treeview'; import './treeview';
import './treeview/child';

View File

@ -1,61 +0,0 @@
<ul ng-if="::$ctrl.items">
<li
ng-repeat="item in $ctrl.items"
on-drop="$ctrl.onDrop(item, dragged, dropped)"
vn-draggable="{{::$ctrl.draggable}}"
vn-droppable="{{::$ctrl.droppable}}"
ng-class="{expanded: item.active}">
<div
ng-click="$ctrl.toggle($event, item)"
class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: item.sons == 0}"
icon="keyboard_arrow_down"
translate-attr="{title: 'Toggle'}">
</vn-icon>
<vn-check
vn-acl="{{$ctrl.aclRole}}"
ng-if="$ctrl.selectable"
field="item.selected"
disabled="$ctrl.disabled"
on-change="$ctrl.select(item, value)"
triple-state="true"
label="{{::item.name}}">
</vn-check>
<vn-icon-button
icon="{{icon.icon}}"
ng-repeat="icon in $ctrl.icons"
ng-click="$ctrl.onIconClick(icon, item, $ctrl.parent, $parent.$index)"
vn-acl="{{$ctrl.aclRole}}"
vn-acl-action="remove">
</vn-icon-button>
</div>
<vn-treeview-child
items="item.childs"
parent="item"
selectable="$ctrl.selectable"
disabled="$ctrl.disabled"
editable="$ctrl.editable"
draggable="::$ctrl.draggable"
droppable="::$ctrl.droppable"
icons="::$ctrl.icons"
parent-scope="::$ctrl.parentScope"
acl-role="$ctrl.aclRole">
</vn-treeview-child>
</li>
<li
ng-if="$ctrl.isInsertable && $ctrl.editable"
ng-click="$ctrl.onCreate($ctrl.parent)"
vn-acl="{{$ctrl.aclRole}}"
vn-acl-action="remove">
<div class="node">
<vn-icon-button
icon="add_circle">
</vn-icon-button>
<div class="description" translate>
Create new one
</div>
</div>
</li>
</ul>

View File

@ -1,56 +0,0 @@
import ngModule from '../../module';
import Component from '../../lib/component';
class Controller extends Component {
constructor($element, $scope) {
super($element, $scope);
this.$scope = $scope;
}
toggle(event, item) {
if (event.defaultPrevented || !item.sons) return;
event.preventDefault();
this.treeview.onToggle(item);
}
select(item, value) {
this.treeview.onSelection(item, value);
}
onIconClick(icon, item, parent, index) {
let parentController = this.parentScope.$ctrl;
icon.callback.call(parentController, item, parent, index);
}
onCreate(parent) {
this.treeview.onCreate(parent);
}
onDrop(item, dragged, dropped) {
this.treeview.onDrop(item, dragged, dropped);
}
get isInsertable() {
return Array.isArray(this.parent) || this.parent.childs;
}
}
ngModule.component('vnTreeviewChild', {
template: require('./child.html'),
controller: Controller,
bindings: {
items: '<',
parent: '<',
icons: '<?',
disabled: '<?',
selectable: '<?',
editable: '<?',
draggable: '<?',
droppable: '<?',
aclRole: '<?',
parentScope: '<'
},
require: {
treeview: '^vnTreeview'
}
});

View File

@ -0,0 +1,34 @@
<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>
<vn-treeview-childs
items="item.childs"
parent="::item">
</vn-treeview-childs>
</li>
</ul>

View File

@ -0,0 +1,26 @@
import ngModule from '../../module';
import Component from '../../lib/component';
class Controller extends Component {
onClick(event, item) {
if (event.defaultPrevented || !item.sons) return;
event.preventDefault();
this.treeview.onToggle(item);
}
onDrop(item, dragged, dropped) {
this.treeview.onDrop(item, dragged, dropped);
}
}
ngModule.component('vnTreeviewChilds', {
template: require('./childs.html'),
controller: Controller,
bindings: {
items: '<',
parent: '<?'
},
require: {
treeview: '^vnTreeview'
}
});

View File

@ -0,0 +1,39 @@
import ngModule from '../../module';
class Controller {
constructor($element, $scope, $compile) {
this.$element = $element;
this.$scope = $scope;
this.$compile = $compile;
}
$onInit() {
if (this.item.parent) {
this.treeview.$transclude(($clone, $scope) => {
this.$contentScope = $scope;
$scope.item = this.item;
this.$element.append($clone);
});
} else {
let template = `<span translate>{{$ctrl.treeview.rootLabel}}</span>`;
let $clone = this.$compile(template)(this.$scope);
this.$element.append($clone);
}
}
$onDestroy() {
if (this.$contentScope)
this.$contentScope.$destroy();
}
}
Controller.$inject = ['$element', '$scope', '$compile'];
ngModule.component('vnTreeviewContent', {
controller: Controller,
bindings: {
item: '<'
},
require: {
treeview: '^vnTreeview'
}
});

View File

@ -1,13 +1,3 @@
<vn-treeview-child <vn-treeview-childs
acl-role="$ctrl.aclRole" items="$ctrl.items">
items="$ctrl.data" </vn-treeview-childs>
parent="$ctrl.data"
selectable="$ctrl.selectable"
editable="$ctrl.editable"
disabled="$ctrl.disabled"
icons="$ctrl.icons"
parent-scope="$ctrl.$scope.$parent"
draggable="$ctrl.draggable"
droppable="$ctrl.droppable"
vn-droppable="{{$ctrl.droppable}}">
</vn-treeview-child>

View File

@ -2,50 +2,71 @@ import ngModule from '../../module';
import Component from '../../lib/component'; import Component from '../../lib/component';
import './style.scss'; import './style.scss';
import './childs';
import './content';
/** /**
* Treeview * Treeview
*
* @property {String} position The relative position to the parent
*/ */
export default class Treeview extends Component { export default class Treeview extends Component {
constructor($element, $scope) { constructor($element, $scope, $transclude) {
super($element, $scope); super($element, $scope);
this.$scope = $scope; this.$transclude = $transclude;
this.data = []; this.readOnly = true;
} }
$onInit() { get data() {
this.refresh(); return this._data;
} }
refresh() { set data(value) {
this.model.refresh().then(() => { this._data = value;
this.data = this.model.data;
const sons = value.length;
const rootElement = [{
childs: value,
active: true,
sons: sons
}];
this.setParent(rootElement[0], value);
this.items = rootElement;
}
fetch() {
return this.fetchFunc().then(res =>
this.data = res
);
}
setParent(parent, childs) {
childs.forEach(child => {
child.parent = parent;
if (child.childs)
this.setParent(parent, child.childs);
}); });
} }
/**
* Emits selection event
* @param {Object} item - Selected item
* @param {Boolean} value - Changed value
*/
onSelection(item, value) {
this.emit('selection', {item, value});
}
onCreate(parent) {
this.emit('create', {parent});
}
onToggle(item) { onToggle(item) {
if (item.active) if (item.active)
item.childs = undefined; this.fold(item);
else { else
this.model.applyFilter({}, {parentFk: item.id}).then(() => { this.unfold(item);
const newData = this.model.data; }
if (item.childs) { fold(item) {
let childs = item.childs; item.childs = undefined;
item.active = false;
}
unfold(item) {
return this.fetchFunc({$item: item}).then(newData => {
this.setParent(item, newData);
const childs = item.childs;
if (childs) {
childs.forEach(child => { childs.forEach(child => {
let index = newData.findIndex(newChild => { let index = newData.findIndex(newChild => {
return newChild.id == child.id; return newChild.id == child.id;
@ -54,21 +75,49 @@ export default class Treeview extends Component {
}); });
} }
item.childs = newData.sort((a, b) => { if (this.sortFunc) {
if (b.selected !== a.selected) { item.childs = newData.sort((a, b) =>
if (a.selected == null) this.sortFunc({$a: a, $b: b})
return 1; );
if (b.selected == null) }
return -1; }).then(() => item.active = true);
return b.selected - a.selected;
} }
return a.name.localeCompare(b.name); onRemove(item) {
}); if (this.removeFunc)
}); this.removeFunc({$item: item});
} }
item.active = !item.active; remove(item) {
const parent = item.parent;
let childs = parent.childs;
if (!childs) childs = [];
let index = childs.indexOf(item);
childs.splice(index, 1);
if (parent) parent.sons--;
}
onCreate(parent) {
if (this.createFunc)
this.createFunc({$parent: parent});
}
create(item) {
const parent = item.parent;
let childs = parent.childs;
if (!childs) childs = [];
childs.push(item);
if (this.sortFunc) {
childs = childs.sort((a, b) =>
this.sortFunc({$a: a, $b: b})
);
}
if (parent) parent.sons++;
} }
onDrop(item, dragged, dropped) { onDrop(item, dragged, dropped) {
@ -76,19 +125,21 @@ export default class Treeview extends Component {
} }
} }
Treeview.$inject = ['$element', '$scope']; Treeview.$inject = ['$element', '$scope', '$transclude'];
ngModule.component('vnTreeview', { ngModule.component('vnTreeview', {
template: require('./index.html'), template: require('./index.html'),
controller: Treeview, controller: Treeview,
bindings: { bindings: {
model: '<', rootLabel: '@',
icons: '<?', data: '<?',
disabled: '<?',
selectable: '<?',
editable: '<?',
draggable: '<?', draggable: '<?',
droppable: '<?', droppable: '<?',
aclRole: '@?' fetchFunc: '&',
} removeFunc: '&?',
createFunc: '&?',
sortFunc: '&?',
readOnly: '<?'
},
transclude: true
}); });

View File

@ -1,9 +1,8 @@
@import "effects"; @import "effects";
vn-treeview { vn-treeview-childs {
vn-treeview-child { display: block;
display: block
}
ul { ul {
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -11,37 +10,43 @@ vn-treeview {
li { li {
list-style: none; list-style: none;
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
&.expanded > div > .arrow {
transform: rotate(180deg);
}
& > .node { & > .node {
@extend %clickable; @extend %clickable;
display: flex; display: flex;
padding: 5px; padding: 5px;
align-items: center; align-items: center;
}
& > vn-check:not(.indeterminate) { & > div > .arrow {
color: $color-main; min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
& > .btn { & > div.expanded > .arrow {
border-color: $color-main; transform: rotate(180deg);
}
}
& > vn-check.checked {
color: $color-main;
}
} }
ul { ul {
padding-left: 2.2em; padding-left: 2.2em;
} }
} }
} }
vn-icon-button { vn-icon-button {
padding: 0 display: table-cell;
vertical-align: middle;
padding: 0;
}
.node > .buttons {
display: none
}
.node:hover > .buttons {
display: block
} }
} }
vn-treeview-content {
flex-grow: 1
}

View File

@ -223,11 +223,8 @@ export default class Watcher extends Component {
} }
loadOriginalData() { loadOriginalData() {
Object.keys(this.data).forEach(key => { const orgData = JSON.parse(JSON.stringify(this.orgData));
delete this.data[key]; this.data = Object.assign(this.data, orgData);
});
this.data = Object.assign(this.data, this.orgData);
this.setPristine(); this.setPristine();
} }

View File

@ -10,9 +10,6 @@ vn-dialog.modal-form {
margin: 0 auto; margin: 0 auto;
} }
} }
tpl-body {
width: 100%;
}
table { table {
width: 100% width: 100%
} }

View File

@ -56,7 +56,5 @@
"You can't delete a confirmed order": "You can't delete a confirmed order", "You can't delete a confirmed order": "You can't delete a confirmed order",
"Value has an invalid format": "Value has an invalid format", "Value has an invalid format": "Value has an invalid format",
"The postcode doesn't exists. Ensure you put the correct format": "The postcode doesn't exists. Ensure you put the correct format", "The postcode doesn't exists. Ensure you put the correct format": "The postcode doesn't exists. Ensure you put the correct format",
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket", "Can't create stowaway for this ticket": "Can't create stowaway for this ticket"
"is not a valid date": "is not a valid date",
"not zone with this parameters": "not zone with this parameters"
} }

View File

@ -108,5 +108,5 @@
"This postal code is not valid": "This postal code is not valid", "This postal code is not valid": "This postal code is not valid",
"is invalid": "is invalid", "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 postcode doesn't exists. Ensure you put the correct format": "El código postal no existe. Asegúrate de ponerlo con el formato correcto",
"not zone with this parameters": "not zone with this parameters" "The department name can't be repeated": "El nombre del departamento no puede repetirse"
} }

View File

@ -1,35 +0,0 @@
module.exports = Self => {
Self.remoteMethod('toggleIsIncluded', {
description: 'Toggle include to delivery',
accepts: [{
arg: 'zoneFk',
type: 'Number',
required: true,
},
{
arg: 'item',
type: 'Object',
required: true,
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/toggleIsIncluded`,
verb: 'POST'
}
});
Self.toggleIsIncluded = async(zoneFk, item) => {
if (item.isIncluded === null)
return Self.destroyAll({zoneFk, geoFk: item.id});
else {
return Self.upsert({
zoneFk: zoneFk,
geoFk: item.id,
isIncluded: item.isIncluded
});
}
};
};

View File

@ -6,10 +6,11 @@ module.exports = Self => {
{ {
arg: 'id', arg: 'id',
type: 'Number', type: 'Number',
description: 'The zone id',
http: {source: 'path'}, http: {source: 'path'},
required: true required: true
}, { }, {
arg: 'parentFk', arg: 'parentId',
type: 'Number', type: 'Number',
description: 'Get the children of the specified father', description: 'Get the children of the specified father',
}, { }, {
@ -28,10 +29,10 @@ module.exports = Self => {
} }
}); });
Self.getLeaves = async(id, parentFk = null, search) => { Self.getLeaves = async(id, parentId = null, search) => {
let [res] = await Self.rawSql( let [res] = await Self.rawSql(
`CALL zone_getLeaves(?, ?, ?)`, `CALL zone_getLeaves(?, ?, ?)`,
[id, parentFk, search] [id, parentId, search]
); );
let map = new Map(); let map = new Map();
@ -49,7 +50,7 @@ module.exports = Self => {
} }
} }
let leaves = map.get(parentFk); let leaves = map.get(parentId);
setLeaves(leaves); setLeaves(leaves);
return leaves || []; return leaves || [];

View File

@ -0,0 +1,41 @@
module.exports = Self => {
Self.remoteMethod('toggleIsIncluded', {
description: 'Toggle include to delivery',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The zone id',
http: {source: 'path'},
required: true
}, {
arg: 'geoId',
type: 'Number',
required: true
}, {
arg: 'isIncluded',
type: 'Boolean'
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/toggleIsIncluded`,
verb: 'POST'
}
});
Self.toggleIsIncluded = async(id, geoId, isIncluded) => {
const models = Self.app.models;
if (isIncluded === undefined)
return models.ZoneIncluded.destroyAll({zoneFk: id, geoFk: geoId});
else {
return models.ZoneIncluded.upsert({
zoneFk: id,
geoFk: geoId,
isIncluded: isIncluded
});
}
};
};

View File

@ -1,3 +0,0 @@
module.exports = Self => {
require('../methods/zone-included/toggleIsIncluded')(Self);
};

View File

@ -3,6 +3,7 @@ module.exports = Self => {
require('../methods/zone/editPrices')(Self); require('../methods/zone/editPrices')(Self);
require('../methods/zone/getLeaves')(Self); require('../methods/zone/getLeaves')(Self);
require('../methods/zone/getEvents')(Self); require('../methods/zone/getEvents')(Self);
require('../methods/zone/toggleIsIncluded')(Self);
Self.validatesPresenceOf('agencyModeFk', { Self.validatesPresenceOf('agencyModeFk', {
message: `Agency cannot be blank` message: `Agency cannot be blank`

View File

@ -1,23 +1,25 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="/api/Zones/{{$ctrl.$stateParams.id}}/getLeaves" url="/api/Zones/{{$ctrl.$stateParams.id}}/getLeaves"
filter="::$ctrl.filter" filter="::$ctrl.filter">
auto-load="false">
</vn-crud-model> </vn-crud-model>
<div class="vn-w-md"> <div class="vn-w-md">
<vn-card class="vn-px-lg"> <vn-card class="vn-px-lg">
<vn-searchbar <vn-searchbar auto-load="false"
on-search="$ctrl.onSearch($params)" on-search="$ctrl.onSearch($params)"
vn-focus> vn-focus>
</vn-searchbar> </vn-searchbar>
</vn-card> </vn-card>
<vn-card class="vn-pa-lg vn-mt-md"> <vn-card class="vn-pa-lg vn-mt-md">
<vn-treeview <vn-treeview vn-id="treeview" root-label="Locations"
vn-id="treeview" fetch-func="$ctrl.onFetch($item)"
model="model" sort-func="$ctrl.onSort($a, $b)">
acl-role="deliveryBoss" <vn-check
on-selection="$ctrl.onSelection(item, value)" field="item.selected"
selectable="true"> on-change="$ctrl.onSelection(value, item)"
triple-state="true"
label="{{::item.name}}">
</vn-check>
</vn-treeview> </vn-treeview>
</vn-card> </vn-card>
</div> </div>

View File

@ -1,10 +1,32 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'core/lib/section'; import Section from 'core/lib/section';
import './style.scss';
class Controller extends Section { class Controller extends Section {
onSearch(params) { onSearch(params) {
this.$.model.applyFilter(null, params); this.$.model.applyFilter({}, params).then(() => {
this.$.$applyAsync(() => this.$.treeview.refresh()); const data = this.$.model.data;
this.$.treeview.data = data;
});
}
onFetch(item) {
const params = item ? {parentId: item.id} : null;
return this.$.model.applyFilter({}, params).then(() => {
return this.$.model.data;
});
}
onSort(a, b) {
if (b.selected !== a.selected) {
if (a.selected == null)
return 1;
if (b.selected == null)
return -1;
return b.selected - a.selected;
}
return a.name.localeCompare(b.name);
} }
exprBuilder(param, value) { exprBuilder(param, value) {
@ -14,13 +36,11 @@ class Controller extends Section {
} }
} }
onSelection(item, isIncluded) { onSelection(value, item) {
let node = Object.assign({}, item); if (value == null)
node.isIncluded = isIncluded; value = undefined;
node.childs = []; // Data too large const params = {geoId: item.id, isIncluded: value};
const path = `/api/zones/${this.zone.id}/toggleIsIncluded`;
const path = '/agency/api/ZoneIncludeds/toggleIsIncluded';
const params = {zoneFk: this.zone.id, item: node};
this.$http.post(path, params); this.$http.post(path, params);
} }
} }

View File

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

View File

@ -86,8 +86,9 @@ module.exports = Self => {
return {'i.hasPdf': value}; return {'i.hasPdf': value};
case 'created': case 'created':
return {'i.created': value}; return {'i.created': value};
case 'amount':
case 'clientFk': case 'clientFk':
return {'i.clientFk': value};
case 'amount':
case 'companyFk': case 'companyFk':
case 'issued': case 'issued':
case 'dued': case 'dued':

View File

@ -1,5 +1,5 @@
<vn-crud-model <vn-crud-model
url="/item/api/TaxClasses" url="api/TaxClasses"
fields="['id', 'description', 'code']" fields="['id', 'description', 'code']"
data="classes" data="classes"
auto-load="true"> auto-load="true">
@ -11,7 +11,7 @@
</vn-watcher> </vn-watcher>
<form name="form" ng-submit="$ctrl.submit()" compact> <form name="form" ng-submit="$ctrl.submit()" compact>
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal ng-repeat="tax in $ctrl.taxes track by $index"> <vn-horizontal ng-repeat="tax in $ctrl.taxes">
<vn-textfield vn-one <vn-textfield vn-one
label="Country" label="Country"
model="tax.country.country" model="tax.country.country"
@ -21,7 +21,6 @@
label="Class" label="Class"
field="tax.taxClassFk" field="tax.taxClassFk"
data="classes" data="classes"
value-field="id"
show-field="description"> show-field="description">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>

View File

@ -1,11 +1,11 @@
import ngModule from '../module'; import ngModule from '../module';
export default class Controller { export default class Controller {
constructor($stateParams, $http, $translate, vnApp) { constructor($stateParams, $http, $translate, $scope) {
this.$ = $scope;
this.$http = $http; this.$http = $http;
this.$stateParams = $stateParams; this.$stateParams = $stateParams;
this._ = $translate; this._ = $translate;
this.vnApp = vnApp;
} }
$onInit() { $onInit() {
@ -21,9 +21,8 @@ export default class Controller {
}] }]
}; };
let urlFilter = encodeURIComponent(JSON.stringify(filter)); let url = `api/Items/${this.$stateParams.id}/taxes`;
let url = `/item/api/Items/${this.$stateParams.id}/taxes?filter=${urlFilter}`; this.$http.get(url, {params: {filter}}).then(json => {
this.$http.get(url).then(json => {
this.taxes = json.data; this.taxes = json.data;
}); });
} }
@ -33,14 +32,16 @@ export default class Controller {
for (let tax of this.taxes) for (let tax of this.taxes)
data.push({id: tax.id, taxClassFk: tax.taxClassFk}); data.push({id: tax.id, taxClassFk: tax.taxClassFk});
let url = `/item/api/Items/updateTaxes`; this.$.watcher.check();
this.$http.post(url, data).then( let url = `api/Items/updateTaxes`;
() => this.vnApp.showSuccess(this._.instant('Data saved!')) this.$http.post(url, data).then(() => {
); this.$.watcher.notifySaved();
this.$.watcher.updateOriginalData();
});
} }
} }
Controller.$inject = ['$stateParams', '$http', '$translate', 'vnApp']; Controller.$inject = ['$stateParams', '$http', '$translate', '$scope'];
ngModule.component('vnItemTax', { ngModule.component('vnItemTax', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -2,65 +2,65 @@ import './index.js';
describe('Item', () => { describe('Item', () => {
describe('Component vnItemTax', () => { describe('Component vnItemTax', () => {
let $element;
let $stateParams; let $stateParams;
let controller; let controller;
let $httpBackend; let $httpBackend;
let vnApp;
beforeEach(angular.mock.module('item', $translateProvider => { beforeEach(angular.mock.module('item', $translateProvider => {
$translateProvider.translations('en', {}); $translateProvider.translations('en', {});
})); }));
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, _$stateParams_, _vnApp_) => { beforeEach(angular.mock.inject((_$httpBackend_, $rootScope, _$stateParams_, $compile) => {
$stateParams = _$stateParams_; $stateParams = _$stateParams_;
$stateParams.id = 1; $stateParams.id = 1;
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
vnApp = _vnApp_;
spyOn(vnApp, 'showSuccess'); $httpBackend.whenGET(url => url.startsWith(`api/TaxClasses`))
controller = $componentController('vnItemTax', {$state: $stateParams}); .respond([
{id: 1, description: 'Reduced VAT', code: 'R'},
{id: 2, description: 'General VAT', code: 'G'}
]);
$httpBackend.whenGET(url => url.startsWith(`api/Items/${$stateParams.id}/taxes`))
.respond([
{id: 1, taxClassFk: 1}
]);
$element = $compile(`<vn-item-tax></vn-item-tax`)($rootScope);
controller = $element.controller('vnItemTax');
})); }));
afterEach(() => {
$element.remove();
});
describe('getTaxes()', () => { describe('getTaxes()', () => {
it('should perform a query to set the array of taxes into the controller', () => { it('should perform a query to set the array of taxes into the controller', () => {
let filter = {
fields: ['id', 'countryFk', 'taxClassFk'],
include: [{
relation: 'country',
scope: {fields: ['country']}
}]
};
let response = [{id: 1, taxClassFk: 1}];
filter = encodeURIComponent(JSON.stringify(filter));
$httpBackend.when('GET', `/item/api/Items/1/taxes?filter=${filter}`).respond(response);
$httpBackend.expect('GET', `/item/api/Items/1/taxes?filter=${filter}`);
controller.$onInit();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.taxes).toEqual(response); expect(controller.taxes[0].id).toEqual(1);
expect(controller.taxes[0].taxClassFk).toEqual(1);
}); });
}); });
describe('submit()', () => { describe('submit()', () => {
it('should perform a post to update taxes', () => { it('should perform a post to update taxes', () => {
let filter = { spyOn(controller.$.watcher, 'notifySaved');
fields: ['id', 'countryFk', 'taxClassFk'], spyOn(controller.$.watcher, 'updateOriginalData');
include: [{ controller.taxes = [
relation: 'country', {id: 37, countryFk: 1, taxClassFk: 1, country: {id: 1, country: 'España'}}
scope: {fields: ['country']} ];
}] controller.$.watcher.data = [
}; {id: 37, countryFk: 1, taxClassFk: 2, country: {id: 1, country: 'España'}}
let response = [{id: 1, taxClassFk: 1}]; ];
filter = encodeURIComponent(JSON.stringify(filter));
$httpBackend.when('GET', `/item/api/Items/1/taxes?filter=${filter}`).respond(response);
controller.$onInit();
$httpBackend.flush();
$httpBackend.when('POST', `/item/api/Items/updateTaxes`).respond('ok'); $httpBackend.whenPOST(`api/Items/updateTaxes`).respond('oki doki');
$httpBackend.expect('POST', `/item/api/Items/updateTaxes`);
controller.submit(); controller.submit();
$httpBackend.flush(); $httpBackend.flush();
expect(vnApp.showSuccess).toHaveBeenCalledWith('Data saved!'); expect(controller.$.watcher.notifySaved).toHaveBeenCalledWith();
expect(controller.$.watcher.updateOriginalData).toHaveBeenCalledWith();
}); });
}); });
}); });

View File

@ -8,9 +8,6 @@
<vn-label-value label="Nickname" <vn-label-value label="Nickname"
value="{{$ctrl.summary.address.nickname}}"> value="{{$ctrl.summary.address.nickname}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Warehouse"
value="{{$ctrl.summary.sourceApp}}">
</vn-label-value>
<vn-check label="Confirmed" disabled="true" <vn-check label="Confirmed" disabled="true"
field="$ctrl.summary.isConfirmed"> field="$ctrl.summary.isConfirmed">
</vn-check> </vn-check>
@ -27,6 +24,9 @@
<vn-label-value label="Phone" <vn-label-value label="Phone"
value="{{$ctrl.summary.address.phone}}"> value="{{$ctrl.summary.address.phone}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Created from"
value="{{$ctrl.summary.sourceApp}}">
</vn-label-value>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-label-value label="{{'Notes'}}" <vn-label-value label="{{'Notes'}}"

View File

@ -45,7 +45,7 @@ module.exports = Self => {
clientFk: ship.clientFk, clientFk: ship.clientFk,
addressFk: ship.addressFk, addressFk: ship.addressFk,
agencyModeFk: ship.agencyModeFk, agencyModeFk: ship.agencyModeFk,
warehouse: {neq: ship.warehouseFk}, warehouseFk: {neq: ship.warehouseFk},
shipped: { shipped: {
between: [lowestDate.toJSON(), highestDate.toJSON()] between: [lowestDate.toJSON(), highestDate.toJSON()]
} }

View File

@ -1,17 +1,17 @@
<vn-crud-model <vn-crud-model vn-id="model"
url="/api/Tickets/{{$ctrl.$stateParams.id}}/getPossibleStowaways" url="/api/Tickets/{{$ctrl.$stateParams.id}}/getPossibleStowaways"
vn-id="model"
data="possibleStowaways"> data="possibleStowaways">
</vn-crud-model> </vn-crud-model>
<vn-dialog <vn-dialog
vn-id="dialog" vn-id="dialog"
class="modal-form" class="modal-form"
on-open="model.reload()"> on-open="model.refresh()">
<tpl-body> <tpl-body>
<vn-horizontal class="header vn-pa-md"> <vn-horizontal class="header vn-pa-md vn-w-lg">
<h5><span translate>Stowaways to add</span></h5> <h5><span translate>Stowaways to add</span></h5>
</vn-horizontal> </vn-horizontal>
<vn-horizontal class="vn-pa-md"> <vn-horizontal class="vn-pa-md vn-w-lg">
<vn-data-viewer class="vn-w-xs" model="model">
<vn-table model="model" auto-load="false"> <vn-table model="model" auto-load="false">
<vn-thead> <vn-thead>
<vn-tr> <vn-tr>
@ -32,6 +32,7 @@
</vn-tr> </vn-tr>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>
</vn-data-viewer>
</vn-horizontal> </vn-horizontal>
</tpl-body> </tpl-body>
</vn-dialog> </vn-dialog>

View File

@ -0,0 +1,40 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('createChild', {
description: 'Creates a new child department',
accessType: 'WRITE',
accepts: [{
arg: 'parentId',
type: 'Number'
},
{
arg: 'name',
type: 'String',
required: true,
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/createChild`,
verb: 'POST'
}
});
Self.createChild = async(parentId = null, name) => {
const models = Self.app.models;
const nameExists = await models.Department.count({name});
if (nameExists)
throw new UserError(`The department name can't be repeated`);
const newDep = await models.Department.create({
parentFk: parentId,
name: name
});
return newDep;
};
};

View File

@ -1,20 +1,14 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('getLeaves', { Self.remoteMethod('getLeaves', {
description: 'Returns the first shipped and landed possible for params', description: 'Returns the nodes for a department',
accepts: [{ accepts: [{
arg: 'parentFk', arg: 'parentId',
type: 'Number', type: 'Number',
default: 1, description: 'Get the children of the specified father',
required: false, }, {
}, arg: 'search',
{ type: 'String',
arg: 'filter', description: 'Filter nodes whose name starts with',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}], }],
returns: { returns: {
type: ['object'], type: ['object'],
@ -26,61 +20,30 @@ module.exports = Self => {
} }
}); });
Self.getLeaves = async(parentFk = 1, filter) => { Self.getLeaves = async(parentId = null, search) => {
let conn = Self.dataSource.connector; let [res] = await Self.rawSql(
let stmt = new ParameterizedSQL( `CALL department_getLeaves(?, ?)`,
`SELECT [parentId, search]
child.id, );
child.name,
child.lft,
child.rgt,
child.depth,
child.sons
FROM department parent
JOIN department child ON child.lft > parent.lft
AND child.rgt < parent.rgt
AND child.depth = parent.depth + 1
WHERE parent.id = ?`, [parentFk]);
// Get nodes from depth greather than Origin let map = new Map();
stmt.merge(conn.makeSuffix(filter)); for (let node of res) {
if (!map.has(node.parentFk))
const nodes = await Self.rawStmt(stmt); map.set(node.parentFk, []);
map.get(node.parentFk).push(node);
if (nodes.length == 0)
return nodes;
// Get parent nodes
const minorDepth = nodes.reduce((a, b) => {
return b < a ? b : a;
}).depth;
const parentNodes = nodes.filter(element => {
return element.depth === minorDepth;
});
const leaves = Object.assign([], parentNodes);
nestLeaves(leaves);
function nestLeaves(elements) {
elements.forEach(element => {
let childs = Object.assign([], getLeaves(element));
if (childs.length > 0) {
element.childs = childs;
nestLeaves(element.childs);
}
});
} }
function getLeaves(parent) { function setLeaves(nodes) {
let elements = nodes.filter(element => { if (!nodes) return;
return element.lft > parent.lft && element.rgt < parent.rgt for (let node of nodes) {
&& element.depth === parent.depth + 1; node.childs = map.get(node.id);
}); setLeaves(node.childs);
}
return elements;
} }
return leaves; let leaves = map.get(parentId);
setLeaves(leaves);
return leaves || [];
}; };
}; };

View File

@ -1,43 +0,0 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('nodeAdd', {
description: 'Returns the first shipped and landed possible for params',
accessType: 'WRITE',
accepts: [{
arg: 'parentFk',
type: 'Number',
required: false,
},
{
arg: 'name',
type: 'String',
required: true,
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/nodeAdd`,
verb: 'POST'
}
});
Self.nodeAdd = async(parentFk = 1, name) => {
let stmts = [];
let conn = Self.dataSource.connector;
let nodeIndex = stmts.push(new ParameterizedSQL(
`CALL nst.nodeAdd('vn', 'department', ?, ?)`, [parentFk, name])) - 1;
stmts.push(`CALL nst.nodeRecalc('vn', 'department')`);
let sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql);
let [node] = result[nodeIndex];
return node;
};
};

View File

@ -1,29 +0,0 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('nodeDelete', {
description: 'Returns the first shipped and landed possible for params',
accessType: 'WRITE',
accepts: [{
arg: 'parentFk',
type: 'Number',
required: false,
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/nodeDelete`,
verb: 'POST'
}
});
Self.nodeDelete = async parentFk => {
let stmt = new ParameterizedSQL(
`CALL nst.nodeDelete('vn', 'department', ?)`, [parentFk]);
return await Self.rawStmt(stmt);
};
};

View File

@ -0,0 +1,27 @@
module.exports = Self => {
Self.remoteMethod('removeChild', {
description: 'Removes a child department',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The department id',
http: {source: 'path'}
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/removeChild`,
verb: 'POST'
}
});
Self.removeChild = async id => {
const models = Self.app.models;
const department = await models.Department.findById(id);
return await department.destroy();
};
};

View File

@ -1,15 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
require('../methods/department/getLeaves')(Self); require('../methods/department/getLeaves')(Self);
require('../methods/department/nodeAdd')(Self); require('../methods/department/createChild')(Self);
require('../methods/department/nodeDelete')(Self); require('../methods/department/removeChild')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_ROW_IS_REFERENCED_2')
return new UserError(`You cannot remove this department`);
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`The department name can't be repeated`);
return err;
});
}; };

View File

@ -13,6 +13,9 @@
}, },
"name": { "name": {
"type": "String" "type": "String"
},
"parentFk": {
"type": "Number"
} }
} }
} }

View File

@ -1,25 +1,24 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="/worker/api/departments/getLeaves" url="/worker/api/departments/getLeaves"
params="::$ctrl.params"
auto-load="false"> auto-load="false">
</vn-crud-model> </vn-crud-model>
<div class="content-block" compact>
<form name="form"> <form name="form" compact>
<div class="vn-ma-md">
<vn-card class="vn-my-md vn-pa-md"> <vn-card class="vn-my-md vn-pa-md">
<vn-treeview vn-id="treeview" model="model" <vn-treeview vn-id="treeview" root-label="Departments" read-only="false"
on-selection="$ctrl.onSelection(item, value)" fetch-func="$ctrl.onFetch($item)"
on-create="$ctrl.onCreate(parent)" remove-func="$ctrl.onRemove($item)"
on-drop="$ctrl.onDrop(item, dragged, dropped)" create-func="$ctrl.onCreate($parent)"
icons="$ctrl.icons" sort-func="$ctrl.onSort($a, $b)">
draggable="true" droppable="true" {{::item.name}}
acl-role="hr" editable="true">
</vn-treeview> </vn-treeview>
</vn-card> </vn-card>
</div> <vn-button-bar>
<vn-button ui-sref="worker.index" label="Back"></vn-button>
</vn-button-bar>
</form> </form>
</div>
<vn-confirm <vn-confirm
vn-id="deleteNode" vn-id="deleteNode"
on-response="$ctrl.onRemoveResponse(response)" on-response="$ctrl.onRemoveResponse(response)"
@ -37,7 +36,7 @@
<vn-horizontal> <vn-horizontal>
<vn-textfield vn-one <vn-textfield vn-one
label="Name" label="Name"
model="$ctrl.newNode.name"> model="$ctrl.newChild.name">
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
</tpl-body> </tpl-body>

View File

@ -2,31 +2,28 @@ import ngModule from '../module';
class Controller { class Controller {
constructor($scope, $http, vnApp, $translate) { constructor($scope, $http, vnApp, $translate) {
this.$scope = $scope; this.$ = $scope;
this.$http = $http; this.$http = $http;
this.vnApp = vnApp; this.vnApp = vnApp;
this.$translate = $translate; this.$translate = $translate;
this.params = {parentFk: 1};
this.icons = [{icon: 'delete', tooltip: 'Delete', callback: this.onDelete}];
this.newNode = {
name: ''
};
} }
onCreate(parent) { $postLink() {
if (parent instanceof Object) this.$.treeview.fetch();
this.newNode.parentFk = parent.id;
this.selectedNode = {parent};
this.$scope.createNode.show();
} }
onDelete(item, parent, index) { onFetch(item) {
this.selectedNode = {id: item.id, parent, index}; const params = item ? {parentId: item.id} : null;
this.$scope.deleteNode.show(); return this.$.model.applyFilter({}, params).then(() => {
return this.$.model.data;
});
} }
onDrop(item, dragged, dropped) { onSort(a, b) {
return a.name.localeCompare(b.name);
}
/* onDrop(item, dragged, dropped) {
if (dropped.scope.item) { if (dropped.scope.item) {
const droppedItem = dropped.scope.item; const droppedItem = dropped.scope.item;
const draggedItem = dragged.scope.item; const draggedItem = dragged.scope.item;
@ -38,27 +35,43 @@ class Controller {
this.$scope.$apply(); this.$scope.$apply();
} }
} */
onCreate(parent) {
this.newChild = {
parent: parent,
name: ''
};
this.$.createNode.show();
} }
onCreateDialogOpen() { onCreateDialogOpen() {
this.newNode.name = ''; this.newChild.name = '';
} }
onCreateResponse(response) { onCreateResponse(response) {
if (response == 'ACCEPT') { if (response == 'ACCEPT') {
try { try {
if (!this.newNode.name) if (!this.newChild.name)
throw new Error(`Name can't be empty`); throw new Error(`Name can't be empty`);
this.$http.post(`/worker/api/Departments/nodeAdd`, this.newNode).then(response => { const params = {name: this.newChild.name};
if (response.data) { const parent = this.newChild.parent;
let parent = this.selectedNode.parent;
if ((parent instanceof Object) && !(parent instanceof Array)) { if (parent && parent.id)
const childs = parent.childs; params.parentId = parent.id;
childs.push(response.data);
} else if ((parent instanceof Object) && (parent instanceof Array)) if (!parent.active)
parent.push(response.data); 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;
this.$.treeview.create(item);
}); });
} catch (e) { } catch (e) {
this.vnApp.showError(this.$translate.instant(e.message)); this.vnApp.showError(this.$translate.instant(e.message));
@ -68,17 +81,17 @@ class Controller {
return true; return true;
} }
onRemove(item) {
this.removedChild = item;
this.$.deleteNode.show();
}
onRemoveResponse(response) { onRemoveResponse(response) {
if (response === 'ACCEPT') { if (response === 'ACCEPT') {
const path = `/worker/api/Departments/nodeDelete`; const childId = this.removedChild.id;
this.$http.post(path, {parentFk: this.selectedNode.id}).then(() => { const path = `/api/departments/${childId}/removeChild`;
let parent = this.selectedNode.parent; this.$http.post(path).then(() => {
this.$.treeview.remove(this.removedChild);
if ((parent instanceof Object) && !(parent instanceof Array)) {
const childs = parent.childs;
childs.splice(this.selectedNode.index, 1);
} else if ((parent instanceof Object) && (parent instanceof Array))
parent.splice(this.selectedNode.index, 1);
}); });
} }
} }