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

GJ
This commit is contained in:
Carlos Jimenez Ruiz 2019-10-08 11:12:49 +00:00 committed by Gitea
commit 4b235faa8a
33 changed files with 837 additions and 486 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

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

View File

@ -45,4 +45,3 @@ import './table';
import './td-editable';
import './th';
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
acl-role="$ctrl.aclRole"
items="$ctrl.data"
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>
<vn-treeview-childs
items="$ctrl.items">
</vn-treeview-childs>

View File

@ -2,73 +2,122 @@ import ngModule from '../../module';
import Component from '../../lib/component';
import './style.scss';
import './childs';
import './content';
/**
* Treeview
*
* @property {String} position The relative position to the parent
*/
export default class Treeview extends Component {
constructor($element, $scope) {
constructor($element, $scope, $transclude) {
super($element, $scope);
this.$scope = $scope;
this.data = [];
this.$transclude = $transclude;
this.readOnly = true;
}
$onInit() {
this.refresh();
get data() {
return this._data;
}
refresh() {
this.model.refresh().then(() => {
this.data = this.model.data;
set data(value) {
this._data = value;
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) {
if (item.active)
item.childs = undefined;
else {
this.model.applyFilter({}, {parentFk: item.id}).then(() => {
const newData = this.model.data;
this.fold(item);
else
this.unfold(item);
}
if (item.childs) {
let childs = item.childs;
childs.forEach(child => {
let index = newData.findIndex(newChild => {
return newChild.id == child.id;
});
newData[index] = child;
fold(item) {
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 => {
let index = newData.findIndex(newChild => {
return newChild.id == child.id;
});
}
item.childs = newData.sort((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);
newData[index] = child;
});
});
}
if (this.sortFunc) {
item.childs = newData.sort((a, b) =>
this.sortFunc({$a: a, $b: b})
);
}
}).then(() => item.active = true);
}
onRemove(item) {
if (this.removeFunc)
this.removeFunc({$item: item});
}
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})
);
}
item.active = !item.active;
if (parent) parent.sons++;
}
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', {
template: require('./index.html'),
controller: Treeview,
bindings: {
model: '<',
icons: '<?',
disabled: '<?',
selectable: '<?',
editable: '<?',
rootLabel: '@',
data: '<?',
draggable: '<?',
droppable: '<?',
aclRole: '@?'
}
fetchFunc: '&',
removeFunc: '&?',
createFunc: '&?',
sortFunc: '&?',
readOnly: '<?'
},
transclude: true
});

View File

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

@ -107,5 +107,6 @@
"Invalid quantity": "Cantidad invalida",
"This postal code is not valid": "This postal code is not valid",
"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",
"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',
type: 'Number',
description: 'The zone id',
http: {source: 'path'},
required: true
}, {
arg: 'parentFk',
arg: 'parentId',
type: 'Number',
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(
`CALL zone_getLeaves(?, ?, ?)`,
[id, parentFk, search]
[id, parentId, search]
);
let map = new Map();
@ -49,7 +50,7 @@ module.exports = Self => {
}
}
let leaves = map.get(parentFk);
let leaves = map.get(parentId);
setLeaves(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/getLeaves')(Self);
require('../methods/zone/getEvents')(Self);
require('../methods/zone/toggleIsIncluded')(Self);
Self.validatesPresenceOf('agencyModeFk', {
message: `Agency cannot be blank`

View File

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

View File

@ -1,10 +1,32 @@
import ngModule from '../module';
import Section from 'core/lib/section';
import './style.scss';
class Controller extends Section {
onSearch(params) {
this.$.model.applyFilter(null, params);
this.$.$applyAsync(() => this.$.treeview.refresh());
this.$.model.applyFilter({}, params).then(() => {
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) {
@ -14,13 +36,11 @@ class Controller extends Section {
}
}
onSelection(item, isIncluded) {
let node = Object.assign({}, item);
node.isIncluded = isIncluded;
node.childs = []; // Data too large
const path = '/agency/api/ZoneIncludeds/toggleIsIncluded';
const params = {zoneFk: this.zone.id, item: node};
onSelection(value, item) {
if (value == null)
value = undefined;
const params = {geoId: item.id, isIncluded: value};
const path = `/api/zones/${this.zone.id}/toggleIsIncluded`;
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

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

View File

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

View File

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

View File

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