Merge with upstream

This commit is contained in:
Juan Ferrer 2019-10-23 17:42:41 +02:00
commit 53003246c3
84 changed files with 1436 additions and 774 deletions

View File

@ -9,25 +9,31 @@ module.exports = Self => {
{
arg: 'warehouseId',
type: 'Number',
description: 'The warehouse id'
description: 'The warehouse id',
required: true
}, {
arg: 'companyId',
type: 'Number',
description: 'The company id'
description: 'The company id',
required: true
}, {
arg: 'dmsTypeId',
type: 'Number',
description: 'The dms type id'
description: 'The dms type id',
required: true
}, {
arg: 'reference',
type: 'String'
type: 'String',
required: true
}, {
arg: 'description',
type: 'String'
type: 'String',
required: true
}, {
arg: 'hasFile',
type: 'Boolean',
description: 'True if has an attached file'
description: 'True if has an attached file',
required: true
}],
returns: {
type: 'Object',

View File

@ -52,6 +52,15 @@
},
"Postcode": {
"dataSource": "vn"
},
"UserPhoneType": {
"dataSource": "vn"
},
"UserPhone": {
"dataSource": "vn"
},
"UserLog": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,9 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`This phone already exists`);
return err;
});
};

View File

@ -2,7 +2,8 @@
"name": "UserPhone",
"base": "Loggable",
"log": {
"model":"UserLog"
"model":"UserLog",
"relation": "user"
},
"options": {
"mysql": {
@ -12,11 +13,15 @@
"properties": {
"id": {
"id": true,
"type": "String"
"type": "Number"
},
"phone": {
"type": "Number",
"required": true
},
"typeFk": {
"type": "String",
"required": true
}
},
"relations": {

View File

@ -1,6 +1,6 @@
CREATE TABLE `vn`.`userLog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`originFk` int(11) NOT NULL,
`originFk` int(10) unsigned NOT NULL,
`userFk` int(10) unsigned DEFAULT NULL,
`action` set('insert','update','delete') COLLATE utf8_unicode_ci NOT NULL,
`creationDate` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
@ -13,6 +13,6 @@ CREATE TABLE `vn`.`userLog` (
PRIMARY KEY (`id`),
KEY `originFk` (`originFk`),
KEY `userFk` (`userFk`),
CONSTRAINT `userLog_ibfk_1` FOREIGN KEY (`originFk`) REFERENCES `client` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `userLog_ibfk_1` FOREIGN KEY (`originFk`) REFERENCES `account`.`user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `userLog_ibfk_2` FOREIGN KEY (`userFk`) REFERENCES `account`.`user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

View File

@ -2,7 +2,7 @@ CREATE TABLE `vn`.`userPhone` (
`id` INT NOT NULL AUTO_INCREMENT,
`userFk` INT(10) UNSIGNED NOT NULL,
`typeFk` VARCHAR(45) NOT NULL,
`phone` INT(15) NOT NULL,
`phone` VARCHAR(15) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `UserFK_Phone` (`userFk` ASC, `phone` ASC));
@ -22,7 +22,7 @@ ADD CONSTRAINT `fgnUserFk`
ON UPDATE CASCADE;
insert into vn.userPhone(userFk,typeFk,phone)
select id,'PersonalPhone', phone
select id,'personalPhone', phone
from vn.client
where phone is not null;

View File

@ -471,7 +471,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF
(11, 1, 7, 1, 6, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 102, 'NY roofs', 122, NULL, 0, 3, CURDATE()),
(12, 1, 1, 1, 1, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 103, 'Phone Box', 123, NULL, 0, 1, CURDATE()),
(13, 1, 7, 1, 6, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 103, 'Phone Box', 123, NULL, 0, 3, CURDATE()),
(14, 1, 2, 1, NULL, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 104, 'Malibu Point', 4, NULL, 0, 9, CURDATE()),
(14, 1, 2, 1, NULL, CURDATE(), CURDATE(), 104, 'Malibu Point', 4, NULL, 0, 9, CURDATE()),
(15, 1, 7, 1, 6, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 105, 'Plastic Cell', 125, NULL, 0, 3, CURDATE()),
(16, 1, 7, 1, 6, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 106, 'Many Places', 126, NULL, 0, 3, CURDATE()),
(17, 1, 7, 2, 6, CURDATE(), DATE_ADD(CURDATE(), INTERVAL + 1 DAY), 106, 'Many Places', 126, NULL, 0, 3, CURDATE()),
@ -1862,4 +1862,67 @@ INSERT INTO `vn`.`queuePriority`(`id`, `priority`)
VALUES
(1, 'Alta'),
(2, 'Normal'),
(3, 'Baja');
(3, 'Baja');
INSERT INTO `vn`.`userPhone`(`id`, `userFk`, `typeFk`, `phone`)
VALUES
(1, 101, 'personalPhone', 1111111111),
(2, 102, 'personalPhone', 1111111111),
(3, 103, 'personalPhone', 1111111111),
(4, 104, 'personalPhone', 1111111111),
(5, 105, 'personalPhone', 1111111111),
(6, 106, 'personalPhone', 1111111111),
(7, 107, 'personalPhone', 1111111111),
(8, 108, 'personalPhone', 1111111111),
(9, 109, 'personalPhone', 1111111111),
(10, 110, 'personalPhone', 1111111111),
(11, 111, 'personalPhone', 1111111111),
(12, 112, 'personalPhone', 1111111111),
(13, 1, 'personalPhone', 623111111),
(14, 2, 'personalPhone', 623111111),
(15, 3, 'personalPhone', 623111111),
(16, 5, 'personalPhone', 623111111),
(17, 6, 'personalPhone', 623111111),
(18, 9, 'personalPhone', 623111111),
(19, 13, 'personalPhone', 623111111),
(20, 15, 'personalPhone', 623111111),
(21, 16, 'personalPhone', 623111111),
(22, 17, 'personalPhone', 623111111),
(23, 18, 'personalPhone', 623111111),
(24, 19, 'personalPhone', 623111111),
(25, 20, 'personalPhone', 623111111),
(26, 21, 'personalPhone', 623111111),
(27, 22, 'personalPhone', 623111111),
(28, 30, 'personalPhone', 623111111),
(29, 31, 'personalPhone', 623111111),
(30, 32, 'personalPhone', 623111111),
(31, 34, 'personalPhone', 623111111),
(32, 35, 'personalPhone', 623111111),
(33, 36, 'personalPhone', 623111111),
(34, 37, 'personalPhone', 623111111),
(35, 38, 'personalPhone', 623111111),
(36, 39, 'personalPhone', 623111111),
(37, 40, 'personalPhone', 623111111),
(38, 41, 'personalPhone', 623111111),
(39, 42, 'personalPhone', 623111111),
(40, 43, 'personalPhone', 623111111),
(41, 44, 'personalPhone', 623111111),
(42, 45, 'personalPhone', 623111111),
(43, 47, 'personalPhone', 623111111),
(44, 48, 'personalPhone', 623111111),
(45, 50, 'personalPhone', 623111111),
(46, 51, 'personalPhone', 623111111),
(47, 52, 'personalPhone', 623111111),
(48, 54, 'personalPhone', 623111111),
(49, 55, 'personalPhone', 623111111),
(50, 56, 'personalPhone', 623111111),
(51, 57, 'personalPhone', 623111111),
(52, 58, 'personalPhone', 623111111),
(53, 59, 'personalPhone', 623111111),
(54, 60, 'personalPhone', 623111111),
(55, 61, 'personalPhone', 623111111),
(56, 65, 'personalPhone', 623111111),
(57, 66, 'personalPhone', 623111111),
(65, 107, 'businessPhone', 700987987),
(67, 106, 'businessPhone', 1111111112),
(68, 106, 'personalPhone', 1111111113);

View File

@ -61,7 +61,7 @@ export default {
fiscalDataButton: 'vn-left-menu a[ui-sref="client.card.fiscalData"]',
socialNameInput: `vn-textfield input[name="socialName"]`,
fiscalIdInput: `vn-textfield input[name="fi"]`,
equalizationTaxCheckbox: 'vn-check[label="Is equalizated"]',
equalizationTaxCheckbox: 'vn-check[ng-model="$ctrl.client.isEqualizated"]',
acceptPropagationButton: 'vn-client-fiscal-data > vn-confirm button[response=ACCEPT]',
addressInput: `vn-textfield input[name="street"]`,
postcodeInput: `vn-textfield input[name="postcode"]`,
@ -482,7 +482,7 @@ export default {
addRequestButton: 'vn-ticket-request-index > a > vn-float-button > button',
request: 'vn-ticket-request-index vn-table vn-tr',
descriptionInput: 'vn-ticket-request-create > form > div > vn-card > vn-horizontal:nth-child(1) > vn-textfield input',
atenderAutocomplete: 'vn-ticket-request-create vn-autocomplete[ng-model="$ctrl.ticketRequest.atenderFk"]',
atenderAutocomplete: 'vn-ticket-request-create vn-autocomplete[ng-model="$ctrl.ticketRequest.attenderFk"]',
quantityInput: 'vn-ticket-request-create vn-input-number input[name=quantity]',
priceInput: 'vn-ticket-request-create vn-input-number input[name=price]',
firstRemoveRequestButton: 'vn-ticket-request-index vn-icon[icon="delete"]:nth-child(1)',
@ -534,7 +534,7 @@ export default {
itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor',
itemDescriptorPopoverItemDiaryButton: '.vn-popover.shown vn-item-descriptor a[href="#!/item/2/diary"]',
firstDevelopmentWorker: 'vn-claim-summary vn-horizontal > vn-auto:nth-child(5) vn-table > div > vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(4) > span',
firstDevelopmentWorkerGoToClientButton: '.vn-popover.shown vn-worker-descriptor div.quicklinks > a[href="#!/client/21/summary"]',
firstDevelopmentWorkerGoToClientButton: '.vn-popover.shown vn-worker-descriptor vn-quick-links > a[href="#!/client/21/summary"]',
firstActionTicketId: 'vn-claim-summary > vn-card > vn-horizontal > vn-auto:nth-child(6) vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(2) > span',
firstActionTicketDescriptor: '.vn-popover.shown vn-ticket-descriptor'
},

View File

@ -17,7 +17,7 @@ describe('Ticket Summary path', () => {
it(`should display details from the ticket and it's client on the top of the header`, async() => {
let result = await nightmare
.waitForSpinnerLoad()
.waitForTextInElement(selectors.ticketSummary.header, 'Bruce Banner')
.waitToGetProperty(selectors.ticketSummary.header, 'innerText');
expect(result).toContain(`Ticket #${ticketId}`);

View File

@ -3,7 +3,7 @@ import ngModule from '../module';
const regex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i;
export const isMobile = regex.test(navigator.userAgent);
export function focus(input) {
export function focus($scope, input) {
if (isMobile) return;
const element = input;
@ -23,9 +23,9 @@ export function focus(input) {
}
input.focus();
if (input.select)
$scope.$applyAsync(() => {
input.select();
});
}
/**
@ -37,7 +37,7 @@ export function directive() {
return {
restrict: 'A',
link: function($scope, $element) {
$scope.$watch('', () => focus($element[0]));
$scope.$watch('', () => focus($scope, $element[0]));
}
};
}

View File

@ -40,6 +40,7 @@ describe('Directive focus', () => {
it('should call select function on the element', () => {
let html = `<input vn-focus></input>`;
compile(html);
$scope.$apply();
expect($element[0].select).toHaveBeenCalledWith();
});

View File

@ -1,9 +1,12 @@
@import "variables";
html, body {
html {
background-color: $color-bg;
overflow: auto;
height: 100%;
}
body {
height: 100%;
font-family: vn-font;
color: $color-font;
font-size: $font-size;

View File

@ -1,7 +1,7 @@
<a ng-if="$ctrl.links.btnOne"
vn-tooltip="{{::$ctrl.links.btnOne.tooltip}}"
class="vn-button colored"
ui-sref="{{::$ctrl.links.btnOne.state}}" target="_blank">
ui-sref="{{::$ctrl.links.btnOne.state}}">
<vn-icon
icon="{{::$ctrl.links.btnOne.icon}}">
</vn-icon>
@ -9,7 +9,7 @@
<a ng-if="$ctrl.links.btnTwo"
vn-tooltip="{{::$ctrl.links.btnTwo.tooltip}}"
class="vn-button colored"
ui-sref="{{::$ctrl.links.btnTwo.state}}" target="_blank">
ui-sref="{{::$ctrl.links.btnTwo.state}}">
<vn-icon
icon="{{::$ctrl.links.btnTwo.icon}}">
</vn-icon>
@ -17,7 +17,7 @@
<a ng-if="$ctrl.links.btnThree"
vn-tooltip="{{::$ctrl.links.btnThree.tooltip}}"
class="vn-button colored"
ui-sref="{{::$ctrl.links.btnThree.state}}" target="_blank">
ui-sref="{{::$ctrl.links.btnThree.state}}">
<vn-icon
icon="{{::$ctrl.links.btnThree.icon}}">
</vn-icon>

View File

@ -109,6 +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",
"This phone already exists": "Este teléfono ya existe",
"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

@ -120,16 +120,19 @@ module.exports = Self => {
}
async function getTicketId(params, options) {
const currentDate = new Date();
currentDate.setHours(null, null, null);
const minDate = new Date();
minDate.setHours(0, 0, 0, 0);
const maxDate = new Date();
maxDate.setHours(23, 59, 59, 59);
let ticket = await Self.app.models.Ticket.findOne({
where: {
addressFk: params.addressFk,
companyFk: params.companyFk,
warehouseFk: params.warehouseFk,
shipped: currentDate,
landed: currentDate
shipped: {between: [minDate, maxDate]},
landed: {between: [minDate, maxDate]}
}
}, options);

View File

@ -7,36 +7,34 @@ module.exports = Self => {
type: 'Number',
description: 'The claim id',
http: {source: 'path'}
},
{
}, {
arg: 'warehouseId',
type: 'Number',
description: ''
},
{
description: 'The warehouse id',
required: true
}, {
arg: 'companyId',
type: 'Number',
description: ''
},
{
description: 'The company id',
required: true
}, {
arg: 'dmsTypeId',
type: 'Number',
description: ''
},
{
description: 'The dms type id',
required: true
}, {
arg: 'reference',
type: 'String',
description: ''
},
{
required: true
}, {
arg: 'description',
type: 'String',
description: ''
},
{
required: true
}, {
arg: 'hasFile',
type: 'Boolean',
description: ''
description: 'True if has an attached file',
required: true
}],
returns: {
type: 'Object',

View File

@ -1,10 +1,14 @@
<vn-crud-model auto-load="true"
vn-id="model"
<vn-crud-model vn-id="model" auto-load="true" auto-save="true"
url="claim/api/ClaimEnds"
filter="$ctrl.filter"
data="$ctrl.salesClaimed">
</vn-crud-model>
<vn-crud-model auto-load="true"
url="/claim/api/ClaimDestinations"
data="claimDestinations">
</vn-crud-model>
<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"
@ -28,7 +32,6 @@
translate-attr="{title: 'Imports ticket lines'}">
</vn-button>
<vn-range
vn-one
label="Responsability"
min-label="Company"
max-label="Sales/Client"
@ -82,14 +85,12 @@
</span>
</vn-td>
<vn-td>
<vn-autocomplete vn-one
id="claimDestinationFk"
<vn-autocomplete vn-one id="claimDestinationFk"
ng-model="saleClaimed.claimDestinationFk"
url="/claim/api/ClaimDestinations"
data="claimDestinations"
fields="['id','description']"
value-field="id"
show-field="description"
on-change="$ctrl.setClaimDestination(saleClaimed.id, value)">
show-field="description">
</vn-autocomplete>
</vn-td>
<vn-td>{{::saleClaimed.sale.ticket.landed | date: 'dd/MM/yyyy'}}</vn-td>

View File

@ -19,7 +19,8 @@ class Controller {
}
}
},
{relation: 'claimBeggining'}
{relation: 'claimBeggining'},
{relation: 'claimDestination'}
]
};
this.resolvedState = 3;
@ -82,16 +83,6 @@ class Controller {
this.calculateTotals();
}
setClaimDestination(id, claimDestinationFk) {
if (claimDestinationFk) {
let params = {id: id, claimDestinationFk: claimDestinationFk};
let query = `claim/api/ClaimEnds/`;
this.$http.patch(query, params).then(() => {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
});
}
}
calculateTotals() {
this.claimedTotal = 0;
this.salesClaimed.forEach(sale => {

View File

@ -82,17 +82,6 @@ describe('claim', () => {
});
});
describe('setClaimDestination(id, claimDestinationFk)', () => {
it('should make a patch and call refresh and showSuccess', () => {
spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPATCH(`claim/api/ClaimEnds/`).respond({});
controller.setClaimDestination(1, 1);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
});
});
describe('calculateTotals()', () => {
it('should calculate the total price of the items claimed', () => {
controller.salesClaimed = [

View File

@ -7,36 +7,34 @@ module.exports = Self => {
type: 'Number',
description: 'The client id',
http: {source: 'path'}
},
{
}, {
arg: 'warehouseId',
type: 'Number',
description: ''
},
{
description: 'The warehouse id',
required: true
}, {
arg: 'companyId',
type: 'Number',
description: ''
},
{
description: 'The company id',
required: true
}, {
arg: 'dmsTypeId',
type: 'Number',
description: ''
},
{
description: 'The dms type id',
required: true
}, {
arg: 'reference',
type: 'String',
description: ''
},
{
required: true
}, {
arg: 'description',
type: 'String',
description: ''
},
{
required: true
}, {
arg: 'hasFile',
type: 'Boolean',
description: ''
description: 'True if has an attached file',
required: true
}],
returns: {
type: 'Object',

View File

@ -13,6 +13,10 @@ class Controller {
callback.call(this);
}
get client() {
return this._client;
}
set client(value) {
this._client = value;
@ -32,10 +36,6 @@ class Controller {
};
}
get client() {
return this._client;
}
set quicklinks(value = {}) {
this._quicklinks = Object.assign(value, this._quicklinks);
}

View File

@ -92,6 +92,7 @@ module.exports = Self => {
case 'companyFk':
case 'issued':
case 'dued':
param = `i.${param}`;
return {[param]: value};
}
});

View File

@ -105,15 +105,18 @@ module.exports = Self => {
async function getTicketId(params, options) {
const currentDate = new Date();
currentDate.setHours(null, null, null);
const minDate = new Date();
minDate.setHours(0, 0, 0, 0);
const maxDate = new Date();
maxDate.setHours(23, 59, 59, 59);
let ticket = await Self.app.models.Ticket.findOne({
where: {
addressFk: params.addressFk,
warehouseFk: params.warehouseFk,
shipped: currentDate,
landed: currentDate
shipped: {between: [minDate, maxDate]},
landed: {between: [minDate, maxDate]}
}
}, options);

View File

@ -52,24 +52,31 @@ class Controller {
get freeLineIndex() {
let lines = this.$scope.model.data;
let currentDate = new Date();
currentDate.setHours(0, 0, 0);
let minDate = new Date();
minDate.setHours(0, 0, 0, 0);
let maxDate = new Date();
maxDate.setHours(23, 59, 59, 59);
for (let i = 0; i < lines.length; i++) {
let isFutureDate = new Date(lines[i].date) >= currentDate;
const dated = new Date(lines[i].date);
if (isFutureDate)
let isForFuture = dated > maxDate;
let isForToday = (dated >= minDate && dated <= maxDate);
if (isForFuture || isForToday)
return i;
}
}
get onPreparationLineIndex() {
let lines = this.$scope.model.data;
for (let i = this.freeLineIndex; i >= 0; i--) {
let line = lines[i];
let currentDate = new Date();
currentDate.setHours(0, 0, 0);
currentDate.setHours(0, 0, 0, 0);
let isPastDate = new Date(lines[i].date) < currentDate;
let isPicked = line.alertLevel == 1 && line.isPicked;
@ -95,6 +102,7 @@ class Controller {
let selectedTicketLineIndex = this.givenTicketIndex;
let lineIndex = this.onPreparationLineIndex;
let lines = body.querySelector('vn-tbody').children;
if (lineIndex == undefined || !lines.length) return;
@ -120,7 +128,6 @@ class Controller {
offsetTop = onPreparationLine.offsetTop - headerHeight;
this.$window.scrollTo(0, offsetTop);
this.ticketFk = null;
}

View File

@ -8,12 +8,11 @@
<div class="content-block">
<vn-card class="vn-pa-md vn-w-sm">
<vn-horizontal style="align-items: center;">
<vn-searchbar
<vn-searchbar vn-focus
panel="vn-item-search-panel"
on-search="$ctrl.onSearch($params)"
info="Search items by id, name or barcode"
suggested-filter="{isActive: true}"
vn-focus>
suggested-filter="{isActive: true}">
</vn-searchbar>
<vn-icon-menu
vn-id="more-button"

View File

@ -16,7 +16,7 @@
</vn-textfield>
<vn-autocomplete
vn-one
ng-model="filter.atenderFk"
ng-model="filter.attenderFk"
url="/client/api/Clients/activeWorkersWithRole"
search-function="{firstName: $search}"
value-field="id"

View File

@ -1,121 +1,116 @@
<vn-crud-model
<vn-crud-model auto-load="true"
vn-id="model"
url="/ticket/api/TicketRequests/filter"
limit="20"
data="requests"
order="isOk ASC"
auto-load="false">
order="shipped DESC, isOk ASC">
</vn-crud-model>
<form name="form">
<div class="vn-ma-md">
<vn-card class="vn-pa-md vn-list">
<vn-horizontal>
<vn-searchbar
auto-load="false"
panel="vn-request-search-panel"
on-search="$ctrl.onSearch($params)"
info="Search request by id or alias"
vn-one
vn-focus>
</vn-searchbar>
</vn-horizontal>
<vn-searchbar vn-one vn-focus
auto-load="false"
panel="vn-request-search-panel"
on-search="$ctrl.onSearch($params)"
info="Search request by id or alias"
suggested-filter="$ctrl.filter.where">
</vn-searchbar>
</vn-card>
<vn-card class="vn-my-md">
<vn-table model="model" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th field="ticketFk" number>Ticket ID</vn-th>
<vn-th field="shipped">Shipped</vn-th>
<vn-th field="warehouse">Warehouse</vn-th>
<vn-th field="salesPersonNickname">SalesPerson</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="quantity" number editable>Quantity</vn-th>
<vn-th field="price" number>Price</vn-th>
<vn-th field="atenderNickname">Atender</vn-th>
<vn-th field="itemFk">Item</vn-th>
<vn-th field="description">Concept</vn-th>
<vn-th field="saleQuantity" number>Sale quantity</vn-th>
<vn-th field="isOk">State</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="request in requests">
<vn-td number>
<span class="link"
ng-click="$ctrl.showTicketDescriptor($event, request.ticketFk)">
{{request.ticketFk}}
</span>
</vn-td>
<vn-td>
<span title="{{::request.shipped | date: 'dd/MM/yyyy'}}"
class="chip {{$ctrl.compareDate(request.shipped)}}">
{{::request.shipped | date: 'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td>{{::request.warehouse}}</vn-td>
<vn-td>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.salesPersonFk)">
{{::request.salesPersonNickname}}
</span>
</vn-td>
<vn-td title="{{::request.description}}">{{::request.description}}</vn-td>
<vn-td number>{{::request.quantity}}</vn-td>
<vn-td number>{{::request.price | currency: 'EUR':2}}</vn-td>
<vn-td>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.atenderFk)">
{{::request.atenderNickname}}
</span>
</vn-td>
<vn-td-editable disabled="request.isOk === 0" number>
<text>{{request.itemFk}}</text>
<field>
<vn-input-number
ng-model="request.itemFk"
on-change="$ctrl.confirmRequest(request)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td>
<span
class="link"
ng-click="$ctrl.showItemDescriptor($event, request.itemFk)"
title="{{::request.itemDescription}}">
{{::request.itemDescription}}
</span>
</vn-td>
<vn-td-editable disabled="request.isOk === 0" number>
<text number>{{request.saleQuantity}}</text>
<field>
<vn-input-number
ng-model="request.saleQuantity"
on-change="$ctrl.changeQuantity(request)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td>{{::$ctrl.getState(request.isOk)}}</vn-td>
<vn-td>
<vn-icon
ng-if="request.response.length"
vn-tooltip="{{request.response}}"
icon="insert_drive_file">
</vn-icon>
<vn-icon-button
ng-if="request.isOk != 0"
number
icon="thumb_down"
ng-click="$ctrl.showDenyReason($event, request.id)"
vn-tooltip="Discard">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
<vn-pagination model="model"></vn-pagination>
<vn-data-viewer model="model" class="vn-my-md">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="ticketFk" number>Ticket ID</vn-th>
<vn-th field="shipped">Shipped</vn-th>
<vn-th field="warehouse">Warehouse</vn-th>
<vn-th field="salesPersonNickname">SalesPerson</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="quantity" number editable>Requested</vn-th>
<vn-th field="price" number>Price</vn-th>
<vn-th field="atenderNickname">Atender</vn-th>
<vn-th field="itemFk">Item</vn-th>
<vn-th field="saleQuantity">Achieved</vn-th>
<vn-th field="description">Concept</vn-th>
<vn-th field="isOk">State</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="request in requests">
<vn-td number>
<span class="link"
ng-click="$ctrl.showTicketDescriptor($event, request.ticketFk)">
{{request.ticketFk}}
</span>
</vn-td>
<vn-td>
<span title="{{::request.shipped | date: 'dd/MM/yyyy'}}"
class="chip {{$ctrl.compareDate(request.shipped)}}">
{{::request.shipped | date: 'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td>{{::request.warehouse}}</vn-td>
<vn-td>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.salesPersonFk)">
{{::request.salesPersonNickname}}
</span>
</vn-td>
<vn-td title="{{::request.description}}">{{::request.description}}</vn-td>
<vn-td number>{{::request.quantity}}</vn-td>
<vn-td number>{{::request.price | currency: 'EUR':2}}</vn-td>
<vn-td>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.attenderFk)">
{{::request.atenderNickname}}
</span>
</vn-td>
<vn-td-editable disabled="request.isOk != null" number>
<text>{{request.itemFk}}</text>
<field>
<vn-input-number class="dense" vn-focus
ng-model="request.itemFk">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td-editable disabled="!request.itemFk || request.isOk != null" number>
<text number>{{request.saleQuantity}}</text>
<field>
<vn-input-number class="dense" vn-focus
ng-model="request.saleQuantity"
on-change="$ctrl.changeQuantity(request)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td>
<span
class="link"
ng-click="$ctrl.showItemDescriptor($event, request.itemFk)"
title="{{request.itemDescription}}">
{{request.itemDescription}}
</span>
</vn-td>
<vn-td>{{$ctrl.getState(request.isOk)}}</vn-td>
<vn-td>
<vn-icon
ng-if="request.response.length"
ranslate-attr="{title: request.response}"
icon="insert_drive_file">
</vn-icon>
<vn-icon-button
ng-if="request.isOk != 0"
icon="thumb_down"
ng-click="$ctrl.showDenyReason($event, request)"
translate-attr="{title: 'Discard'}">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
</div>
</form>
<vn-worker-descriptor-popover
@ -129,21 +124,17 @@
</vn-item-descriptor-popover>
<vn-dialog
vn-id="denyReason"
class="modal-form">
on-response="$ctrl.denyRequest(response)">
<tpl-body>
<vn-horizontal class="header">
<h5><span translate>Indicate the reasons to deny this request</span></h5>
</vn-horizontal>
<h5 class="vn-pa-md" translate>Specify the reasons to deny this request</h5>
<vn-horizontal class="vn-pa-md">
<vn-textarea
ng-model="$ctrl.denyObservation">
</vn-textarea>
</vn-horizontal>
<vn-horizontal class="vn-pa-md">
<vn-button
label="Save"
ng-click="$ctrl.denyRequest()">
</vn-button>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="CANCEL" translate-attr="{value: 'Cancel'}"/>
<button response="ACCEPT" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -9,8 +9,23 @@ export default class Controller {
this.$ = $;
this.vnApp = vnApp;
this._ = $translate;
if (!$stateParams.q)
this.filter = {isOk: false, mine: true};
if (!$stateParams.q) {
const today = new Date();
today.setHours(23, 59, 59, 59);
const lastWeek = new Date();
lastWeek.setHours(0, 0, 0, 0);
lastWeek.setDate(lastWeek.getDate() - 7);
this.filter = {
where: {
isOk: false,
mine: true,
from: lastWeek,
to: today
}
};
}
}
$postLink() {
@ -21,7 +36,7 @@ export default class Controller {
getState(isOk) {
if (isOk === null)
return 'Nueva';
else if (isOk === -1 || isOk === 1)
else if (isOk === -1 || isOk)
return 'Aceptada';
else
return 'Denegada';
@ -34,14 +49,12 @@ export default class Controller {
quantity: request.saleQuantity
};
let endpoint = `/api/TicketRequests/${request.id}/confirm`;
let query = `/api/TicketRequests/${request.id}/confirm`;
this.$http.post(query, params).then(res => {
request.itemDescription = res.data.concept;
request.isOk = true;
this.$http.post(endpoint, params).then(() => {
this.vnApp.showSuccess(this._.instant('Data saved!'));
this.$.model.refresh();
}).catch( e => {
this.$.model.refresh();
throw e;
});
}
}
@ -56,10 +69,7 @@ export default class Controller {
this.$http.patch(endpoint, params).then(() => {
this.vnApp.showSuccess(this._.instant('Data saved!'));
}).catch( e => {
this.$.model.refresh();
throw e;
});
}).then(() => this.confirmRequest(request));
} else
this.confirmRequest(request);
}
@ -86,7 +96,7 @@ export default class Controller {
}
showDenyReason(event, requestId) {
this.denyRequestId = requestId;
this.selectedRequest = requestId;
this.$.denyReason.parent = event.target;
this.$.denyReason.show();
document.querySelector('vn-item-request vn-textarea textArea').focus();
@ -96,17 +106,21 @@ export default class Controller {
delete this.denyRequestId;
}
denyRequest() {
denyRequest(response) {
if (response !== 'ACCEPT') return;
let params = {
observation: this.denyObservation
};
let endpoint = `/api/TicketRequests/${this.denyRequestId}/deny`;
let query = `/api/TicketRequests/${this.selectedRequest.id}/deny`;
this.$http.post(query, params).then(res => {
const request = res.data;
this.selectedRequest.isOk = request.isOk;
this.selectedRequest.attenderFk = request.attenderFk;
this.selectedRequest.response = request.response;
this.$http.post(endpoint, params).then(() => {
this.vnApp.showSuccess(this._.instant('Data saved!'));
this.$.model.refresh();
this.$.denyReason.hide();
this.denyObservation = null;
});
}

View File

@ -53,15 +53,15 @@ describe('Item', () => {
let model = controller.$.model;
spyOn(model, 'refresh');
const expectedResult = {concept: 'Melee Weapon'};
let request = {itemFk: 1, saleQuantity: 1, id: 1};
$httpBackend.when('POST', `/api/TicketRequests/${request.id}/confirm`).respond();
$httpBackend.expect('POST', `/api/TicketRequests/${request.id}/confirm`).respond();
$httpBackend.when('POST', `/api/TicketRequests/${request.id}/confirm`).respond(expectedResult);
$httpBackend.expect('POST', `/api/TicketRequests/${request.id}/confirm`).respond(expectedResult);
controller.confirmRequest(request);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect($scope.model.refresh).toHaveBeenCalledWith();
});
});
@ -110,20 +110,17 @@ describe('Item', () => {
describe('denyRequest()', () => {
it(`should perform a query and call vnApp.showSuccess(), refresh(), hide() and set denyObservation to null in the controller`, () => {
spyOn(controller.vnApp, 'showSuccess');
let model = controller.$.model;
spyOn(model, 'refresh');
spyOn(controller.$.denyReason, 'hide');
controller.denyRequestId = 1;
const request = {id: 1};
const expectedResult = {isOk: false, attenderFk: 106, response: 'Denied!'};
controller.selectedRequest = request;
$httpBackend.when('POST', `/api/TicketRequests/${controller.denyRequestId}/deny`).respond();
$httpBackend.expect('POST', `/api/TicketRequests/${controller.denyRequestId}/deny`).respond();
controller.denyRequest();
$httpBackend.when('POST', `/api/TicketRequests/${request.id}/deny`).respond(expectedResult);
$httpBackend.expect('POST', `/api/TicketRequests/${request.id}/deny`).respond(expectedResult);
controller.denyRequest('ACCEPT');
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect($scope.model.refresh).toHaveBeenCalledWith();
expect($scope.denyReason.hide).toHaveBeenCalledWith();
});
});
});

View File

@ -1,5 +1,6 @@
Discard: Descartar
Indicate the reasons to deny this request: Indique las razones para descartar esta peticion
Specify the reasons to deny this request: Especifica las razones para descartar la petición
Buy requests: Peticiones de compra
Search request by id or alias: Buscar peticiones por identificador o alias
Sale quantity: C. conseguida
Requested: Solicitado
Achieved: Conseguido

View File

@ -66,7 +66,7 @@ class Controller {
this.$scope.watcher.check();
this.$scope.model.save().then(() => {
this.$scope.watcher.notifySaved();
this.$scope.model.refresh();
this.$scope.watcher.updateOriginalData();
this.card.reload();
});
}

View File

@ -85,10 +85,13 @@ module.exports = Self => {
return {'r.m3': value};
case 'description':
return {'r.description': {like: `%${value}%`}};
case 'workerFk':
case 'warehouseFk':
param = `v.${param}`;
return {[param]: value};
case 'workerFk':
case 'vehicleFk':
case 'agencyModeFk':
param = `r.${param}`;
return {[param]: value};
}
});

View File

@ -28,12 +28,6 @@ module.exports = Self => {
fields: ['id', 'packages', 'warehouseFk', 'nickname', 'clientFk', 'priority', 'addressFk'],
order: 'priority',
include: [
{
relation: 'client',
scope: {
fields: ['id', 'street', 'postcode'],
}
},
{
relation: 'state',
scope: {

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Route getSelectedItems() should return the selected items 1`] = `
Array [
Object {
"checked": true,
"id": 1,
},
Object {
"checked": true,
"id": 3,
},
Object {
"checked": true,
"id": 5,
},
]
`;

View File

@ -1,104 +1,105 @@
<vn-crud-model
vn-id="model"
url="/api/Routes/{{$ctrl.$stateParams.id}}/getTickets"
url="api/Routes/{{$ctrl.$stateParams.id}}/getTickets"
order="priority ASC"
data="$ctrl.tickets"
auto-load="true">
</vn-crud-model>
<form name="form">
<vn-card class="vn-pa-lg">
<vn-tool-bar class="vn-mb-sm">
<vn-button
icon="icon-wand"
ng-click="$ctrl.guessPriority()"
vn-tooltip="Sort routes">
</vn-button>
<vn-button
disabled="!$ctrl.isChecked"
ng-click="$ctrl.goToBuscaman()"
vn-tooltip="Open buscaman"
tooltip-position="up"
icon="icon-buscaman">
</vn-button>
</vn-tool-bar>
<vn-icon-button
vn-tooltip="Load more"
ng-click="$ctrl.goToBuscaman()">
</vn-icon-button>
<vn-table model="model" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th>Order</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th>Client</vn-th>
<vn-th number shrink>Packages</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink>Warehouse</vn-th>
<vn-th shrink>PC</vn-th>
<vn-th>Street</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="ticket in $ctrl.tickets">
<vn-td shrink>
<vn-check
ng-model="ticket.checked">
</vn-check>
</vn-td>
<vn-td>
<vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority"
rule="Ticket">
</vn-input-number>
</vn-td>
<vn-td number>
<span
ng-click="$ctrl.showTicketDescriptor($event, ticket.id)"
class="link">
{{ticket.id}}
</span>
</vn-td>
<vn-td>
<span
ng-click="$ctrl.showClientDescriptor($event, ticket.clientFk)"
class="link">
{{ticket.nickname}}
</span>
</vn-td>
<vn-td number shrink>{{ticket.packages}}</vn-td>
<vn-td shrink>{{ticket.volume}}</vn-td>
<vn-td shrink>{{ticket.warehouse.name}}</vn-td>
<vn-td shrink>{{ticket.client.postcode}}</vn-td>
<vn-td expand title="{{ticket.client.street}}">{{ticket.client.street}}</vn-td>
<vn-td shrink>
<vn-icon-button
ng-if="ticket.notes.length"
title="::{{ticket.notes[0].description}}"
icon="insert_drive_file">
</vn-icon-button>
</vn-td>
<vn-td>
<vn-icon-button
translate-attr="::{title: 'Remove ticket'}"
icon="delete"
ng-click="$ctrl.showDeleteConfirm(ticket.id)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</form>
<vn-data-viewer model="model">
<form name="form">
<vn-card class="vn-pa-lg">
<vn-tool-bar class="vn-mb-sm">
<vn-button
icon="icon-wand"
ng-click="$ctrl.guessPriority()"
vn-tooltip="Sort routes">
</vn-button>
<vn-button
disabled="!$ctrl.isChecked"
ng-click="$ctrl.goToBuscaman()"
vn-tooltip="Open buscaman"
tooltip-position="up"
icon="icon-buscaman">
</vn-button>
</vn-tool-bar>
<vn-icon-button
vn-tooltip="Load more"
ng-click="$ctrl.goToBuscaman()">
</vn-icon-button>
<vn-table model="model" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th>Order</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th>Client</vn-th>
<vn-th number shrink>Packages</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink>Warehouse</vn-th>
<vn-th expand>Postcode</vn-th>
<vn-th>Street</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="ticket in $ctrl.tickets">
<vn-td shrink>
<vn-check
ng-model="ticket.checked">
</vn-check>
</vn-td>
<vn-td>
<vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority"
rule="Ticket">
</vn-input-number>
</vn-td>
<vn-td number>
<span
ng-click="$ctrl.showTicketDescriptor($event, ticket.id)"
class="link">
{{ticket.id}}
</span>
</vn-td>
<vn-td>
<span
ng-click="$ctrl.showClientDescriptor($event, ticket.clientFk)"
class="link">
{{ticket.nickname}}
</span>
</vn-td>
<vn-td number shrink>{{ticket.packages}}</vn-td>
<vn-td shrink>{{ticket.volume}}</vn-td>
<vn-td shrink>{{ticket.warehouse.name}}</vn-td>
<vn-td number shrink>{{ticket.address.postalCode}}</vn-td>
<vn-td expand title="{{ticket.address.street}}">{{ticket.address.street}}</vn-td>
<vn-td shrink>
<vn-icon-button
ng-if="ticket.notes.length"
title="::{{ticket.notes[0].description}}"
icon="insert_drive_file">
</vn-icon-button>
</vn-td>
<vn-td>
<vn-icon-button
translate-attr="::{title: 'Remove ticket'}"
icon="delete"
ng-click="$ctrl.showDeleteConfirm(ticket.id)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</form>
</vn-data-viewer>
<vn-ticket-descriptor-popover
vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover>
@ -109,4 +110,72 @@
vn-id="confirm"
question="Delete ticket from route?"
on-response="$ctrl.removeTicketFromRoute(response)">
</vn-confirm>
</vn-confirm>
<vn-crud-model
vn-id="possibleTicketsModel"
url="api/Tickets"
filter="$ctrl.possibleTicketsFilter"
data="$ctrl.possibleTickets">
</vn-crud-model>
<vn-dialog
vn-id="possibleTicketsDialog"
on-response="$ctrl.setTicketsRoute(response)">
<tpl-body>
<section class="header vn-pa-md">
<h5><span translate>Tickets to add</span></h5>
</section>
<vn-data-viewer class="vn-pa-md" model="possibleTicketsModel">
<vn-table model="possibleTicketsModel" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="possibleTicketsModel">
</vn-multi-check>
</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th>Client</vn-th>
<vn-th number shrink>Packages</vn-th>
<vn-th shrink>Warehouse</vn-th>
<vn-th expand>Postcode</vn-th>
<vn-th>Address</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="ticket in $ctrl.possibleTickets">
<vn-td shrink>
<vn-check
ng-model="ticket.checked">
</vn-check>
</vn-td>
<vn-td number>{{ticket.id}}</vn-td>
<vn-td number>
<span
ng-click="$ctrl.showClientDescriptor($event, ticket.clientFk)"
class="link">
{{ticket.nickname}}
</span>
</vn-td>
<vn-td number shrink>{{ticket.packages}}</vn-td>
<vn-td expand>{{ticket.warehouse.name}}</vn-td>
<vn-td number shrink>{{ticket.address.postalCode}}</vn-td>
<vn-td expand title="{{ticket.address.street}}">{{ticket.address.street}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-data-viewer>
</tpl-body>
<tpl-buttons>
<input type="button" response="CANCEL" translate-attr="{value: 'Cancel'}"/>
<button response="ACCEPT" translate>Add</button>
</tpl-buttons>
</vn-dialog>
<vn-float-button
icon="add"
ng-click="$ctrl.openPossibleTicketsDialog()"
vn-tooltip="Add ticket"
vn-acl="delivery"
vn-acl-action="remove"
vn-bind="+"
fixed-bottom-right>
</vn-float-button>

View File

@ -2,12 +2,23 @@ import ngModule from '../module';
import './style.scss';
class Controller {
constructor($stateParams, $, $translate, $http, vnApp) {
constructor($stateParams, $scope, $translate, $http, vnApp, $filter) {
this.$translate = $translate;
this.$stateParams = $stateParams;
this.$ = $;
this.$ = $scope;
this.$http = $http;
this.vnApp = vnApp;
this.$filter = $filter;
}
set route(value) {
this._route = value;
if (value)
this.buildPossibleTicketsFilter();
}
get route() {
return this._route;
}
get isChecked() {
@ -19,13 +30,37 @@ class Controller {
return false;
}
buildPossibleTicketsFilter() {
let minDate = new Date(this.route.finished);
minDate.setHours(0, 0, 0, 0);
let maxDate = new Date(this.route.finished);
maxDate.setHours(23, 59, 59, 59);
this.possibleTicketsFilter = {
where: {
zoneFk: this.route.zoneFk,
routeFk: null,
landed: {between: [minDate, maxDate]},
},
include: [
{
relation: 'warehouse',
scope: {
fields: ['name']
},
}, {
relation: 'address'
}
]
};
}
getHighestPriority() {
let max = 0;
this.$.model.data.forEach(tag => {
if (tag.priority > max)
max = tag.priority;
});
return max + 1;
let highestPriority = Math.max(...this.$.model.data.map(tag => {
return tag.priority;
}));
return highestPriority + 1;
}
setPriority(id, priority) {
@ -37,16 +72,16 @@ class Controller {
});
}
getCheckedLines() {
let lines = [];
let data = this.tickets;
if (data) {
for (let i = 0; i < data.length; i++) {
if (data[i].checked)
lines.push(data[i]);
getSelectedItems(items) {
const selectedItems = [];
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].checked)
selectedItems.push(items[i]);
}
}
return lines;
return selectedItems;
}
goToBuscaman() {
@ -54,7 +89,7 @@ class Controller {
let firstAddress = `46460 Av Espioca 100-46460 Silla`;
let addresses = firstAddress;
let lines = this.getCheckedLines();
let lines = this.getSelectedItems(this.tickets);
let url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
lines.forEach(line => {
@ -64,8 +99,8 @@ class Controller {
window.open(url + addresses, '_blank');
}
showDeleteConfirm(ticket) {
this.selectedTicket = ticket;
showDeleteConfirm(id) {
this.selectedTicket = id;
this.$.confirm.show();
}
@ -109,14 +144,38 @@ class Controller {
this.$.clientDescriptor.show();
event.preventDefault();
}
openPossibleTicketsDialog() {
this.$.possibleTicketsModel.refresh();
this.$.possibleTicketsDialog.show();
}
setTicketsRoute(response) {
if (response === 'ACCEPT') {
let tickets = this.getSelectedItems(this.possibleTickets);
for (let i = 0; i < tickets.length; i++) {
delete tickets[i].checked;
tickets[i].routeFk = this.route.id;
}
return this.$.possibleTicketsModel.save().then(() => {
this.$.model.data = this.$.model.data.concat(tickets);
});
}
return Promise.resolve();
}
}
Controller.$inject = ['$stateParams', '$scope', '$translate', '$http', 'vnApp'];
Controller.$inject = ['$stateParams', '$scope', '$translate', '$http', 'vnApp', '$filter'];
ngModule.component('vnRouteTickets', {
template: require('./index.html'),
controller: Controller,
require: {
card: '^vnRouteCard'
},
controller: Controller
bindings: {
route: '<'
}
});

View File

@ -0,0 +1,288 @@
import './index.js';
describe('Route', () => {
let controller;
let $httpBackend;
beforeEach(angular.mock.module('route', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
controller = $componentController('vnRouteTickets');
}));
describe('route setter/getter', () => {
it('should return the route id', () => {
controller.route = 2;
expect(controller.route).toEqual(2);
});
});
describe('isChecked getter', () => {
it('should return false if none of the tickets is checked or there are no tickets', () => {
expect(controller.isChecked).toBeFalsy();
});
it('should return true if any of the tickets is checked', () => {
controller.tickets = [{checked: true}];
expect(controller.isChecked).toBeTruthy();
});
});
describe('buildPossibleTicketsFilter()', () => {
it('should build the possible tickets filter', () => {
let expectedFilter = {
include: [
{
relation: 'warehouse',
scope: {
fields: ['name']
}
}, {
relation: 'address'
}
],
where: {
landed: {
between: [
jasmine.any(Date),
jasmine.any(Date)
]
},
routeFk: null,
zoneFk: 67
}
};
controller.route = {
finished: new Date(),
routeFk: null,
zoneFk: 67
};
controller.buildPossibleTicketsFilter();
expect(controller.possibleTicketsFilter).toEqual(expectedFilter);
});
});
describe('getHighestPriority()', () => {
it('should return the highest value found in priorities plus 1', () => {
controller.$.model = {data: [
{priority: 99},
{priority: 1},
{priority: 2},
{priority: 3},
{priority: 4},
{priority: 5},
]};
let result = controller.getHighestPriority();
expect(result).toEqual(100);
});
});
describe('setPriority()', () => {
it('should set a ticket priority', () => {
controller.$.model = {refresh: () => {}};
spyOn(controller.$.model, 'refresh');
spyOn(controller.vnApp, 'showSuccess');
const ticketId = 1;
const priority = 999;
$httpBackend.expectPATCH(`/api/Tickets/${ticketId}/`).respond('ok');
controller.setPriority(ticketId, priority);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
describe('getSelectedItems()', () => {
it('should return the selected items', () => {
let items = [
{id: 1, checked: true},
{id: 2, checked: false},
{id: 3, checked: true},
{id: 4, checked: false},
{id: 5, checked: true},
];
let selectedItems = controller.getSelectedItems(items);
expect(selectedItems).toMatchSnapshot();
});
});
describe('goToBuscaman()', () => {
it('should open buscaman with the given arguments', () => {
spyOn(window, 'open');
const expectedUrl = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=46460 Av Espioca 100-46460 Silla+to:n19 my street-n19 London';
controller.tickets = [
{
id: 1,
checked: true,
address: {
street: 'my street',
postalCode: 'n19',
city: 'London'
}
},
];
controller.goToBuscaman();
expect(window.open).toHaveBeenCalledWith(expectedUrl, '_blank');
});
});
describe('showDeleteConfirm()', () => {
it('should open a confirm dialog after setting the selected ticket into the controller', () => {
controller.$.confirm = {show: () => {}};
spyOn(controller.$.confirm, 'show');
let ticketId = 1;
controller.showDeleteConfirm(ticketId);
expect(controller.selectedTicket).toEqual(ticketId);
expect(controller.$.confirm.show).toHaveBeenCalledWith();
});
});
describe('removeTicketFromRoute()', () => {
it('should perform a patch query then call showSuccess and updateVolume methods', () => {
spyOn(controller, 'updateVolume');
spyOn(controller.vnApp, 'showSuccess');
let ticketId = 1;
controller.selectedTicket = ticketId;
$httpBackend.expectPATCH(`/api/Tickets/${ticketId}/`).respond('ok');
controller.removeTicketFromRoute('ACCEPT');
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Ticket removed from route');
expect(controller.updateVolume).toHaveBeenCalledWith();
});
});
describe('updateVolume()', () => {
it('should perform a POST query then call both reload and refresh methods', () => {
controller.$.model = {refresh: () => {}};
controller.card = {reload: () => {}};
controller.$stateParamds = {id: 999};
spyOn(controller.$.model, 'refresh');
spyOn(controller.card, 'reload');
let ticketId = 1;
controller.selectedTicket = ticketId;
const url = `/route/api/Routes/${controller.$stateParams.id}/updateVolume`;
$httpBackend.expectPOST(url).respond('ok');
controller.updateVolume();
$httpBackend.flush();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
expect(controller.card.reload).toHaveBeenCalledWith();
});
});
describe('guessPriority()', () => {
it('should perform a GET query then call both refresh and showSuccess methods', () => {
controller.$.model = {refresh: () => {}};
spyOn(controller.$.model, 'refresh');
spyOn(controller.vnApp, 'showSuccess');
controller.$stateParams = {id: 99};
const url = `/api/Routes/${controller.$stateParams.id}/guessPriority/`;
$httpBackend.expectGET(url).respond('ok');
controller.guessPriority();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Order changed');
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
describe('showTicketDescriptor()', () => {
it('should call the descriptor show function after setting the parent and the ticket id', () => {
controller.$.ticketDescriptor = {show: () => {}};
spyOn(controller.$.ticketDescriptor, 'show');
const event = {target: {}, preventDefault: () => {}};
spyOn(event, 'preventDefault');
const ticketId = 999;
controller.showTicketDescriptor(event, ticketId);
expect(controller.$.ticketDescriptor.ticketFk).toEqual(ticketId);
expect(controller.$.ticketDescriptor.show).toHaveBeenCalledWith();
expect(event.preventDefault).toHaveBeenCalledWith();
});
});
describe('showClientDescriptor()', () => {
it('should call the descriptor show method after setting the parent and the client id', () => {
controller.$.clientDescriptor = {show: () => {}};
spyOn(controller.$.clientDescriptor, 'show');
const event = {target: {}, preventDefault: () => {}};
spyOn(event, 'preventDefault');
const clientId = 999;
controller.showClientDescriptor(event, clientId);
expect(controller.$.clientDescriptor.clientFk).toEqual(clientId);
expect(controller.$.clientDescriptor.show).toHaveBeenCalledWith();
expect(event.preventDefault).toHaveBeenCalledWith();
});
});
describe('openPossibleTicketsDialog()', () => {
it('should call both refresh and show methods in posible tickets model and dialog', () => {
controller.$.possibleTicketsModel = {refresh: () => {}};
spyOn(controller.$.possibleTicketsModel, 'refresh');
controller.$.possibleTicketsDialog = {show: () => {}};
spyOn(controller.$.possibleTicketsDialog, 'show');
controller.openPossibleTicketsDialog();
expect(controller.$.possibleTicketsModel.refresh).toHaveBeenCalledWith();
expect(controller.$.possibleTicketsDialog.show).toHaveBeenCalledWith();
});
});
describe('setTicketsRoute()', () => {
it('should perform a POST query to add tickets to the route', done => {
controller.$.possibleTicketsModel = {save: () => {}};
spyOn(controller.$.possibleTicketsModel, 'save').and.returnValue(Promise.resolve());
controller.$.model = {data: [
{id: 1, checked: false}
]};
controller.route = {id: 111};
controller.possibleTickets = [
{id: 2, checked: false},
{id: 3, checked: true},
{id: 4, checked: false},
{id: 5, checked: true},
];
let expectedResult = [
{checked: false, id: 1},
{id: 3, routeFk: 111},
{id: 5, routeFk: 111}
];
controller.setTicketsRoute('ACCEPT').then(() => {
expect(controller.$.model.data).toEqual(expectedResult);
done();
}).catch(done.fail);
});
it('should just return a promise', () => {
expect(controller.setTicketsRoute('CANCEL')).toEqual(jasmine.any(Promise));
});
});
});

View File

@ -3,4 +3,6 @@ Open buscaman: Abrir buscaman
Ticket removed from route: Ticket borrado de la ruta
Order changed: Orden cambiado
Delete ticket from route?: ¿Borrar ticket de la ruta?
Sort routes: Ordenar rutas
Sort routes: Ordenar rutas
Add ticket: Añadir ticket
Tickets to add: Tickets a añadir

View File

@ -38,27 +38,27 @@ module.exports = Self => {
try {
let options = {transaction: tx};
let item = await models.Item.findById(ctx.args.itemFk);
let item = await models.Item.findById(ctx.args.itemFk, null, options);
if (!item)
throw new UserError(`That item doesn't exists`);
let request = await models.TicketRequest.findById(ctx.args.id, {
include: {relation: 'ticket'}
});
}, options);
let [[stock]] = await Self.rawSql(`CALL vn.getItemVisibleAvailable(?,?,?,?)`, [
ctx.args.itemFk,
request.ticket().shipped,
request.ticket().warehouseFk,
false
]);
], options);
if (stock.available < 0)
throw new UserError(`This item is not available`);
if (request.saleFk) {
sale = await models.Sale.findById(request.saleFk);
sale = await models.Sale.findById(request.saleFk, null, options);
sale.updateAttributes({
itemFk: ctx.args.itemFk,
quantity: ctx.args.quantity,
@ -71,7 +71,11 @@ module.exports = Self => {
quantity: ctx.args.quantity,
concept: item.name
}, options);
request.updateAttributes({saleFk: sale.id, itemFk: sale.itemFk, isOk: true}, options);
request.updateAttributes({
saleFk: sale.id,
itemFk: sale.itemFk,
isOk: true
}, options);
}
query = `CALL vn.ticketCalculateSale(?)`;
@ -86,6 +90,8 @@ module.exports = Self => {
}, options);
await tx.commit();
return sale;
} catch (error) {
await tx.rollback();
throw error;

View File

@ -29,7 +29,7 @@ module.exports = Self => {
let params = {
isOk: false,
atenderFk: worker.id,
attenderFk: worker.id,
response: ctx.args.observation,
};

View File

@ -28,7 +28,7 @@ module.exports = Self => {
type: 'Number',
description: `Search by warehouse`
}, {
arg: 'atenderFk',
arg: 'attenderFk',
type: 'Number',
description: `Search requests atended by the given worker`
}, {
@ -65,7 +65,7 @@ module.exports = Self => {
let worker = await Self.app.models.Worker.findOne({where: {userFk: userId}});
if (ctx.args.mine)
ctx.args.atenderFk = worker.id;
ctx.args.attenderFk = worker.id;
let where = buildFilter(ctx.args, (param, value) => {
switch (param) {
@ -75,7 +75,7 @@ module.exports = Self => {
: {'t.nickname': {like: `%${value}%`}};
case 'ticketFk':
return {'t.id': value};
case 'atenderFk':
case 'attenderFk':
return {'tr.atenderFk': value};
case 'isOk':
return {'tr.isOk': value};
@ -106,13 +106,13 @@ module.exports = Self => {
tr.ticketFk,
tr.quantity,
tr.price,
tr.atenderFk,
tr.atenderFk attenderFk,
tr.description,
tr.response,
tr.saleFk,
tr.isOk,
s.quantity AS saleQuantity,
s.itemFK,
s.itemFk,
i.name AS itemDescription,
t.shipped,
t.nickname,

View File

@ -1,27 +1,14 @@
const app = require('vn-loopback/server/server');
describe('ticket-request confirm()', () => {
let request;
let sale;
let originalRequest;
let originalSale;
let createdSaleId;
afterAll(async done => {
const paramsForRequest = {
saleFk: request.saleFk,
isOk: request.isOk,
itemFk: request.itemFk,
ticketFk: request.ticketFk
};
const paramsForSale = {
itemFk: sale.itemFk,
quantity: sale.quantity,
concept: sale.concept,
};
await request.updateAttributes(paramsForRequest);
await sale.updateAttributes(paramsForSale);
app.models.Sale.destroyById(createdSaleId);
await originalRequest.updateAttributes(originalRequest);
await originalSale.updateAttributes(originalSale);
await app.models.Sale.destroyById(createdSaleId);
done();
});
@ -65,10 +52,11 @@ describe('ticket-request confirm()', () => {
const itemId = 1;
const quantity = 10;
request = await app.models.TicketRequest.findById(requestId);
sale = await app.models.Sale.findById(saleId);
originalRequest = await app.models.TicketRequest.findById(requestId);
originalSale = await app.models.Sale.findById(saleId);
request.updateAttributes({saleFk: saleId});
const request = await app.models.TicketRequest.findById(requestId);
await request.updateAttributes({saleFk: saleId});
let ctx = {req: {accessToken: {userId: 9}}, args: {
itemFk: itemId,
@ -89,7 +77,8 @@ describe('ticket-request confirm()', () => {
const itemId = 1;
const quantity = 10;
request.updateAttributes({saleFk: null});
const request = await app.models.TicketRequest.findById(requestId);
await request.updateAttributes({saleFk: null});
let ctx = {req: {accessToken: {userId: 9}}, args: {
itemFk: itemId,

View File

@ -5,7 +5,7 @@ describe('ticket-request deny()', () => {
afterAll(async done => {
let params = {
isOk: null,
atenderFk: request.atenderFk,
attenderFk: request.attenderFk,
response: null,
};

View File

@ -37,7 +37,7 @@ describe('ticket-request filter()', () => {
});
it('should return the ticket request matching the atender ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {atenderFk: 35}};
let ctx = {req: {accessToken: {userId: 9}}, args: {attenderFk: 35}};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;

View File

@ -40,7 +40,8 @@ module.exports = function(Self) {
}, options);
const hasPurchaseRequests = await models.TicketRequest.count({
ticketFk: id
ticketFk: id,
isOk: true
}, options);
const isEmpty = !hasSales && !hasPackages &&

View File

@ -7,36 +7,34 @@ module.exports = Self => {
type: 'Number',
description: 'The ticket id',
http: {source: 'path'}
},
{
}, {
arg: 'warehouseId',
type: 'Number',
description: ''
},
{
description: 'The warehouse id',
required: true
}, {
arg: 'companyId',
type: 'Number',
description: ''
},
{
description: 'The company id',
required: true
}, {
arg: 'dmsTypeId',
type: 'Number',
description: ''
},
{
description: 'The dms type id',
required: true
}, {
arg: 'reference',
type: 'String',
description: ''
},
{
required: true
}, {
arg: 'description',
type: 'String',
description: ''
},
{
required: true
}, {
arg: 'hasFile',
type: 'Boolean',
description: ''
description: 'True if has an attached file',
required: true
}],
returns: {
type: 'Object',

View File

@ -33,9 +33,12 @@
"isOk": {
"type": "Boolean"
},
"atenderFk": {
"attenderFk": {
"type": "Number",
"required": true
"required": true,
"mysql": {
"columnName": "atenderFk"
}
},
"response": {
"type": "String"
@ -55,7 +58,7 @@
"atender": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "atenderFk"
"foreignKey": "attenderFk"
},
"requester": {
"type": "belongsTo",

View File

@ -176,7 +176,7 @@ class Controller {
links.btnTwo = {
icon: 'icon-stowaway',
state: `ticket.card.summary({id: ${value.stowaway.shipFk}})`,
tooltip: 'Ship'
tooltip: 'Ship stowaways'
};
}

View File

@ -18,7 +18,7 @@
</vn-textfield>
<vn-autocomplete
vn-one
ng-model="$ctrl.ticketRequest.atenderFk"
ng-model="$ctrl.ticketRequest.attenderFk"
url="/client/api/Clients/activeWorkersWithRole"
show-field="nickname"
search-function="{firstName: $search}"

View File

@ -1,7 +1,7 @@
<vn-crud-model
vn-id="model"
url="/ticket/api/TicketRequests"
fields="['id', 'description', 'created', 'requesterFk', 'atenderFk', 'quantity', 'price', 'saleFk', 'isOk']"
fields="['id', 'description', 'created', 'requesterFk', 'attenderFk', 'quantity', 'price', 'saleFk', 'isOk']"
order="created ASC"
link="{ticketFk: $ctrl.$stateParams.id}"
filter="::$ctrl.filter"
@ -43,7 +43,7 @@
<vn-td expand>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.atenderFk)">
ng-click="$ctrl.showWorkerDescriptor($event, request.attenderFk)">
{{::request.atender.user.nickname | dashIfEmpty}}
</span>
</vn-td>

View File

@ -127,7 +127,7 @@
<vn-td-editable ng-if="sale.id" disabled="!$ctrl.isEditable" number>
<text>{{sale.quantity}}</text>
<field>
<vn-input-number
<vn-input-number class="dense"
vn-focus
ng-model="sale.quantity"
on-change="$ctrl.onChangeQuantity(sale)">

View File

@ -1,48 +0,0 @@
/*
Author : Enrique Blasco BLanquer
Date: 27 de mayo de 2019
*/
module.exports = Self => {
Self.remoteMethodCtx('addAutoTime', {
description: 'Adds a new hour registry by app in manual 0',
accessType: 'WRITE',
accepts: [{
arg: 'data',
type: 'object',
required: true,
description: 'timed',
http: {source: 'body'}
}],
returns: [{
type: 'Object',
root: true
}],
http: {
path: `/addAutoTime`,
verb: 'POST'
}
});
Self.addAutoTime = async(ctx, data) => {
const myUserId = ctx.req.accessToken.userId;
// get all worked time control, needed to calculate order
let hours = await Self.rawSql(`SELECT * FROM vn.workerTimeControl
WHERE userFk = ?
AND DATE(timed) = CURDATE()
ORDER BY timed DESC LIMIT 1`, [myUserId]);
// 1 get next order
let order = 0;
if (hours.length > 0)
order = hours[hours.length - 1].order;
// 2 create element in db
return Self.create({
userFk: myUserId,
timed: data.timed,
order: order + 1,
manual: 0
});
};
};

View File

@ -1,121 +0,0 @@
/*
Author : Enrique Blasco BLanquer
Date: 28 de mayo de 2019
*/
module.exports = Self => {
Self.remoteMethodCtx('getHoursWorked', {
description: 'Get worked hours in current week, month and year',
accessType: 'WRITE',
returns: [{
type: 'Object',
root: true
}],
http: {
path: `/getHoursWorked`,
verb: 'GET'
}
});
Self.getHoursWorked = async(ctx, data) => {
let totalHours = 0; // total hours worked in one year
let totalMinutes = 0; // total minutes worked in one year
let totalHoursMonth = 0; // total hours worked in one month
let totalMinutesMonth = 0; // total minutes worked in one month
let totalHoursWeek = 0; // total hours worked in one week
let totalMinutesWeek = 0; // total minutes worked in one week
const myUserId = ctx.req.accessToken.userId; // user id
let today = new Date(); // needed to calculate total hours worked to current date
let fromDate = today.getFullYear() + '-01-01'; // from date, current year
let toDate = today.getFullYear() + '-12-31'; // to date, current year
// 1 hours worked in a year
let hoursYear = await Self.rawSql(`SELECT wtc.userFk, DATE(wtc.timed) dated,
UNIX_TIMESTAMP(MIN(timed))timedStart,
SEC_TO_TIME(SUM(if( mod(wtc.order,2)=1,
UNIX_TIMESTAMP(timed) *-1,
UNIX_TIMESTAMP(timed)))) timeWorkDay
FROM vn.workerTimeControl wtc
WHERE wtc.timed BETWEEN ? AND ? AND userFk = ?
GROUP BY wtc.userFk,dated ORDER BY dated DESC`, [fromDate, toDate, myUserId]);
// 2 Get days of week
let week = [];
// Starting Monday not Sunday
let current = new Date();
current.setDate((current.getDate() - current.getDay() + 1));
for (let i = 0; i < 7; i++) {
week.push(
new Date(current)
);
current.setDate(current.getDate() + 1);
}
// 3 I have all timed control for one year... NOW I CALCULATE TOTAL HOURS IN YEAR, MONTH, WEEK, Let's GO!
for (hour of hoursYear) {
if (parseInt(hour.timeWorkDay.split(':')[0]) > 0) {
// YEAR
totalHours += parseInt(hour.timeWorkDay.split(':')[0]);
totalMinutes += parseInt(hour.timeWorkDay.split(':')[1]);
// If it exceeds 5 hours we add 20 minutes of breakfast.
if (parseInt(hour.timeWorkDay.split(':')[0]) >= 5)
totalMinutes += 20;
// MONTH
if ((new Date(hour.dated)).getMonth() == today.getMonth()) {
totalHoursMonth += parseInt(hour.timeWorkDay.split(':')[0]);
totalMinutesMonth += parseInt(hour.timeWorkDay.split(':')[1]);
// If it exceeds 5 hours we add 20 minutes of breakfast.
if (parseInt(hour.timeWorkDay.split(':')[0]) >= 5)
totalMinutesMonth += 20;
}
// WEEK
for (day of week) {
let dayOfWeek = new Date(day);
let dayOfCurrentWeek = new Date(hour.dated);
if (dayOfWeek.getMonth() == dayOfCurrentWeek.getMonth() && dayOfWeek.getDate() == dayOfCurrentWeek.getDate()) {
totalHoursWeek += parseInt(hour.timeWorkDay.split(':')[0]);
totalMinutesWeek += parseInt(hour.timeWorkDay.split(':')[1]);
// If it exceeds 5 hours we add 20 minutes of breakfast.
if (parseInt(hour.timeWorkDay.split(':')[0]) >= 5)
totalMinutesWeek += 20;
break;
}
}
}
}
// TOTAL WORKED HOURS IN THE YEAR
totalHours += totalMinutes / 60;
totalHours = decimalToHour(totalHours);
// TOTAL WORKED HOURS IN THE MONTH
totalHoursMonth += totalMinutesMonth / 60;
totalHoursMonth = decimalToHour(totalHoursMonth);
// TOTAL WORKED HOURS IN THE WEEK
totalHoursWeek += totalMinutesWeek / 60;
totalHoursWeek = decimalToHour(totalHoursWeek);
return {
'totalWorekdYear': totalHours,
'totalWorekdMonth': totalHoursMonth,
'totalWorkedWeek': totalHoursWeek
};
};
};
/*
function to calculate hours and minutes from decimal value
*/
function decimalToHour(value) {
let decimalTime = parseFloat(value);
decimalTime = decimalTime * 60 * 60;
let hoursDay = Math.floor((decimalTime / (60 * 60)));
decimalTime = decimalTime - (hoursDay * 60 * 60);
let minutesDay = Math.floor((decimalTime / 60));
return hoursDay + ':' + minutesDay;
}

View File

@ -1,78 +0,0 @@
/*
Author : Enrique Blasco BLanquer
Date: 29 de mayo de 2019
*/
module.exports = Self => {
Self.remoteMethodCtx('getWorkedWeek', {
description: 'get worked week info',
accessType: 'WRITE',
returns: [{
type: 'Object',
root: true
}],
http: {
path: `/getWorkedWeek`,
verb: 'GET'
}
});
Self.getWorkedWeek = async(ctx, data) => {
const myUserId = ctx.req.accessToken.userId; // user id
let lastDate = new Date('1986-09-24'); // reference date
let diff = 0; // difference of value between two dates
let total = 0; // total hours
// 1 Get days of week
let week = [];
// 2 Starting Monday not Sunday
let current = new Date();
current.setDate((current.getDate() - current.getDay() + 1));
for (let i = 0; i < 7; i++) {
week.push(
new Date(current)
);
current.setDate(current.getDate() + 1);
}
let fromDate = week[0].getFullYear() + '-' + (week[0].getMonth() + 1) + '-' + week[0].getDate();
let toDate = week[week.length - 1].getFullYear() + '-' + (week[week.length - 1].getMonth() + 1) + '-' + week[week.length - 1].getDate();
// 3 hours worked in a current week
let hoursWeek = await Self.rawSql(`SELECT wtc.timed ,wtc.order
FROM vn.workerTimeControl wtc
WHERE userFk = ?
AND DATE(timed) BETWEEN ? AND ? ORDER BY timed DESC;`, [myUserId, fromDate, toDate]);
// 4 treat data
let isFirst = true;
for (let i = hoursWeek.length - 1; i >= 0; i--) {
let d = new Date(hoursWeek[i].timed);
if (isFirst) {
lastDate = d;
isFirst = false;
} else {
if (lastDate.getDate() === d.getDate()) {
diff += Math.abs(d.getTime() - lastDate.getTime());
lastDate = d;
} else {
total += diff;
diff = 0;
lastDate = d;
}
}
}
total += diff;
// 5 calculate hours and minutes
let decimalTime = total / 1000 / 3600;
decimalTime = decimalTime * 60 * 60;
let hours = Math.floor((decimalTime / (60 * 60)));
decimalTime = decimalTime - (hours * 60 * 60);
let minutes = Math.floor((decimalTime / 60));
return {'timeds': hoursWeek, 'totalWorked': hours + ':' + minutes};
};
};

View File

@ -1,11 +0,0 @@
const app = require('vn-loopback/server/server');
describe('workerTimeControl addAutoTime()', () => {
it('should return an undefined value', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let data = {'timed': new Date()};
let result = await app.models.WorkerTimeControl.addAutoTime(ctx, data);
expect(result).toBeUndefined();
});
});

View File

@ -1,10 +0,0 @@
const app = require('vn-loopback/server/server');
describe('workerTimeControl getHoursWorked()', () => {
it('should return an totalWorkedYear to be defined', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let result = await app.models.WorkerTimeControl.getHoursWorked(ctx, null);
expect(result.totalWorekdYear).toBeDefined();
});
});

View File

@ -1,10 +0,0 @@
const app = require('vn-loopback/server/server');
describe('workerTimeControl getWorkedWeek()', () => {
it('should return an timeds to be defined', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let result = await app.models.WorkerTimeControl.getWorkedWeek(ctx, null);
expect(result.timeds).toBeDefined();
});
});

View File

@ -49,14 +49,5 @@
},
"Device": {
"dataSource": "vn"
},
"UserPhoneType": {
"dataSource": "vn"
},
"UserPhone": {
"dataSource": "vn"
},
"UserLog": {
"dataSource": "vn"
}
}

View File

@ -3,9 +3,6 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
require('../methods/worker-time-control/filter')(Self);
require('../methods/worker-time-control/addTime')(Self);
require('../methods/worker-time-control/addAutoTime')(Self);
require('../methods/worker-time-control/getHoursWorked')(Self);
require('../methods/worker-time-control/getWorkedWeek')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')

View File

@ -54,6 +54,12 @@
"type": "hasMany",
"model": "WorkerTeamCollegues",
"foreignKey": "workerFk"
},
"phones": {
"type": "hasMany",
"model": "UserPhone",
"foreignKey": "userFk",
"primaryKey": "userFk"
}
}
}

View File

@ -23,14 +23,6 @@
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Phone"
ng-model="$ctrl.worker.phone"
rule>
</vn-textfield>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>

View File

@ -40,6 +40,12 @@ class Controller {
relation: 'department'
}
}
}, {
relation: 'phones',
scope: {
fields: ['phone'],
order: 'typeFk ASC'
}
}
]
};

View File

@ -26,20 +26,16 @@
<vn-label-value label="Department"
value="{{$ctrl.worker.department.department.name}}">
</vn-label-value>
<vn-label-value label="Phone"
value="{{$ctrl.worker.phone}}">
<vn-label-value ng-repeat ="phone in $ctrl.worker.phones"
label="Phone"
value="{{phone.phone}}">
</vn-label-value>
<vn-label-value label="Extension"
value="{{$ctrl.worker.sip.extension}}">
</vn-label-value>
</div>
<div class="quicklinks">
<a
ui-sref="client.card.summary({id: $ctrl.worker.userFk})"
vn-tooltip="Go to client"
class="vn-button">
<vn-icon icon="person"></vn-icon>
</a>
</div>
<vn-quick-links
links="$ctrl.quicklinks">
</vn-quick-links>
</div>
</div>

View File

@ -1,7 +1,41 @@
import ngModule from '../module';
class Controller {
constructor($http, $state) {
this.$state = $state;
this.$http = $http;
}
get worker() {
return this._worker;
}
set worker(value) {
this._worker = value;
if (!value) return;
this._quicklinks = {
btnOne: {
icon: 'person',
state: `client.card.summary({id: ${value.userFk}})`,
tooltip: 'Go to client'
}
};
}
set quicklinks(value = {}) {
this._quicklinks = Object.assign(value, this._quicklinks);
}
get quicklinks() {
return this._quicklinks;
}
}
ngModule.component('vnWorkerDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
worker: '<'
}

View File

@ -12,3 +12,4 @@ import './department';
import './calendar';
import './time-control';
import './log';
import './phones';

View File

@ -0,0 +1,54 @@
<vn-crud-model
url="/api/UserPhoneTypes"
data="phoneTypes"
auto-load="true">
</vn-crud-model>
<vn-crud-model
vn-id="model"
url="/api/UserPhones"
data="$ctrl.phones">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="$ctrl.phones">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" compact>
<vn-card class="vn-pa-lg">
<vn-horizontal ng-repeat="workerPhone in $ctrl.phones">
<vn-autocomplete
vn-one
ng-model="workerPhone.typeFk"
initial-data="workerPhone.typeFk"
data ="phoneTypes"
show-field="code"
value-field="code"
label="Type"
vn-focus>
</vn-autocomplete>
<vn-textfield
vn-one
label="Phone"
ng-model="workerPhone.phone">
</vn-textfield>
<vn-none>
<vn-icon-button
vn-tooltip="Remove phone"
icon="delete"
ng-click="model.remove($index)"
tabindex="-1">
</vn-icon-button>
</vn-none>
</vn-horizontal>
<vn-one>
<vn-icon-button
vn-bind="+"
vn-tooltip="Add phone"
icon="add_circle"
ng-click="$ctrl.add()">
</vn-icon-button>
</vn-one>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,48 @@
import ngModule from '../module';
class Controller {
constructor($scope) {
this.$scope = $scope;
}
get worker() {
return this._worker;
}
set worker(value) {
this._worker = value;
if (value)
this.setLink(value);
}
setLink(value) {
this.$scope.$applyAsync(()=> {
this.$scope.model.link = {userFk: value.userFk};
this.$scope.model.refresh();
});
}
onSubmit() {
this.$scope.watcher.check();
this.$scope.model.save().then(() => {
this.$scope.watcher.updateOriginalData();
this.$scope.watcher.notifySaved();
this.card.reload();
});
}
add() {
this.$scope.model.insert();
}
}
Controller.$inject = ['$scope'];
ngModule.component('vnWorkerPhones', {
template: require('./index.html'),
controller: Controller,
require: {card: '^vnWorkerCard'},
bindings: {
worker: '<'
}
});

View File

@ -0,0 +1,26 @@
import './index';
describe('Component vnWorkerPhones', () => {
let controller;
beforeEach(angular.mock.module('worker', $translateProvider => {
$translateProvider.translations('en', {});
}));
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
let $scope = $rootScope.$new();
controller = $componentController('vnWorkerPhones', $scope);
controller.$scope.model = {link: 1};
controller.$scope.$applyAsync = () => {};
}));
describe('setLink()', () => {
it('set the link in the model and refreshes it', () => {
spyOn(controller.$scope, '$applyAsync');
let value = {userFk: 106};
controller.setLink(value);
expect(controller.$scope.$applyAsync).toHaveBeenCalledWith(jasmine.any(Function));
});
});
});

View File

@ -0,0 +1,4 @@
Phones: Teléfonos
Type: Tipo
Remove phone: Eliminar teléfono
Add phone: Añadir teléfono

View File

@ -7,7 +7,8 @@
{"state": "worker.card.basicData", "icon": "settings"},
{"state": "worker.card.pbx", "icon": "icon-pbx"},
{"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.timeControl", "icon": "access_time"}
{"state": "worker.card.timeControl", "icon": "access_time"},
{"state": "worker.card.phones", "icon": "contact_phone"}
],
"routes": [
{
@ -79,6 +80,15 @@
"state": "worker.department",
"component": "vn-worker-department",
"description": "Departments"
},
{
"url": "/phones",
"state": "worker.card.phones",
"component": "vn-worker-phones",
"description": "Phones",
"params": {
"worker": "$ctrl.worker"
}
}
]
}

View File

@ -12,8 +12,9 @@
<vn-label-value label="Department"
value="{{worker.department.department.name}}">
</vn-label-value>
<vn-label-value label="Phone"
value="{{worker.phone}}">
<vn-label-value ng-repeat = "phone in worker.phones"
label="Phone"
value="{{phone.phone}}">
</vn-label-value>
</vn-one>
<vn-one>

View File

@ -50,6 +50,12 @@ class Controller {
relation: 'department'
}
}
}, {
relation: 'phones',
scope: {
fields: ['phone'],
order: 'typeFk ASC'
}
}
]
};

View File

@ -5,6 +5,13 @@
"senderMail": "nocontestar@verdnatura.es",
"senderName": "Verdnatura"
},
"pdf": {
"format": "A4",
"border": "1.5cm",
"footer": {
"height": "55px"
}
},
"mysql": {
"host": "localhost",
"port": 3306,

View File

@ -17,6 +17,7 @@
{"type": "report", "name": "rpt-zone"},
{"type": "report", "name": "rpt-route"},
{"type": "report", "name": "rpt-lcr"},
{"type": "report", "name": "rpt-item-label"},
{"type": "static", "name": "email-header"},
{"type": "static", "name": "email-footer"},
{"type": "static", "name": "report-header"},

View File

@ -2,9 +2,9 @@ const Vue = require('vue');
const VueI18n = require('vue-i18n');
const renderer = require('vue-server-renderer').createRenderer();
const fs = require('fs-extra');
// const pdf = require('phantom-html2pdf');
const pdf = require('html-pdf');
const juice = require('juice');
const config = require('./config');
Vue.use(VueI18n);
@ -104,13 +104,11 @@ module.exports = {
async toPdf(name, ctx) {
const html = await this.render(name, ctx);
const options = {
format: 'A4',
border: '1.5cm',
footer: {
height: '55px',
}
};
let options = config.pdf;
const optionsPath = `${this.path}/${name}/options.json`;
if (fs.existsSync(optionsPath))
options = Object.assign(options, require(optionsPath));
return new Promise(resolve => {
pdf.create(html, options).toStream((err, stream) => {

View File

@ -18,6 +18,7 @@
"juice": "^5.0.1",
"mysql2": "^1.6.5",
"nodemailer": "^4.7.0",
"qrcode": "^1.4.2",
"strftime": "^0.10.0",
"vue": "^2.6.7",
"vue-i18n": "^8.8.2",

View File

@ -0,0 +1,8 @@
const CssReader = require(`${appPath}/lib/cssReader`);
module.exports = new CssReader([
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/report.css`,
`${appPath}/common/css/misc.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,88 @@
* {
box-sizing: border-box;
}
.label {
font-size: 1.2em;
}
.barcode {
float: left;
width: 40%;
}
.barcode h1 {
text-align: center;
font-size: 1.8em;
margin: 0 0 10px 0
}
.barcode .image {
text-align: center
}
.barcode .image img {
width: 170px
}
.data {
float: left;
width: 60%;
}
.data .header {
background-color: #000;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
margin-bottom: 25px;
text-align: right;
font-size: 1.2em;
padding: 0.2em;
color: #FFF
}
.data .color,
.data .producer {
text-transform: uppercase;
text-align: right;
font-size: 1.5em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.data .producer {
text-justify: inter-character;
}
.data .details {
border-top: 4px solid #000;
padding-top: 2px;
}
.data .details .package {
padding-right: 5px;
float: left;
width: 50%;
}
.package .packing,
.package .dated,
.package .labelNumber {
text-align: right
}
.package .packing {
font-size: 1.8em;
font-weight: 400
}
.data .details .size {
background-color: #000;
text-align: center;
font-size: 3em;
padding: 0.2em 0;
float: left;
width: 50%;
color: #FFF
}

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="es">
<body>
<section class="container" id="report">
<section class="label">
<section class="barcode">
<h1>{{item.id}}</h1>
<section class="image">
<img v-bind:src="barcode"/>
</section>
</section>
<section class="data">
<section class="header">{{item.name}}</section>
<section class="color">{{tags.color}}</section>
<section class="producer">{{tags.producer}}</section>
<section class="details">
<section class="package">
<section class="packing">{{packing()}}</section>
<section class="dated">{{dated}}</section>
<section class="labelNumber">{{labelPage()}}</section>
</section>
<section class="size">{{item.size}}</section>
</section>
</section>
</section>
</section>
</body>
</html>

View File

@ -0,0 +1,83 @@
const database = require(`${appPath}/lib/database`);
const UserException = require(`${appPath}/lib/exceptions/userException`);
const strftime = require('strftime');
const qrcode = require('qrcode');
module.exports = {
name: 'rpt-item-label',
async asyncData(ctx, params) {
Object.assign(this, this.methods);
if (!params.itemId)
throw new UserException('No item id specified');
if (!params.warehouseId)
throw new UserException('No warehouse id specified');
const data = {
item: await this.fetchItem(params.itemId, params.warehouseId),
tags: await this.fetchItemTags(params.itemId),
barcode: await this.getBarcodeBase64(params.itemId),
labelNumber: params.labelNumber,
totalLabels: params.totalLabels
};
return data;
},
computed: {
dated() {
return strftime('%W/%d', new Date());
}
},
methods: {
fetchItem(id, warehouseId) {
return database.pool.query(
`SELECT
i.id,
i.name,
i.stems,
i.size,
b.packing
FROM vn.item i
JOIN cache.last_buy clb ON clb.item_id = i.id
JOIN vn.buy b ON b.id = clb.buy_id
JOIN vn.entry e ON e.id = b.entryFk
WHERE i.id = ? AND clb.warehouse_id = ?`, [id, warehouseId])
.then(([rows]) => {
if (rows.length == 0)
throw new UserException(`Item #${id} not found on warehouse #${warehouseId}`);
return rows[0];
});
},
fetchItemTags(itemId) {
return database.pool.query(
`SELECT t.code, t.name, it.value
FROM vn.itemTag it
JOIN vn.tag t ON t.id = it.tagFk
WHERE it.itemFk = ?`, [itemId])
.then(([rows]) => {
const tags = {};
rows.forEach(row => tags[row.code] = row.value);
return tags;
});
},
getBarcodeBase64(itemId) {
return qrcode.toDataURL(itemId, {margin: 0});
},
packing() {
const stems = this.item.stems ? this.item.stems : 1;
return `${this.item.packing}x${stems}`;
},
labelPage() {
const labelNumber = this.labelNumber ? this.labelNumber : 1;
const totalLabels = this.totalLabels ? this.totalLabels : 1;
return `${labelNumber}/${totalLabels}`;
}
},
components: {
'report-header': require('../report-header'),
'report-footer': require('../report-footer'),
},
};

View File

@ -0,0 +1,24 @@
module.exports = {
messages: {
es: {
title: 'Recibo',
date: 'Fecha',
payed: 'En {0}, a {1} de {2} de {3}',
client: 'Cliente {0}',
months: [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre'
]
},
},
};

View File

@ -0,0 +1,10 @@
{
"format": "A4",
"orientation": "landscape",
"width": "10.4cm",
"height": "4.8cm",
"border": "0cm",
"footer": {
"height": "0"
}
}